My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

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

day11/runner: extract Run_in_layers into its own library

The runner is the intersection of day11_layer (overlayfs lowerdir
planning, stack merging, LRU touching) and day11_container (Overlay,
Runc, Oci_spec). Both leaf libraries deliberately don't depend on
each other; this small library is the explicit place where they
meet. Putting Run_in_layers into either of those libraries would
have forced that library to depend on the other, breaking its
isolation.

The new library is one file plus a dune and a README. Its dep set
is exactly day11_container, day11_exec, day11_layer — no
opam-format, no day11_graph, no day11_build, no day11_opam_layer.

Run_in_layers.run's signature dropped its Types.build_env
parameter in favour of taking ~base directly. The function only
ever used benv.base.dir, so the build_env wrapper was overkill;
removing it lets the file have zero references to anything in
day11_build, which is what made the move possible.

Callers updated:

- day11/build/build_layer.ml — calls
Day11_runner.Run_in_layers.run env ~base:benv.base ~build_dirs
~prep_upper spec

- day11/build/dune — adds day11_runner

- day11/doc/test/test_generate_docs.ml — was already a direct
caller of Run_in_layers.run; updated to use the new module path
and the new ~base signature

- day11/doc/test/dune — adds day11_runner and day11_opam_layer
(the test was already using the latter implicitly)

day11_doc and day11_jtw still go through Build_layer.build for the
opam-flavoured orchestration. They're not yet direct consumers of
the runner — that would be a follow-up refactor that lets each of
them call Day11_runner.Run_in_layers.run with their own spec
construction. The runner library is now in place to support that
when/if it happens.

12 test suites pass, 196 tests. End-to-end smoke build of
logs.0.10.0 succeeds; layer.json shows the prep_upper timing
phase, build.json sidecar is written correctly.

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

