My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

day11: wire base image digest through build pipeline

When a profile has a pinned base_image_digest, it is now used:
- In build_hash: hash derived from digest, not tag name
- In Dockerfile generation: FROM debian@sha256:... instead of debian:bookworm
- Saved on disk alongside the base layer for future loads
- All callers (cmd_batch, cmd_build) pass profile digest through

The APIs are designed to be callable from ocaml-docs-ci:
- Profile.resolve_base_digest: query Docker registry (no pull)
- Base.build_hash ~digest: deterministic hash from digest
- Base.build ~digest: build with pinned image

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+66 -31
+2 -2
day11/bin/cmd_batch.ml
··· 208 208 let base_opt = Day11_opam_build.Base.load_cached ~cache_dir 209 209 ~os_distribution ~os_version in 210 210 let base_hash = Day11_opam_build.Base.build_hash 211 - ~os_distribution ~os_version ~arch in 211 + ~os_distribution ~os_version ~arch ?digest:profile.base_image_digest () in 212 212 (* Build DAG — no Eio needed *) 213 213 let cache = Day11_opam_build.Hash_cache.create ~find_opam ?patches () in 214 214 let nodes = Day11_opam_build.Dag.build_dag cache ··· 278 278 (match Day11_opam_build.Base.build env ~cache_dir 279 279 ~os_distribution ~os_version ~arch 280 280 ~opam_repositories:(List.map Fpath.v opam_repositories) ~uid ~gid 281 - () with 281 + ?digest:profile.base_image_digest () with 282 282 | Ok base -> base 283 283 | Error (`Msg e) -> 284 284 Printf.eprintf "Base image build failed: %s\n%!" e;
+3 -2
day11/bin/cmd_build.ml
··· 46 46 let build_solutions = [ (target, solution) ] in 47 47 let base_hash = Day11_opam_build.Base.build_hash 48 48 ~os_distribution:profile.os_distribution 49 - ~os_version:profile.os_version ~arch:profile.arch in 49 + ~os_version:profile.os_version ~arch:profile.arch 50 + ?digest:profile.base_image_digest () in 50 51 let nodes = Day11_opam_build.Dag.build_dag cache 51 52 ~base_hash build_solutions in 52 53 Printf.printf "DAG: %d nodes\n%!" (List.length nodes); ··· 96 97 ~os_distribution:profile.os_distribution 97 98 ~os_version:profile.os_version ~arch:profile.arch 98 99 ~opam_repositories:(List.map Fpath.v opam_repositories) 99 - ~uid ~gid () with 100 + ~uid ~gid ?digest:profile.base_image_digest () with 100 101 | Ok base -> base 101 102 | Error (`Msg e) -> 102 103 Printf.eprintf "Base image build failed: %s\n%!" e; exit 1)
+37 -11
day11/opam_build/base.ml
··· 3 3 4 4 let hash ~image = Day11_layer.Hash.base_hash ~image 5 5 6 - let build_hash ~os_distribution ~os_version ~arch:_ = 7 - let image = Printf.sprintf "%s:%s" os_distribution os_version in 8 - hash ~image 6 + let build_hash ~os_distribution ~os_version ~arch:_ ?digest () = 7 + match digest with 8 + | Some d -> hash ~image:d 9 + | None -> 10 + let image = Printf.sprintf "%s:%s" os_distribution os_version in 11 + hash ~image 9 12 10 13 let make_base_layer ~image ~base_dir : Day11_layer.Base.t = 11 14 { hash = hash ~image; dir = base_dir; image } 15 + 16 + let digest_file base_dir = Fpath.(base_dir / "base-digest") 17 + 18 + let save_digest base_dir digest = 19 + ignore (Bos.OS.File.write (digest_file base_dir) digest) 20 + 21 + let load_digest base_dir = 22 + match Bos.OS.File.read (digest_file base_dir) with 23 + | Ok d -> Some (String.trim d) 24 + | Error _ -> None 12 25 13 26 let ensure env ~cache_dir ~image = 14 27 let base_dir = Fpath.(cache_dir / "base") in ··· 63 76 @@ run "install -m 755 _build/default/bin/main.exe /usr/local/bin/opam-build" 64 77 65 78 let generate_dockerfile ~os_distribution ~os_version ~arch ~uid ~gid 66 - ~repo_count ~has_opam_build_bin = 79 + ~repo_count ~has_opam_build_bin ?digest () = 67 80 let open Dockerfile in 68 - let base_image = Printf.sprintf "%s:%s" os_distribution os_version in 81 + let base_image = match digest with 82 + | Some d -> Printf.sprintf "%s@%s" os_distribution d 83 + | None -> Printf.sprintf "%s:%s" os_distribution os_version 84 + in 69 85 let plat = platform arch in 70 86 (* Stage 1: download opam binary *) 71 87 let stage1 = ··· 192 208 let load_cached ~cache_dir ~os_distribution ~os_version = 193 209 let base_dir = Fpath.(cache_dir / "base") in 194 210 let marker = Fpath.(base_dir / "fs" / "usr") in 195 - let image = Printf.sprintf "%s:%s" os_distribution os_version in 196 - if Bos.OS.Dir.exists marker |> Result.get_ok then 211 + if Bos.OS.Dir.exists marker |> Result.get_ok then begin 212 + (* Use stored digest if available, otherwise fall back to tag *) 213 + let image = match load_digest base_dir with 214 + | Some d -> d 215 + | None -> Printf.sprintf "%s:%s" os_distribution os_version 216 + in 197 217 Some (make_base_layer ~image ~base_dir) 198 - else 218 + end else 199 219 None 200 220 201 221 let build env ~cache_dir ~os_distribution ~os_version ~arch 202 - ~opam_repositories ~uid ~gid () = 222 + ~opam_repositories ~uid ~gid ?digest () = 203 223 let base_dir = Fpath.(cache_dir / "base") in 204 224 let marker = Fpath.(base_dir / "fs" / "usr") in 205 225 let image = Printf.sprintf "%s:%s" os_distribution os_version in ··· 232 252 let dockerfile = 233 253 generate_dockerfile ~os_distribution ~os_version ~arch ~uid ~gid 234 254 ~repo_count:(List.length opam_repositories) 235 - ~has_opam_build_bin 255 + ~has_opam_build_bin ?digest () 236 256 in 237 257 let dockerfile_path = Fpath.(temp_dir / "Dockerfile") in 238 258 Bos.OS.File.write dockerfile_path ··· 267 287 Fpath.(base_dir / "fs" / "home" / "opam" 268 288 / ".opam" / "repo" 269 289 / "state-*.cache")))); 270 - Ok (make_base_layer ~image ~base_dir) 290 + (* Save the digest so future loads use it for the hash *) 291 + (match digest with 292 + | Some d -> save_digest base_dir d 293 + | None -> ()); 294 + let image_for_hash = match digest with 295 + | Some d -> d | None -> image in 296 + Ok (make_base_layer ~image:image_for_hash ~base_dir) 271 297 | Error _ as e -> e) 272 298 | `Exited n -> 273 299 (* Preserve log and Dockerfile before deleting temp dir *)
+24 -16
day11/opam_build/base.mli
··· 3 3 Builds and caches the root filesystem layer that all package builds 4 4 start from. The base contains a Debian image with opam, build tools, 5 5 and pre-initialised opam repositories. Docker is used to produce the 6 - image, which is then imported as a layer for overlayfs use. *) 6 + image, which is then imported as a layer for overlayfs use. 7 7 8 - (** Ensure a base layer exists for the given Docker [image] tag, building 9 - and importing it if not already cached. *) 8 + When a [digest] is provided (e.g. [sha256:abc123...] from the 9 + profile), the base image is pulled by digest for reproducibility. 10 + The digest is saved alongside the base layer so future loads use 11 + the same hash. *) 12 + 10 13 val ensure : 11 14 Eio_unix.Stdenv.base -> 12 15 cache_dir:Fpath.t -> 13 16 image:string -> 14 17 (Day11_layer.Base.t, [> Rresult.R.msg ]) result 18 + (** Ensure a base layer exists for the given Docker [image] tag, 19 + building and importing it if not already cached. *) 15 20 16 - (** Build a base layer from scratch using the given OS distribution, 17 - version, architecture, and opam repositories. The [uid] and [gid] 18 - are set as the non-root user inside the image. *) 19 21 val build : 20 22 Eio_unix.Stdenv.base -> 21 23 cache_dir:Fpath.t -> ··· 25 27 opam_repositories:Fpath.t list -> 26 28 uid:int -> 27 29 gid:int -> 30 + ?digest:string -> 28 31 unit -> 29 32 (Day11_layer.Base.t, [> Rresult.R.msg ]) result 33 + (** Build a base layer from scratch. When [digest] is provided 34 + (e.g. from {!Day11_batch.Profile.base_image_digest}), the 35 + Dockerfile uses [debian\@sha256:...] instead of [debian:bookworm] 36 + for a reproducible base. The digest is saved on disk so that 37 + {!load_cached} returns a base with the correct hash. *) 30 38 31 - (** Build the [opam-build] binary in a Docker container and cache it. 32 - Returns the path to the cached binary. If [opam_build_repo] is 33 - provided, that local checkout is used instead of cloning from GitHub. *) 34 39 val build_opam_build : 35 40 Eio_unix.Stdenv.base -> 36 41 cache_dir:Fpath.t -> ··· 38 43 ?opam_build_repo:Fpath.t -> 39 44 unit -> 40 45 (Fpath.t, [> Rresult.R.msg ]) result 46 + (** Build the [opam-build] binary in a Docker container and cache it. *) 41 47 42 - (** Return a read-only bind mount for the cached [opam-build] binary, 43 - or [None] if it has not been built yet. *) 44 48 val opam_build_mount : 45 49 cache_dir:Fpath.t -> Day11_container.Mount.t option 50 + (** Return a read-only bind mount for the cached [opam-build] binary. *) 46 51 47 - (** Compute a content hash for a Docker image tag. *) 48 52 val hash : image:string -> string 53 + (** Compute a content hash for a Docker image identifier (tag or digest). *) 49 54 50 - (** Compute a content hash from OS distribution, version, and architecture. *) 51 55 val build_hash : 52 - os_distribution:string -> os_version:string -> arch:string -> string 56 + os_distribution:string -> os_version:string -> arch:string -> 57 + ?digest:string -> unit -> string 58 + (** Compute the base hash. When [digest] is provided, it is used 59 + instead of the tag name, ensuring the hash changes when the 60 + upstream image is updated. *) 53 61 54 - (** Load a previously cached base layer, returning [None] if the cache 55 - directory does not contain a valid base image. *) 56 62 val load_cached : 57 63 cache_dir:Fpath.t -> 58 64 os_distribution:string -> os_version:string -> 59 65 Day11_layer.Base.t option 66 + (** Load a previously cached base layer. Uses the stored digest 67 + (if any) for the hash, otherwise falls back to the tag name. *)