+188 -98
+2 -2
day11/build/build_layer.ml
··· 197 197 let spec = opam_build_spec ~cmd:strategy.cmd 198 198 ~mounts:all_mounts ~uid:benv.uid ~gid:benv.gid 199 199 in 200 - match Run_in_layers.run env benv ~build_dirs:dep_dirs 201 - ~prep_upper spec with 200 + match Day11_runner.Run_in_layers.run env ~base:benv.base 201 + ~build_dirs:dep_dirs ~prep_upper spec with 202 202 | Ok (run, upper, timing) -> 203 203 strategy.cleanup env upper; 204 204 let _exit_code =
+2 -1
day11/build/dune
··· 1 1 (library 2 2 (name day11_build) 3 3 (libraries day11_container day11_exec day11_graph 4 - day11_layer day11_opam day11_opam_layer day11_solver_pool 4 + day11_layer day11_opam day11_opam_layer day11_runner 5 + day11_solver_pool 5 6 bos dockerfile eio fpath opam-format rresult yojson unix))
+27 -38
day11/build/run_in_layers.ml day11/runner/run_in_layers.ml
··· 1 - let src = Logs.Src.create "day11.build.run_in_layers" ~doc:"Run in layered container" 1 + let src = Logs.Src.create "day11.runner.run_in_layers" 2 + ~doc:"Run a command in a layered container" 2 3 module Log = (val Logs.src_log src) 3 4 4 5 let ( let* ) r f = match r with Ok v -> f v | Error _ as e -> e ··· 6 7 let mkdir path = 7 8 Bos.OS.Dir.create ~path:true path |> ignore 8 9 9 - let _timed name f = 10 - let t0 = Unix.gettimeofday () in 11 - let r = f () in 12 - let elapsed = Unix.gettimeofday () -. t0 in 13 - if elapsed > 0.1 then 14 - Log.info (fun m -> m "%s: %.3fs" name elapsed); 15 - r 16 - 17 - (** Like [timed] but also stores elapsed time in a ref *) 10 + (** Like [timed] but also stores elapsed time in a ref. *) 18 11 let timed_to name dst f = 19 12 let t0 = Unix.gettimeofday () in 20 13 let r = f () in ··· 24 17 Log.info (fun m -> m "%s: %.3fs" name elapsed); 25 18 r 26 19 27 - let run env (benv : Types.build_env) 20 + let run env ~(base : Day11_layer.Base.t) 28 21 ~build_dirs ?prep_upper (spec : Day11_container.Oci_spec.t) = 29 22 let t_total = Unix.gettimeofday () in 30 23 let t_merge = ref 0. in ··· 33 26 let t_runc = ref 0. in 34 27 let t_umount = ref 0. in 35 28 let t_cleanup = ref 0. in 36 - let base_fs = Fpath.add_seg benv.base.dir "fs" in 29 + let base_fs = Fpath.add_seg base.dir "fs" in 37 30 let temp_dir = 38 31 let tmp = Fpath.v (Filename.get_temp_dir_name ()) in 39 32 let name = Printf.sprintf "day11_run_%06x" ··· 47 40 let lower = Fpath.(temp_dir / "lower") in 48 41 List.iter mkdir [ upper; work; merged ]; 49 42 (* Hybrid lowerdir layout. The classic mount(2) options string is 50 - capped at PAGE_SIZE (typically 4096 bytes), which limits how many 51 - dep layers we can pass as separate lowerdirs. The hybrid: 43 + capped at PAGE_SIZE (typically 4096 bytes), which limits how 44 + many dep layers we can pass as separate lowerdirs. The hybrid: 52 45 53 - 1. Try to fit all dep layers as individual lowerdirs in the mount 54 - options. For typical opam packages (<60 deps with current paths, 55 - ~160 with /c-style short paths) this works directly. 46 + 1. Try to fit all dep layers as individual lowerdirs in the 47 + mount options. For typical opam packages (<60 deps with 48 + long /home/jjl25/cache/... paths, ~160 with /c-style short 49 + paths) this works directly. 56 50 57 51 2. If the option string would exceed the budget, keep as many 58 52 layers as possible as separate lowerdirs and cp-merge the 59 - excess into one [lower/] dir, which becomes one extra lowerdir. 53 + excess into one [lower/] dir, which becomes one extra 54 + lowerdir. 60 55 61 - Multi-lower is correct: per the kernel docs, overlayfs DOES merge 62 - directories across multiple lowers — see docs.kernel.org/ 63 - filesystems/overlayfs.html: "Where both upper and lower objects 64 - are directories, a merged directory is formed". The 65 - trusted.overlay.opaque xattr only takes effect when set on the 66 - UPPER layer, not on lowers, so layers carrying opaque=y from a 67 - prior overlay-build don't cause shadowing here. *) 56 + Multi-lower is correct: per the kernel docs, overlayfs DOES 57 + merge directories across multiple lowers — see 58 + docs.kernel.org/filesystems/overlayfs.html: "Where both upper 59 + and lower objects are directories, a merged directory is 60 + formed". The trusted.overlay.opaque xattr only takes effect 61 + when set on the UPPER layer, not on lowers, so layers carrying 62 + opaque=y from a prior overlay-build don't cause shadowing. *) 68 63 (* Mark every dep layer as recently used for LRU eviction. *) 69 64 List.iter Day11_layer.Last_used.touch build_dirs; 70 - (* Compute the byte budget for plan_lowerdir. The overlayfs mount(2) 71 - options string is capped at PAGE_SIZE; we leave ~96 bytes of 72 - headroom below 4096. Fixed overhead is the keyword, the base 73 - entry (plus leading colon), and the upper/work options. *) 74 65 let dep_entry_cost d = 75 66 String.length (Fpath.to_string Fpath.(d / "fs")) + 1 (* colon *) 76 67 in ··· 103 94 | Error (`Msg e) -> 104 95 Log.err (fun m -> m "stack.merge failed: %s" e)) 105 96 end; 106 - (* layer_fs_dirs is the list of dep lowers in the order used in the 107 - overlayfs mount (separate first, then merged-lower if any). It 108 - is also passed to [prep_upper] so domain-aware callers can read 109 - per-dep state from the lowers if they need to. *) 97 + (* layer_fs_dirs is the list of dep lowers in the order used in 98 + the overlayfs mount (separate first, then merged-lower if any). 99 + It is also passed to [prep_upper] so domain-aware callers can 100 + read per-dep state from the lowers if they need to. *) 110 101 let layer_fs_dirs = 111 102 List.map (fun d -> Fpath.(d / "fs")) separate_dirs 112 103 @ (if did_merge then [ lower ] else []) ··· 119 110 ignore (Bos.OS.File.delete Fpath.(temp_dir / "config.json")) 120 111 in 121 112 (* Caller-supplied prep work on the upper, before the mount. 122 - This is where opam-aware callers write switch-state, chown 123 - /home, mkdir mount points etc. — all the domain-specific 124 - work that used to live in this function. *) 113 + This is where domain-aware callers seed switch state, chown 114 + home directories, mkdir mount points, etc. *) 125 115 (match prep_upper with 126 116 | None -> () 127 117 | Some f -> ··· 159 149 in 160 150 (* Always clean up internals — only upper survives *) 161 151 timed_to "cleanup internals" t_cleanup (fun () -> cleanup_internals ()); 162 - let timing : Day11_layer.Meta.timing = [ 152 + let timing = [ 163 153 "merge", !t_merge; 164 154 "prep_upper", !t_prep; 165 155 "overlay_mount", !t_mount; 166 156 "runc_run", !t_runc; 167 157 "overlay_umount", !t_umount; 168 158 "cleanup", !t_cleanup; 169 - (* "extract" is filled in by build_layer *) 170 159 "total", Unix.gettimeofday () -. t_total; 171 160 ] in 172 161 match run_result with
-53
day11/build/run_in_layers.mli
··· 1 - (** Run a command in a container with layers stacked. 2 - 3 - Handles the generic container lifecycle: stack base + build 4 - layers as an overlayfs, optionally let the caller seed the upper 5 - with domain-specific files via [~prep_upper], mount, run command 6 - via runc, clean up. 7 - 8 - This module knows nothing about opam, opam switches, or doc 9 - generation. Domain-specific prep work (writing an opam 10 - switch-state file, chowning home dirs, mkdir'ing mount points 11 - for the container's bind mounts) is supplied by the caller via 12 - the [~prep_upper] callback. *) 13 - 14 - val run : 15 - Eio_unix.Stdenv.base -> 16 - Types.build_env -> 17 - build_dirs:Fpath.t list -> 18 - ?prep_upper:(upper:Fpath.t -> lowers:Fpath.t list -> unit) -> 19 - Day11_container.Oci_spec.t -> 20 - (Day11_exec.Run.t * Fpath.t * Day11_layer.Meta.timing, 21 - [> Rresult.R.msg ]) result 22 - (** [run env benv ~build_dirs ?prep_upper spec] mounts an overlayfs 23 - rootfs from [benv.base] + [build_dirs], optionally seeded by 24 - [prep_upper], then runs the container described by [spec]. 25 - 26 - {b The lifecycle:} 27 - + Make a temp dir with upper/work/merged/lower subdirs. 28 - + Touch every dep layer (LRU bookkeeping). 29 - + Plan the lowerdir layout via {!Day11_layer.Stack.plan_lowerdir}, 30 - cp-merging excess layers if the mount-options string would 31 - overflow PAGE_SIZE. 32 - + Call [prep_upper ~upper ~lowers] (if supplied) so the caller 33 - can seed the upper with whatever files / chowns / mkdirs the 34 - container will need. [lowers] is the final list of lowerdirs 35 - in their mount order (separate dep dirs first, then merged 36 - lower if any). The caller can read from these to populate the 37 - upper based on dep contents (e.g. an opam switch-state file). 38 - + Mount overlayfs. 39 - + Instantiate [spec] with the merged path as the rootfs and 40 - write [config.json] into the bundle dir. 41 - + Run runc. 42 - + Umount and clean up everything except [upper], which the 43 - caller takes ownership of. 44 - 45 - [spec] is a fully-described container template — every field 46 - except the rootfs path is baked in. {!Day11_container.Oci_spec} 47 - documents the defaults; the caller is responsible for choosing 48 - cwd, env, hostname, network, mounts, and argv. 49 - 50 - Returns [(run_result, upper_dir, timing)] on success. [timing] 51 - records how long each phase took (merge, prep_upper, overlay 52 - mount, runc run, cleanup, etc.). The caller is responsible for 53 - extracting what they need from [upper_dir] and cleaning it up. *)
+1 -1
day11/doc/test/dune
··· 11 11 (executable 12 12 (name test_generate_docs) 13 13 (libraries day11_build day11_container day11_doc day11_exec day11_layer 14 - day11_solver day11_test_util 14 + day11_opam_layer day11_runner day11_solver day11_test_util 15 15 alcotest astring bos eio_main fpath opam-format)) 16 16 17 17 (executable
+1 -3
day11/doc/test/test_generate_docs.ml
··· 94 94 ] in 95 95 let build_dirs = List.map 96 96 (Day11_opam_layer.Build.dir ~os_dir) all_builds in 97 - let benv = Day11_build.Types.make_build_env 98 - ~base ~os_dir ~uid:1000 ~gid:1000 () in 99 97 let spec = Day11_build.Build_layer.opam_build_spec 100 98 ~cmd:voodoo_cmd ~mounts ~uid:1000 ~gid:1000 in 101 99 let run, upper, _timing = 102 - Day11_build.Run_in_layers.run env benv ~build_dirs spec 100 + Day11_runner.Run_in_layers.run env ~base ~build_dirs spec 103 101 |> ok_or_fail "run voodoo" 104 102 in 105 103 Fun.protect
+93
day11/runner/README.md
··· 1 + # runner — Run a command in a layered container 2 + 3 + A small library that composes [`day11_layer`](../layer/) and 4 + [`day11_container`](../container/) to run an OCI container in an 5 + overlayfs rootfs assembled from a base layer plus a set of 6 + dependency layers. 7 + 8 + This is the intersection of "layered storage" and "container 9 + runtime". The two leaf libraries deliberately don't depend on each 10 + other; this library is the explicit place where they meet. 11 + 12 + ## What it does 13 + 14 + ```ocaml 15 + val Run_in_layers.run : 16 + Eio_unix.Stdenv.base -> 17 + base:Day11_layer.Base.t -> 18 + build_dirs:Fpath.t list -> 19 + ?prep_upper:(upper:Fpath.t -> lowers:Fpath.t list -> unit) -> 20 + Day11_container.Oci_spec.t -> 21 + (Day11_exec.Run.t * Fpath.t * (string * float) list, _) result 22 + ``` 23 + 24 + The single function `Run_in_layers.run`: 25 + 26 + 1. Makes a temp dir with overlayfs upper / work / merged subdirs. 27 + 2. Marks every dep layer as recently used (LRU bookkeeping). 28 + 3. Decides whether to pass dep layers as separate overlayfs lowerdirs 29 + (the fast path) or to cp-merge some of them into a single lower dir 30 + (the fallback when the kernel mount-options string would overflow 31 + PAGE_SIZE). Uses `Day11_layer.Stack.plan_lowerdir` for the 32 + decision. 33 + 4. Calls the optional `prep_upper` callback so the caller can seed 34 + the upper with whatever domain-specific files / chowns / mkdirs 35 + the container needs (e.g. an opam switch-state file, a 36 + `/home/$USER/odoc-out` mount point). 37 + 5. Mounts overlayfs at the temp dir's `merged/` subdir. 38 + 6. Instantiates the caller's `Oci_spec.t` template with the merged 39 + path as the rootfs and writes `config.json` into the bundle. 40 + 7. Runs runc, with `Fun.protect` cleanup. 41 + 8. Unmounts the overlay and removes everything except `upper/`. 42 + 9. Returns the run result, the upper directory, and a per-phase 43 + timing alist. 44 + 45 + ## What it does NOT do 46 + 47 + - Does not write `layer.json` or any sidecar metadata. That's the 48 + caller's job — the caller takes ownership of the returned 49 + `upper` and decides what to do with it. 50 + - Does not know about opam, opam switches, opam package builds, or 51 + documentation. The opam-flavoured spec defaults (cwd `/home/opam`, 52 + bash wrapping, network on, etc.) live in `Day11_build.Build_layer`, 53 + not here. 54 + - Does not know about cache layout, layer naming conventions, dep 55 + DAGs, or universes. Those are layer/opam concerns. 56 + 57 + ## Dependencies 58 + 59 + ``` 60 + (libraries day11_container day11_exec day11_layer 61 + bos eio fpath logs rresult unix) 62 + ``` 63 + 64 + Notably no `day11_opam_layer`, no `opam-format`, no `day11_build`. 65 + This library can be linked from any consumer that wants the 66 + "layered container runner" primitive. 67 + 68 + ## Why a separate library 69 + 70 + `Run_in_layers` is the intersection of `day11_layer` and 71 + `day11_container`, which deliberately don't depend on each other: 72 + 73 + - `day11_layer` is the on-disk data layer (read sidecars, plan 74 + overlayfs lowerdirs, do cp-merge); a doc-browsing tool can 75 + consume it without pulling in runc. 76 + - `day11_container` is the OCI runtime wrapper (Overlay, Runc, 77 + Oci_spec); a non-day11 container runner can consume it without 78 + pulling in the cache format. 79 + 80 + Putting `Run_in_layers` into either of those libraries would force 81 + that library to depend on the other, breaking its isolation. 82 + A separate `day11_runner` library is the right home: it depends 83 + on both, and is itself depended on by callers that need the 84 + combined primitive (currently `day11_build`). 85 + 86 + ## Testing 87 + 88 + There are no unit tests in this library — the function is too 89 + end-to-end to be meaningfully tested without runc and sudo. 90 + Integration tests live in `day11/container/test/test_integration.ml` 91 + where they exercise overlayfs mounts and runc directly. Those 92 + tests cover the same kernel/runc interactions 93 + `Run_in_layers.run` performs, just one step lower in the stack.
+4
day11/runner/dune
··· 1 + (library 2 + (name day11_runner) 3 + (libraries day11_container day11_exec day11_layer 4 + bos eio fpath logs rresult unix))
+58
day11/runner/run_in_layers.mli
··· 1 + (** Run a command in a container with layers stacked. 2 + 3 + Handles the generic container lifecycle: stack a base layer + 4 + dep layers as an overlayfs, optionally seed the upper with 5 + domain-specific files via [~prep_upper], mount, run the 6 + container described by an {!Day11_container.Oci_spec.t}, clean 7 + up. 8 + 9 + This module knows nothing about opam, opam switches, or doc 10 + generation. Any domain-specific concerns (writing an opam 11 + switch-state file, chowning home directories, mkdir'ing mount 12 + points for the container's bind mounts, choosing the cwd / 13 + hostname / env / argv that the contained process expects) are 14 + the caller's responsibility — supplied via [prep_upper] for 15 + upper-dir prep, and via the [Oci_spec.t] for everything else. *) 16 + 17 + val run : 18 + Eio_unix.Stdenv.base -> 19 + base:Day11_layer.Base.t -> 20 + build_dirs:Fpath.t list -> 21 + ?prep_upper:(upper:Fpath.t -> lowers:Fpath.t list -> unit) -> 22 + Day11_container.Oci_spec.t -> 23 + (Day11_exec.Run.t * Fpath.t * (string * float) list, 24 + [> Rresult.R.msg ]) result 25 + (** [run env ~base ~build_dirs ?prep_upper spec] mounts an overlayfs 26 + rootfs from [base] + [build_dirs], optionally seeded by 27 + [prep_upper], then runs the container described by [spec]. 28 + 29 + {b The lifecycle:} 30 + + Make a temp dir with upper/work/merged/lower subdirs. 31 + + Touch every dep layer (LRU bookkeeping via 32 + {!Day11_layer.Last_used}). 33 + + Plan the lowerdir layout via {!Day11_layer.Stack.plan_lowerdir}, 34 + cp-merging excess layers if the mount-options string would 35 + overflow PAGE_SIZE. 36 + + Call [prep_upper ~upper ~lowers] (if supplied) so the caller 37 + can seed the upper with whatever files / chowns / mkdirs the 38 + container will need. [lowers] is the final list of lowerdirs 39 + in their mount order (separate dep dirs first, then merged 40 + lower if any). The caller can read from these to populate 41 + the upper based on dep contents. 42 + + Mount overlayfs at [merged]. 43 + + Instantiate [spec] with [merged] as the rootfs path and 44 + write [config.json] into the bundle dir. 45 + + Run runc. 46 + + Umount and clean up everything except [upper], which the 47 + caller takes ownership of. 48 + 49 + [spec] is a fully-described container template — every field 50 + except the rootfs path is baked in. {!Day11_container.Oci_spec} 51 + documents the defaults; the caller is responsible for choosing 52 + cwd, env, hostname, network, mounts, and argv. 53 + 54 + Returns [(run_result, upper_dir, timing)] on success. [timing] 55 + is an alist of [(phase_name, seconds)] pairs in the order each 56 + phase ran (merge, prep_upper, overlay_mount, runc_run, 57 + overlay_umount, cleanup, total). The caller is responsible for 58 + extracting what they need from [upper_dir] and cleaning it up. *)