My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

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

Add Layer.t type, refactor generate.ml to eliminate duplication and state

Layer library:
- Add layer.ml/mli with Layer.t = { hash; dir } and helpers
(of_hash, exists, is_ok, fs, meta_path, log_path)

Doc generate.ml:
- Extract shared DAG construction into build_internal_plan
- Extract dispatch logic into make_dispatch
- run = build_internal_plan + Dag_executor.execute
- plan_doc_dag = resolve_tools + build_internal_plan + make_dispatch
- build_tools_and_run = resolve_tools + run
- Remove compile_results mutable hashtable — all state on disk
- Remove current_build_hash ref — pass odoc_tool explicitly
- Use Layer.is_ok instead of ad-hoc layer_ok helper

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

+361 -283
+262 -282
day11/doc/generate.ml
··· 29 29 in 30 30 walk node 31 31 32 - (* Thin wrappers that extract DAG state and delegate to Doc_build primitives *) 32 + (* Stateless wrappers that delegate to Doc_build primitives. 33 + All state (which compile layers exist, dep relationships) is 34 + derived from the DAG structure and disk. *) 35 + 36 + module Layer = Day11_layer.Layer 33 37 34 - let compile_package env benv ~os_dir ~find_odoc_tool ~build_hash_blessed 35 - ~driver_tool ~compile_results ~dag_hash (node : build) = 38 + (** Find dep compile layer dirs by looking up each transitive build 39 + dep's compile/doc-all hash from the precomputed mapping, then 40 + checking if the layer exists on disk. *) 41 + let find_dep_compile_layers ~os_dir ~build_to_doc_hash (node : build) = 42 + let seen = Hashtbl.create 16 in 43 + List.iter (collect_transitive_deps seen) node.deps; 44 + Hashtbl.fold (fun dep_bh () acc -> 45 + match Hashtbl.find_opt build_to_doc_hash dep_bh with 46 + | Some doc_hash -> 47 + let layer = Layer.of_hash ~os_dir doc_hash in 48 + if Layer.is_ok layer then Layer.dir layer :: acc 49 + else acc 50 + | _ -> acc 51 + ) seen [] 52 + 53 + let compile_package env benv ~os_dir ~odoc_tool ~build_hash_blessed 54 + ~driver_tool ~build_to_doc_hash ~dag_hash (node : build) = 36 55 let blessed = match Hashtbl.find_opt build_hash_blessed node.hash with 37 56 | Some true -> true | _ -> false in 38 - match find_odoc_tool node.pkg with 39 - | None -> None 57 + match odoc_tool with 58 + | None -> false 40 59 | Some (odoc_tool : Tool.t) -> 41 60 let config : Doc_build.doc_config = 42 61 { driver_tool; odoc_tool; os_dir; blessed } in 43 62 let build_layer = Build.dir ~os_dir node in 44 - let seen = Hashtbl.create 16 in 45 - List.iter (collect_transitive_deps seen) node.deps; 46 - let dep_compile_layers = Hashtbl.fold (fun bh () acc -> 47 - match Hashtbl.find_opt compile_results bh with 48 - | Some bl -> Build.dir ~os_dir bl :: acc 49 - | None -> acc 50 - ) seen [] in 63 + let dep_compile_layers = 64 + find_dep_compile_layers ~os_dir ~build_to_doc_hash node in 51 65 match Doc_build.compile env benv ~config ~build_layer 52 66 ~dep_compile_layers ~hash:dag_hash node.pkg with 53 - | Ok _layer_dir -> 54 - let compile_node : build = 55 - { hash = dag_hash; pkg = node.pkg; deps = [ node ]; 56 - universe = Day11_solution.Universe.dummy } in 57 - Hashtbl.replace compile_results node.hash compile_node; 58 - Some compile_node 67 + | Ok _ -> true 59 68 | Error msg -> 60 69 Printf.printf " %s: compile FAILED (%s)\n%!" 61 70 (OpamPackage.to_string node.pkg) msg; 62 - None 71 + false 63 72 64 - let link_package env benv ~os_dir ~find_odoc_tool ~build_hash_blessed 65 - ~driver_tool ~compile_results ~doc_dep_hashes 66 - ~build_hash ~dag_hash (node : build) = 73 + let link_package env benv ~os_dir ~odoc_tool ~build_hash_blessed 74 + ~driver_tool ~build_to_doc_hash ~doc_dep_hashes 75 + ~build_hash ~compile_hash ~dag_hash (node : build) = 67 76 let blessed = match Hashtbl.find_opt build_hash_blessed node.hash with 68 77 | Some true -> true | _ -> false in 69 - match find_odoc_tool node.pkg, Hashtbl.find_opt compile_results build_hash with 70 - | None, _ | _, None -> None 71 - | Some (odoc_tool : Tool.t), Some compile_bl -> 78 + let compile_layer = Layer.of_hash ~os_dir compile_hash in 79 + if not (Layer.is_ok compile_layer) then false 80 + else 81 + match odoc_tool with 82 + | None -> false 83 + | Some (odoc_tool : Tool.t) -> 72 84 let config : Doc_build.doc_config = 73 85 { driver_tool; odoc_tool; os_dir; blessed } in 74 86 let build_layer = Build.dir ~os_dir node in 75 - let compile_layer = Build.dir ~os_dir compile_bl in 87 + let compile_layer = Layer.dir compile_layer in 76 88 let doc_dep_bhs = match Hashtbl.find_opt doc_dep_hashes build_hash with 77 89 | Some bhs -> bhs | None -> [] in 78 90 let dep_compile_layers = List.filter_map (fun bh -> 79 - match Hashtbl.find_opt compile_results bh with 80 - | Some bl -> Some (Build.dir ~os_dir bl) 81 - | None -> None 91 + match Hashtbl.find_opt build_to_doc_hash bh with 92 + | Some doc_hash -> 93 + let l = Layer.of_hash ~os_dir doc_hash in 94 + if Layer.is_ok l then Some (Layer.dir l) else None 95 + | _ -> None 82 96 ) doc_dep_bhs in 83 97 let html_dir = Fpath.(os_dir / "html") in 84 98 match Doc_build.link env benv ~config ~build_layer ~compile_layer 85 99 ~dep_compile_layers ~html_dir ~hash:dag_hash node.pkg with 86 - | Ok () -> 87 - Printf.printf " %s: linked\n%!" (OpamPackage.to_string node.pkg); 88 - Some 0 100 + | Ok () -> true 89 101 | Error msg -> 90 102 Printf.printf " %s: link FAILED (%s)\n%!" 91 103 (OpamPackage.to_string node.pkg) msg; 92 - None 104 + false 93 105 94 - let doc_all_package env benv ~os_dir ~find_odoc_tool ~build_hash_blessed 95 - ~driver_tool ~compile_results ~build_hash ~dag_hash (node : build) = 106 + let doc_all_package env benv ~os_dir ~odoc_tool ~build_hash_blessed 107 + ~driver_tool ~build_to_doc_hash ~dag_hash (node : build) = 96 108 let blessed = match Hashtbl.find_opt build_hash_blessed node.hash with 97 109 | Some true -> true | _ -> false in 98 - match find_odoc_tool node.pkg with 99 - | None -> None 110 + match odoc_tool with 111 + | None -> false 100 112 | Some (odoc_tool : Tool.t) -> 101 113 let config : Doc_build.doc_config = 102 114 { driver_tool; odoc_tool; os_dir; blessed } in 103 115 let build_layer = Build.dir ~os_dir node in 104 - let seen = Hashtbl.create 16 in 105 - List.iter (collect_transitive_deps seen) node.deps; 106 - let dep_compile_layers = Hashtbl.fold (fun bh () acc -> 107 - match Hashtbl.find_opt compile_results bh with 108 - | Some bl -> Build.dir ~os_dir bl :: acc 109 - | None -> acc 110 - ) seen [] in 116 + let dep_compile_layers = 117 + find_dep_compile_layers ~os_dir ~build_to_doc_hash node in 111 118 let html_dir = Fpath.(os_dir / "html") in 112 119 match Doc_build.doc_all env benv ~config ~build_layer 113 120 ~dep_compile_layers ~html_dir ~hash:dag_hash node.pkg with 114 - | Ok _layer_dir -> 115 - let doc_node : build = 116 - { hash = dag_hash; pkg = node.pkg; deps = [ node ]; 117 - universe = Day11_solution.Universe.dummy } in 118 - Hashtbl.replace compile_results build_hash doc_node; 119 - Printf.printf " %s: doc-all OK\n%!" (OpamPackage.to_string node.pkg); 120 - Some 0 121 + | Ok _ -> true 121 122 | Error msg -> 122 123 Printf.printf " %s: doc-all FAILED (%s)\n%!" 123 124 (OpamPackage.to_string node.pkg) msg; 124 - None 125 + false 126 + 127 + (* ── Internal: shared DAG construction ───────────────────────── *) 125 128 126 - let run env benv ~np ~os_dir ~(driver_tool : Tool.t) ~odoc_tools 127 - ~tool_source_dirs ~mounts 128 - ~run_log 129 - ~build_one ~nodes ~solutions ~blessing_maps:_ = 130 - (* Ensure HTML output directory exists *) 129 + (** Internal plan tables produced by [build_internal_plan]. 130 + Contains all immutable mappings needed for dispatch. *) 131 + type internal_plan = { 132 + all_nodes : build list; 133 + build_by_hash : (string, build) Hashtbl.t; 134 + build_to_doc_hash : (string, string) Hashtbl.t; 135 + build_hash_blessed : (string, bool) Hashtbl.t; 136 + doc_dep_hashes : (string, string list) Hashtbl.t; 137 + compile_to_build : (string, string) Hashtbl.t; 138 + doc_all_to_build : (string, string) Hashtbl.t; 139 + link_to_build : (string, string) Hashtbl.t; 140 + compile_set : (string, unit) Hashtbl.t; 141 + doc_all_set : (string, unit) Hashtbl.t; 142 + link_set : (string, unit) Hashtbl.t; 143 + tool_node_set : (string, unit) Hashtbl.t; 144 + find_odoc_tool_for_hash : string -> Tool.t option; 145 + driver_tool : Tool.t; 146 + } 147 + 148 + (** Build the doc DAG: compute compile/link/doc-all nodes with 149 + deterministic hashes, derive all dispatch tables. Pure function 150 + of the inputs — no mutable state escapes. *) 151 + let build_internal_plan ~os_dir ~(driver_tool : Tool.t) ~odoc_tools 152 + ~nodes ~solutions = 131 153 ignore (Bos.OS.Dir.create ~path:true Fpath.(os_dir / "html")); 132 - (* Collect all tool nodes for inclusion in the unified DAG *) 154 + (* Collect tool nodes *) 133 155 let tool_nodes = 134 156 let seen = Hashtbl.create 64 in 135 157 let add_nodes builds = ··· 142 164 List.iter (fun (_, (tool : Tool.t)) -> add_nodes tool.builds) odoc_tools; 143 165 Hashtbl.fold (fun _ n acc -> n :: acc) seen [] 144 166 in 145 - (* Find the final build node for each tool *) 146 167 let driver_final = List.find (fun (n : build) -> 147 168 String.equal n.hash driver_tool.hash) driver_tool.builds in 148 169 let odoc_finals = List.map (fun (compiler, (tool : Tool.t)) -> ··· 150 171 String.equal n.hash tool.hash) tool.builds in 151 172 (compiler, tool, final) 152 173 ) odoc_tools in 153 - (* Derive compiler per build node. First pass: walk DAG deps to find 154 - the concrete compiler (ocaml-base-compiler/variants/system). *) 174 + (* Derive compiler per build node *) 155 175 let node_compiler : (string, OpamPackage.t) Hashtbl.t = 156 176 Hashtbl.create (List.length nodes) in 157 177 let rec derive_compiler (node : build) = ··· 172 192 result 173 193 in 174 194 List.iter (fun node -> ignore (derive_compiler node)) nodes; 175 - (* find_odoc_tool: given a build node hash, return the matching odoc tool *) 195 + (* find_odoc_tool_for_hash: given a build node hash, return the matching odoc tool *) 176 196 let find_odoc_tool_for_hash build_hash = 177 197 match Hashtbl.find_opt node_compiler build_hash with 178 198 | None -> None ··· 189 209 OpamPackage.equal c compiler) odoc_finals 190 210 |> Option.map (fun (_, _, final) -> final) 191 211 in 192 - (* find_odoc_tool for prepare/compile/link: takes build_hash via thread-local ref. 193 - Safe because Eio fibers are cooperatively scheduled — no preemption between 194 - setting the ref and calling prepare_package. *) 195 - let current_build_hash = ref "" in 196 - let find_odoc_tool _pkg = find_odoc_tool_for_hash !current_build_hash in 197 - (* Build hash -> build node index *) 212 + (* Build indexes *) 198 213 let build_by_hash : (string, build) Hashtbl.t = 199 214 Hashtbl.create (List.length nodes) in 200 215 List.iter (fun (node : build) -> 201 - Hashtbl.replace build_by_hash node.hash node 202 - ) nodes; 203 - (* Per build hash: does it need separate compile+link? 204 - Compare build_deps (no {post}) vs doc_deps (with {post} + x-extra-doc-deps). *) 216 + Hashtbl.replace build_by_hash node.hash node) nodes; 205 217 let needs_split : (string, bool) Hashtbl.t = Hashtbl.create 64 in 206 218 List.iter (fun (_target, (result : Day11_solution.Solve_result.t)) -> 207 219 let compiler = find_compiler result.build_deps in ··· 213 225 Hashtbl.replace needs_split (pkg_s ^ ":" ^ compiler_s) true 214 226 ) result.build_deps 215 227 ) solutions; 216 - (* Reverse index: pkg_string -> list of (build_hash, build) *) 217 228 let pkg_to_builds : (string, (string * build) list) Hashtbl.t = 218 229 Hashtbl.create (List.length nodes) in 219 230 Hashtbl.iter (fun bh (node : build) -> ··· 222 233 with Not_found -> [] in 223 234 Hashtbl.replace pkg_to_builds pkg_s ((bh, node) :: existing) 224 235 ) build_by_hash; 225 - (* Build (pkg, compiler) -> build_hash index from solutions. 226 - Each solution has one compiler; every package in it gets that compiler. 227 - Also populate node_compiler for all build nodes. *) 228 236 let pkg_compiler_to_hash : (string * string, string) Hashtbl.t = 229 237 Hashtbl.create (List.length nodes) in 230 238 List.iter (fun (_target, (result : Day11_solution.Solve_result.t)) -> ··· 239 247 | Some builds -> 240 248 List.iter (fun (bh, _node) -> 241 249 Hashtbl.replace pkg_compiler_to_hash (pkg_s, compiler_s) bh; 242 - (* Only assign compiler to nodes that derive_compiler missed 243 - (leaf nodes with no DAG deps like ocaml-compiler) *) 244 250 (match compiler with 245 251 | Some c when not (Hashtbl.mem node_compiler bh) -> 246 252 Hashtbl.replace node_compiler bh c ··· 248 254 ) builds 249 255 ) solution 250 256 ) solutions; 251 - (* build_hash -> doc dep build hashes. 252 - For the link phase, we need to mount odoc output from doc_deps 253 - (which includes {post} deps and x-extra-doc-deps like odig), 254 - not just the build DAG deps. *) 255 257 let doc_dep_hashes : (string, string list) Hashtbl.t = Hashtbl.create 64 in 256 258 List.iter (fun (_target, (result : Day11_solution.Solve_result.t)) -> 257 259 let compiler = find_compiler result.build_deps in 258 260 let compiler_s = match compiler with 259 261 | Some c -> OpamPackage.to_string c | None -> "" in 260 - OpamPackage.Map.iter (fun pkg doc_deps_set -> 261 - let pkg_s = OpamPackage.to_string pkg in 262 - match Hashtbl.find_opt pkg_compiler_to_hash (pkg_s, compiler_s) with 263 - | None -> () 264 - | Some pkg_bh -> 265 - let dep_bhs = OpamPackage.Set.fold (fun dep_pkg acc -> 266 - let dep_s = OpamPackage.to_string dep_pkg in 267 - match Hashtbl.find_opt pkg_compiler_to_hash (dep_s, compiler_s) with 268 - | Some bh when not (String.equal bh pkg_bh) -> bh :: acc 269 - | _ -> acc 262 + OpamPackage.Map.iter (fun _pkg doc_deps_set -> 263 + let pkg_s = OpamPackage.to_string _pkg in 264 + let pkg_bh = match Hashtbl.find_opt pkg_compiler_to_hash 265 + (pkg_s, compiler_s) with 266 + | Some bh -> bh | None -> "" in 267 + if pkg_bh <> "" then 268 + let dep_bhs = OpamPackage.Set.fold (fun dep acc -> 269 + let dep_s = OpamPackage.to_string dep in 270 + match Hashtbl.find_opt pkg_compiler_to_hash 271 + (dep_s, compiler_s) with 272 + | Some bh -> bh :: acc 273 + | None -> acc 270 274 ) doc_deps_set [] in 271 275 Hashtbl.replace doc_dep_hashes pkg_bh dep_bhs 272 276 ) result.doc_deps 273 277 ) solutions; 274 - (* Resolve needs_split to build hashes *) 275 278 let needs_split_bh : (string, bool) Hashtbl.t = Hashtbl.create 64 in 276 279 Hashtbl.iter (fun key _ -> 277 280 match String.index_opt key ':' with ··· 283 286 | Some bh -> Hashtbl.replace needs_split_bh bh true 284 287 | None -> () 285 288 ) needs_split; 286 - (* Compute blessed per build hash. Each build node carries its 287 - universe. Compare against the blessed universe for its package. *) 288 289 let blessed_universes = Day11_batch.Blessing.compute_blessed_universes 289 290 (List.map (fun (t, (r : Day11_solution.Solve_result.t)) -> 290 291 (t, r.build_deps)) solutions) in ··· 296 297 Hashtbl.replace build_hash_blessed node.hash true 297 298 | _ -> () 298 299 ) nodes; 299 - (* Build doc DAG nodes with deterministic hashes. 300 - Hashes are computed bottom-up: each compile/doc-all hash includes 301 - the dep compile hashes, blessed status, build hash, and tool hash. 302 - This ensures DAG hashes match execution hashes exactly. *) 300 + (* Build doc DAG nodes with deterministic hashes *) 303 301 let compile_nodes : (string, build) Hashtbl.t = Hashtbl.create 64 in 304 302 let doc_all_nodes : (string, build) Hashtbl.t = Hashtbl.create 64 in 305 303 let link_nodes_list = ref [] in 306 - (* Memoized compile hash computation — walks build dep DAG bottom-up *) 307 304 let compile_hash_cache : (string, string) Hashtbl.t = Hashtbl.create 64 in 308 305 let rec compute_compile_hash (node : build) = 309 306 match Hashtbl.find_opt compile_hash_cache node.hash with ··· 315 312 | Some odoc_tool -> 316 313 Day11_layer.Hash.of_strings [ driver_tool.hash; odoc_tool.hash ] 317 314 | None -> "" in 318 - (* Collect dep compile hashes (recursive, memoized) *) 319 315 let seen = Hashtbl.create 16 in 320 316 List.iter (collect_transitive_deps seen) node.deps; 321 317 let dep_compile_hashes = Hashtbl.fold (fun dep_bh () acc -> ··· 333 329 Hashtbl.replace compile_hash_cache node.hash hash; 334 330 hash 335 331 in 336 - (* First pass: create compile or doc-all nodes with correct hashes *) 337 332 List.iter (fun (node : build) -> 338 333 match find_odoc_tool_for_hash node.hash, 339 334 find_odoc_final_for_hash node.hash with 340 335 | None, _ | _, None -> () 341 336 | Some _odoc_tool, Some odoc_final -> 342 337 let hash = compute_compile_hash node in 343 - (* Collect dep doc nodes for DAG edges *) 344 - let seen = Hashtbl.create 16 in 345 - List.iter (collect_transitive_deps seen) node.deps; 346 - let dep_docs = Hashtbl.fold (fun dep_bh () acc -> 347 - (* Will be filled in after all nodes created — use placeholder *) 348 - match Hashtbl.find_opt build_by_hash dep_bh with 349 - | Some _ -> dep_bh :: acc 350 - | None -> acc 351 - ) seen [] in 352 338 let dn : build = { hash; pkg = node.pkg; 353 339 deps = [ node; driver_final; odoc_final ]; 354 340 universe = Day11_solution.Universe.dummy } in 355 341 if Hashtbl.mem needs_split_bh node.hash then 356 342 Hashtbl.replace compile_nodes node.hash dn 357 343 else 358 - Hashtbl.replace doc_all_nodes node.hash dn; 359 - ignore dep_docs 344 + Hashtbl.replace doc_all_nodes node.hash dn 360 345 ) nodes; 361 - (* Patch deps: add transitive build deps' compile/doc-all nodes *) 362 346 let patch_doc_deps build_hash (dn : build) = 363 347 let build_node = Hashtbl.find build_by_hash build_hash in 364 348 let seen = Hashtbl.create 16 in ··· 371 355 | Some dn -> dn :: acc 372 356 | None -> acc) 373 357 ) seen [] in 374 - { dn with deps = dn.deps @ dep_docs; universe = Day11_solution.Universe.dummy } 358 + { dn with deps = dn.deps @ dep_docs } 375 359 in 376 360 let compile_snapshot = Hashtbl.fold (fun k v acc -> (k, v) :: acc) 377 361 compile_nodes [] in ··· 383 367 List.iter (fun (bh, dn) -> 384 368 Hashtbl.replace doc_all_nodes bh (patch_doc_deps bh dn) 385 369 ) doc_all_snapshot; 386 - (* Create link nodes for packages that need separate link *) 387 370 Hashtbl.iter (fun build_hash _cn -> 388 371 let build_node = Hashtbl.find build_by_hash build_hash in 389 372 let dep_compile_layers = ··· 416 399 let doc_all_list = Hashtbl.fold (fun _ dn acc -> dn :: acc) doc_all_nodes [] in 417 400 let all_doc_nodes = nodes @ tool_nodes @ compile_list 418 401 @ doc_all_list @ !link_nodes_list in 419 - Printf.printf " Doc DAG: %d build + %d tool + %d doc-all + %d compile + %d link nodes\n%!" 420 - (List.length nodes) (List.length tool_nodes) 421 - (List.length doc_all_list) (List.length compile_list) 422 - (List.length !link_nodes_list); 423 - Day11_lib.Run_log.write_dag_structure run_log all_doc_nodes; 424 - (* Track results *) 425 - let compile_results : (string, build) Hashtbl.t = Hashtbl.create 64 in 426 - let doc_count = Atomic.make 0 in 427 - let doc_html = Atomic.make 0 in 402 + (* Build immutable dispatch tables *) 403 + let build_to_doc_hash : (string, string) Hashtbl.t = Hashtbl.create 64 in 404 + List.iter (fun (build_hash, _cn) -> 405 + let cn = Hashtbl.find compile_nodes build_hash in 406 + Hashtbl.replace build_to_doc_hash build_hash cn.hash 407 + ) compile_snapshot; 408 + List.iter (fun (build_hash, _dn) -> 409 + let dn = Hashtbl.find doc_all_nodes build_hash in 410 + Hashtbl.replace build_to_doc_hash build_hash dn.hash 411 + ) doc_all_snapshot; 428 412 let compile_to_build : (string, string) Hashtbl.t = Hashtbl.create 64 in 429 413 List.iter (fun (build_hash, _cn) -> 430 414 let cn = Hashtbl.find compile_nodes build_hash in ··· 442 426 Hashtbl.replace link_to_build ln.hash build_node.hash 443 427 | _ -> () 444 428 ) !link_nodes_list; 445 - (* Index for dispatch *) 446 429 let compile_set = Hashtbl.create 64 in 447 - List.iter (fun (cn : build) -> Hashtbl.replace compile_set cn.hash cn) compile_list; 430 + List.iter (fun (cn : build) -> Hashtbl.replace compile_set cn.hash ()) compile_list; 448 431 let doc_all_set = Hashtbl.create 64 in 449 - List.iter (fun (dn : build) -> Hashtbl.replace doc_all_set dn.hash dn) doc_all_list; 432 + List.iter (fun (dn : build) -> Hashtbl.replace doc_all_set dn.hash ()) doc_all_list; 450 433 let link_set = Hashtbl.create 64 in 451 - List.iter (fun (ln : build) -> Hashtbl.replace link_set ln.hash ln) !link_nodes_list; 434 + List.iter (fun (ln : build) -> Hashtbl.replace link_set ln.hash ()) !link_nodes_list; 452 435 let tool_node_set = Hashtbl.create 64 in 453 436 List.iter (fun (n : build) -> 454 - Hashtbl.replace tool_node_set n.hash () 455 - ) tool_nodes; 456 - (* Priority: link=3, doc-all/compile=2, tool=1, build=0 *) 437 + Hashtbl.replace tool_node_set n.hash ()) tool_nodes; 438 + { all_nodes = all_doc_nodes; build_by_hash; build_to_doc_hash; 439 + build_hash_blessed; doc_dep_hashes; 440 + compile_to_build; doc_all_to_build; link_to_build; 441 + compile_set; doc_all_set; link_set; tool_node_set; 442 + find_odoc_tool_for_hash; driver_tool } 443 + 444 + (** Build a dispatch function from the plan tables. *) 445 + let make_dispatch benv ~os_dir ~(plan : internal_plan) ~tool_source_dirs 446 + ~mounts ~build_one = 447 + fun env (node : build) -> 448 + if Hashtbl.mem plan.compile_set node.hash then begin 449 + match Hashtbl.find_opt plan.compile_to_build node.hash with 450 + | None -> true 451 + | Some build_hash -> 452 + let build_node = Hashtbl.find plan.build_by_hash build_hash in 453 + let odoc_tool = plan.find_odoc_tool_for_hash build_hash in 454 + compile_package env benv ~os_dir ~odoc_tool 455 + ~build_hash_blessed:plan.build_hash_blessed 456 + ~driver_tool:plan.driver_tool ~build_to_doc_hash:plan.build_to_doc_hash 457 + ~dag_hash:node.hash build_node 458 + end else if Hashtbl.mem plan.doc_all_set node.hash then begin 459 + match Hashtbl.find_opt plan.doc_all_to_build node.hash with 460 + | None -> true 461 + | Some build_hash -> 462 + let build_node = Hashtbl.find plan.build_by_hash build_hash in 463 + let odoc_tool = plan.find_odoc_tool_for_hash build_hash in 464 + doc_all_package env benv ~os_dir ~odoc_tool 465 + ~build_hash_blessed:plan.build_hash_blessed 466 + ~driver_tool:plan.driver_tool ~build_to_doc_hash:plan.build_to_doc_hash 467 + ~dag_hash:node.hash build_node 468 + end else if Hashtbl.mem plan.link_set node.hash then begin 469 + match Hashtbl.find_opt plan.link_to_build node.hash with 470 + | None -> true 471 + | Some build_hash -> 472 + let build_node = Hashtbl.find plan.build_by_hash build_hash in 473 + let compile_hash = match Hashtbl.find_opt plan.build_to_doc_hash build_hash with 474 + | Some h -> h | None -> "" in 475 + let odoc_tool = plan.find_odoc_tool_for_hash build_hash in 476 + link_package env benv ~os_dir ~odoc_tool 477 + ~build_hash_blessed:plan.build_hash_blessed 478 + ~driver_tool:plan.driver_tool ~build_to_doc_hash:plan.build_to_doc_hash 479 + ~doc_dep_hashes:plan.doc_dep_hashes ~build_hash ~compile_hash 480 + ~dag_hash:node.hash build_node 481 + end else begin 482 + let name = OpamPackage.name node.pkg in 483 + match OpamPackage.Name.Map.find_opt name tool_source_dirs with 484 + | Some dir -> 485 + let src_mount = Day11_container.Mount.bind_ro ~src:dir "/home/opam/src" in 486 + let strategy = Day11_opam_build.Tools.source_dir_strategy node.pkg in 487 + (match Day11_opam_build.Build_layer.build env benv 488 + ~mounts:(src_mount :: mounts) node ~strategy () with 489 + | Day11_opam_build.Types.Success _ -> true | _ -> false) 490 + | None -> build_one node 491 + end 492 + 493 + (* ── Public API ──────────────────────────────────────────────── *) 494 + 495 + type node_kind = Build | Tool | Compile | Doc_all | Link 496 + 497 + type doc_plan = { 498 + all_nodes : Build.t list; 499 + node_kind : Build.t -> node_kind; 500 + build_one : Eio_unix.Stdenv.base -> Build.t -> bool; 501 + } 502 + 503 + let node_kind_of_plan (plan : internal_plan) (n : build) = 504 + if Hashtbl.mem plan.link_set n.hash then Link 505 + else if Hashtbl.mem plan.compile_set n.hash then Compile 506 + else if Hashtbl.mem plan.doc_all_set n.hash then Doc_all 507 + else if Hashtbl.mem plan.tool_node_set n.hash then Tool 508 + else Build 509 + 510 + let run env benv ~np ~os_dir ~(driver_tool : Tool.t) ~odoc_tools 511 + ~tool_source_dirs ~mounts 512 + ~run_log 513 + ~build_one ~nodes ~solutions ~blessing_maps:_ = 514 + let plan = build_internal_plan ~os_dir ~driver_tool ~odoc_tools 515 + ~nodes ~solutions in 516 + Printf.printf " Doc DAG: %d total nodes\n%!" (List.length plan.all_nodes); 517 + Day11_lib.Run_log.write_dag_structure run_log plan.all_nodes; 518 + let doc_count = Atomic.make 0 in 457 519 let node_priority (n : build) = 458 - if Hashtbl.mem link_set n.hash then 3 459 - else if Hashtbl.mem compile_set n.hash then 2 460 - else if Hashtbl.mem doc_all_set n.hash then 2 461 - else if Hashtbl.mem tool_node_set n.hash then 1 520 + if Hashtbl.mem plan.link_set n.hash then 3 521 + else if Hashtbl.mem plan.compile_set n.hash then 2 522 + else if Hashtbl.mem plan.doc_all_set n.hash then 2 523 + else if Hashtbl.mem plan.tool_node_set n.hash then 1 462 524 else 0 463 525 in 464 - (* Pre-populate compile_results for cached compile/doc-all layers *) 465 - List.iter (fun (build_hash, _cn) -> 466 - let cn = Hashtbl.find compile_nodes build_hash in 467 - let layer_dir = Build.dir ~os_dir cn in 468 - let layer_json = Fpath.(layer_dir / "layer.json") in 469 - if Bos.OS.File.exists layer_json |> Result.get_ok then 470 - match Day11_layer.Meta.load layer_json with 471 - | Ok meta when meta.exit_status = 0 -> 472 - Hashtbl.replace compile_results build_hash cn 473 - | _ -> () 474 - ) compile_snapshot; 475 - List.iter (fun (build_hash, _dn) -> 476 - let dn = Hashtbl.find doc_all_nodes build_hash in 477 - let layer_dir = Build.dir ~os_dir dn in 478 - let layer_json = Fpath.(layer_dir / "layer.json") in 479 - if Bos.OS.File.exists layer_json |> Result.get_ok then 480 - match Day11_layer.Meta.load layer_json with 481 - | Ok meta when meta.exit_status = 0 -> 482 - Hashtbl.replace compile_results build_hash dn 483 - | _ -> () 484 - ) doc_all_snapshot; 485 526 let open Day11_opam_build.Dag_executor in 486 527 let is_cached node = 487 528 let layer_dir = Day11_opam_layer.Build.dir ~os_dir node in ··· 498 539 else Cached_ok 499 540 end 500 541 in 542 + let dispatch = make_dispatch benv ~os_dir ~plan ~tool_source_dirs 543 + ~mounts ~build_one in 501 544 let doc_cascaded : (string, unit) Hashtbl.t = Hashtbl.create 256 in 545 + let node_kind = node_kind_of_plan plan in 502 546 Day11_opam_build.Dag_executor.execute env ~np ~priority:node_priority ~is_cached 503 547 ~on_complete:(fun ~stats node success -> 504 - let open Day11_opam_build.Dag_executor in 505 548 if Hashtbl.mem doc_cascaded node.hash then () 506 549 else begin 507 550 let status = if success then "ok" else "fail" in 508 - let kind = 509 - if Hashtbl.mem compile_set node.hash then "compile" 510 - else if Hashtbl.mem doc_all_set node.hash then "doc-all" 511 - else if Hashtbl.mem link_set node.hash then "link" 512 - else if Hashtbl.mem tool_node_set node.hash then "tool" 513 - else "build" 514 - in 551 + let kind = match node_kind node with 552 + | Compile -> "compile" | Doc_all -> "doc-all" 553 + | Link -> "link" | Tool -> "tool" | Build -> "build" in 515 554 let layer = Fpath.to_string 516 555 (Day11_opam_layer.Build.dir ~os_dir node) in 517 556 Day11_lib.Run_log.log_build_result run_log 518 557 ~pkg:(OpamPackage.to_string node.pkg) 519 558 ~hash:node.hash ~status ~failed_dep:None 520 559 ~kind ~layer_dir:layer (); 560 + if success && (Hashtbl.mem plan.doc_all_set node.hash || 561 + Hashtbl.mem plan.link_set node.hash) then 562 + Atomic.incr doc_count; 521 563 if stats.completed mod 100 = 0 || not success then 522 564 Printf.printf " [%d/%d, %d ok, %d failed, %d cascade] %s: %s\n%!" 523 565 stats.completed stats.total stats.ok stats.failed ··· 526 568 end) 527 569 ~on_cascade:(fun ~failed ~failed_dep -> 528 570 Hashtbl.replace doc_cascaded failed.hash (); 529 - let kind = 530 - if Hashtbl.mem compile_set failed.hash then "compile" 531 - else if Hashtbl.mem doc_all_set failed.hash then "doc-all" 532 - else if Hashtbl.mem link_set failed.hash then "link" 533 - else "build" 534 - in 571 + let kind = match node_kind failed with 572 + | Compile -> "compile" | Doc_all -> "doc-all" 573 + | Link -> "link" | _ -> "build" in 535 574 Day11_lib.Run_log.log_build_result run_log 536 575 ~pkg:(OpamPackage.to_string failed.pkg) 537 576 ~hash:failed.hash ~status:"cascade" 538 577 ~failed_dep:(Some (OpamPackage.to_string failed_dep.pkg)) 539 578 ~kind ()) 540 - all_doc_nodes 579 + plan.all_nodes 541 580 (fun node -> 542 - if Hashtbl.mem compile_set node.hash then begin 543 - (* Compile-only phase (for packages needing separate link) *) 544 - match Hashtbl.find_opt compile_to_build node.hash with 545 - | None -> true 546 - | Some build_hash -> 547 - let build_node = Hashtbl.find build_by_hash build_hash in 548 - current_build_hash := build_hash; 549 - (match compile_package env benv ~os_dir ~find_odoc_tool 550 - ~build_hash_blessed ~driver_tool ~compile_results 551 - ~dag_hash:node.hash build_node with 552 - | Some _bl -> true 553 - | None -> true) 554 - end else if Hashtbl.mem doc_all_set node.hash then begin 555 - (* Combined doc-all phase *) 556 - match Hashtbl.find_opt doc_all_to_build node.hash with 557 - | None -> true 558 - | Some build_hash -> 559 - let build_node = Hashtbl.find build_by_hash build_hash in 560 - current_build_hash := build_hash; 561 - (match doc_all_package env benv ~os_dir ~find_odoc_tool 562 - ~build_hash_blessed ~driver_tool ~compile_results 563 - ~build_hash ~dag_hash:node.hash 564 - build_node with 565 - | Some n -> 566 - Atomic.incr doc_count; 567 - ignore (Atomic.fetch_and_add doc_html n); 568 - true 569 - | None -> true) 570 - end else if Hashtbl.mem link_set node.hash then begin 571 - (* Link phase (for packages needing separate link) *) 572 - match Hashtbl.find_opt link_to_build node.hash with 573 - | None -> true 574 - | Some build_hash -> 575 - let build_node = Hashtbl.find build_by_hash build_hash in 576 - current_build_hash := build_hash; 577 - (match link_package env benv ~os_dir ~find_odoc_tool 578 - ~build_hash_blessed ~driver_tool ~compile_results 579 - ~doc_dep_hashes 580 - ~build_hash ~dag_hash:node.hash 581 - build_node with 582 - | Some n -> 583 - Atomic.incr doc_count; 584 - ignore (Atomic.fetch_and_add doc_html n); 585 - true 586 - | None -> true) 587 - end else begin 588 - (* Build or tool node *) 589 - let name = OpamPackage.name node.pkg in 590 - match OpamPackage.Name.Map.find_opt name tool_source_dirs with 591 - | Some dir -> 592 - (* Pinned tool package: source mount + source_dir_strategy *) 593 - let src_mount = Day11_container.Mount.bind_ro 594 - ~src:dir "/home/opam/src" in 595 - let strategy = Day11_opam_build.Tools.source_dir_strategy node.pkg in 596 - (match Day11_opam_build.Build_layer.build env benv 597 - ~mounts:(src_mount :: mounts) node ~strategy () with 598 - | Day11_opam_build.Types.Success _ -> true 599 - | _ -> false) 600 - | None -> 601 - build_one node 602 - end); 603 - (* Count all successful doc layers (including cached ones that 604 - were pre-resolved and skipped the executor callback). 605 - HTML count only from this run (cached in atomics). *) 581 + let ok = dispatch env node in 582 + if ok && (Hashtbl.mem plan.doc_all_set node.hash || 583 + Hashtbl.mem plan.link_set node.hash) then 584 + Atomic.incr doc_count; 585 + ok); 586 + (* Count results *) 606 587 let total_doc_count = ref 0 in 607 - let count_success node = 608 - let dd = Build.dir ~os_dir node in 609 - let layer_json = Fpath.(dd / "layer.json") in 610 - if Bos.OS.File.exists layer_json |> Result.get_ok then 611 - match Day11_layer.Meta.load layer_json with 612 - | Ok meta when meta.exit_status = 0 -> incr total_doc_count 613 - | _ -> () 588 + let count_success hash = 589 + if Layer.is_ok (Layer.of_hash ~os_dir hash) then incr total_doc_count 614 590 in 615 - List.iter (fun (dn : build) -> count_success dn) doc_all_list; 616 - List.iter (fun (ln : build) -> count_success ln) !link_nodes_list; 617 - (* Count HTML files from the output directory *) 591 + Hashtbl.iter (fun _ h -> count_success h) plan.build_to_doc_hash; 618 592 let html_root = Fpath.(os_dir / "html") in 619 593 let total_html = 620 594 if Bos.OS.Dir.exists html_root |> Result.get_ok then ··· 638 612 | _ -> None 639 613 ) solutions 640 614 641 - let build_tools_and_run env benv ~np ~os_dir ?driver_compiler ~packages ~repos ~opam_env:_ 642 - ~mounts ~odoc_repo ~build_one 643 - ~opam_repositories:_ 644 - ~cache ~run_log 645 - ~nodes ~solutions ~blessing_maps () = 646 - Printf.printf "\nPlanning doc tools...\n%!"; 615 + (** Resolve tools (driver + per-compiler odoc). Returns the tools 616 + and source dirs, or None if driver solving fails. *) 617 + let resolve_tools benv ~packages ~repos ~odoc_repo ~cache 618 + ?driver_compiler ~solutions () = 647 619 let all_pin_dirs, all_source_dirs = match odoc_repo with 648 620 | Some dir -> 649 - Printf.printf "Using local odoc from %s\n%!" dir; 650 621 let pins = Day11_opam_build.Tools.read_pins_from_dir dir in 651 622 let source_dirs = OpamPackage.Name.Map.fold (fun name _ acc -> 652 623 OpamPackage.Name.Map.add name dir acc 653 624 ) pins OpamPackage.Name.Map.empty in 654 625 ([ dir ], source_dirs) 655 - | None -> 656 - ([], OpamPackage.Name.Map.empty) 626 + | None -> ([], OpamPackage.Name.Map.empty) 657 627 in 658 - (* 1. Plan driver — use specified compiler or auto-detect from solutions *) 659 628 let driver_compiler = match driver_compiler with 660 629 | Some c -> c 661 630 | None -> ··· 667 636 | [] -> OpamPackage.of_string "ocaml-base-compiler.5.4.1") 668 637 in 669 638 let driver_pkg = OpamPackage.of_string "odoc-driver.3.1.0" in 670 - Printf.printf "Planning doc driver (%s)...\n%!" 671 - (OpamPackage.to_string driver_compiler); 672 - let driver_result = Day11_opam_build.Tools.plan_tool benv 639 + match Day11_opam_build.Tools.plan_tool benv 673 640 ~packages ~repos ~doc:false ~cache 674 - ~ocaml_version:driver_compiler driver_pkg in 675 - match driver_result with 676 - | Error (`Msg e) -> 677 - Printf.printf "Doc driver solve failed: %s\n%!" e 678 - | Ok (driver_tool, _driver_source_dirs) -> 679 - Printf.printf "Driver: %d nodes\n%!" (List.length driver_tool.builds); 680 - (* 2. Plan odoc per unique compiler in batch *) 641 + ~ocaml_version:driver_compiler driver_pkg with 642 + | Error _ -> None 643 + | Ok (driver_tool, _) -> 681 644 let compiler_versions = unique_compilers solutions in 682 - if compiler_versions = [] then 683 - Printf.printf "No compiler versions found in solutions, skipping docs\n%!" 684 - else begin 645 + if compiler_versions = [] then None 646 + else 685 647 let odoc_pkg = match odoc_repo with 686 648 | Some _ -> OpamPackage.of_string "odoc.dev" 687 649 | None -> OpamPackage.of_string "odoc.3.1.0" 688 650 in 689 651 let odoc_tools = List.filter_map (fun compiler_v -> 690 - Printf.printf "Planning odoc for %s...\n%!" 691 - (OpamPackage.to_string compiler_v); 692 652 match Day11_opam_build.Tools.plan_tool benv 693 653 ~packages ~repos ~pin_dirs:all_pin_dirs 694 654 ~source_dirs:all_source_dirs ~doc:false ~cache 695 655 ~ocaml_version:compiler_v odoc_pkg with 696 - | Error (`Msg e) -> 697 - Printf.printf "Odoc solve for %s failed: %s\n%!" 698 - (OpamPackage.to_string compiler_v) e; 699 - None 700 - | Ok (tool, _source_dirs) -> 701 - Printf.printf " %s: %d nodes\n%!" 702 - (OpamPackage.to_string compiler_v) (List.length tool.builds); 703 - Some (compiler_v, tool) 656 + | Error _ -> None 657 + | Ok (tool, _) -> Some (compiler_v, tool) 704 658 ) compiler_versions in 705 - (* 3. Run unified DAG: build + tools + compile + link *) 659 + Some (driver_tool, odoc_tools, all_source_dirs) 660 + 661 + let plan_doc_dag benv ~os_dir ?driver_compiler ~packages ~repos 662 + ~mounts ~odoc_repo ~build_one ~cache 663 + ~nodes ~solutions ~blessing_maps:_ () = 664 + match resolve_tools benv ~packages ~repos ~odoc_repo ~cache 665 + ?driver_compiler ~solutions () with 666 + | None -> None 667 + | Some (driver_tool, odoc_tools, all_source_dirs) -> 668 + let plan = build_internal_plan ~os_dir ~driver_tool ~odoc_tools 669 + ~nodes ~solutions in 670 + let dispatch = make_dispatch benv ~os_dir ~plan 671 + ~tool_source_dirs:all_source_dirs ~mounts ~build_one in 672 + Some { all_nodes = plan.all_nodes; 673 + node_kind = node_kind_of_plan plan; 674 + build_one = dispatch } 675 + 676 + let build_tools_and_run env benv ~np ~os_dir ?driver_compiler ~packages ~repos ~opam_env:_ 677 + ~mounts ~odoc_repo ~build_one 678 + ~opam_repositories:_ 679 + ~cache ~run_log 680 + ~nodes ~solutions ~blessing_maps:_ () = 681 + Printf.printf "\nPlanning doc tools...\n%!"; 682 + match resolve_tools benv ~packages ~repos ~odoc_repo ~cache 683 + ?driver_compiler ~solutions () with 684 + | None -> 685 + Printf.printf "Tool solving failed, skipping docs\n%!" 686 + | Some (driver_tool, odoc_tools, all_source_dirs) -> 706 687 Printf.printf "Running unified build+doc DAG...\n%!"; 707 688 let doc_count, doc_html = 708 689 run env benv ~np ~os_dir ~driver_tool ~odoc_tools 709 690 ~tool_source_dirs:all_source_dirs ~mounts 710 691 ~run_log 711 - ~build_one ~nodes ~solutions ~blessing_maps in 692 + ~build_one ~nodes ~solutions ~blessing_maps:[] in 712 693 Printf.printf "\n=== Docs: %d packages, %d HTML files ===\n%!" 713 694 doc_count doc_html 714 - end
+37
day11/doc/generate.mli
··· 46 46 blessing_maps:(OpamPackage.t * bool OpamPackage.Map.t) list -> 47 47 int * int 48 48 49 + (** {1 Planned doc DAG} 50 + 51 + [plan_doc_dag] constructs the doc DAG nodes without executing them. 52 + This is useful for external executors (like OCurrent) that want to 53 + create their own scheduling nodes from the DAG. *) 54 + 55 + type node_kind = Build | Tool | Compile | Doc_all | Link 56 + 57 + type doc_plan = { 58 + all_nodes : Day11_opam_layer.Build.t list; 59 + (** All nodes in the unified DAG (build + tool + doc). *) 60 + node_kind : Day11_opam_layer.Build.t -> node_kind; 61 + (** Classify a node by its phase. *) 62 + build_one : Eio_unix.Stdenv.base -> Day11_opam_layer.Build.t -> bool; 63 + (** Callback to build a single node. Takes the Eio env and handles 64 + dispatch to the correct phase (build, tool, compile, link, doc-all). *) 65 + } 66 + 67 + val plan_doc_dag : 68 + Day11_opam_build.Types.build_env -> 69 + os_dir:Fpath.t -> 70 + ?driver_compiler:OpamPackage.t -> 71 + packages:Day11_opam.Git_packages.t -> 72 + repos:(string * string) list -> 73 + mounts:Day11_container.Mount.t list -> 74 + odoc_repo:string option -> 75 + build_one:(Day11_opam_layer.Build.t -> bool) -> 76 + cache:Day11_opam_build.Hash_cache.t -> 77 + nodes:Day11_opam_layer.Build.t list -> 78 + solutions:(OpamPackage.t * Day11_solution.Solve_result.t) list -> 79 + blessing_maps:(OpamPackage.t * bool OpamPackage.Map.t) list -> 80 + unit -> 81 + doc_plan option 82 + (** Plan the doc DAG: solve for tools, construct compile/link/doc-all 83 + nodes with deterministic hashes, and return the unified DAG. 84 + Returns [None] if tool solving fails. *) 85 + 49 86 val build_tools_and_run : 50 87 Eio_unix.Stdenv.base -> 51 88 Day11_opam_build.Types.build_env ->
+26
day11/layer/layer.ml
··· 1 + type t = { 2 + hash : string; 3 + dir : Fpath.t; 4 + } 5 + 6 + let of_hash ~os_dir hash = 7 + let len = min 12 (String.length hash) in 8 + let name = String.sub hash 0 len in 9 + { hash; dir = Fpath.(os_dir / name) } 10 + 11 + let hash t = t.hash 12 + let dir t = t.dir 13 + let fs t = Fpath.(t.dir / "fs") 14 + let meta_path t = Fpath.(t.dir / "layer.json") 15 + let log_path t = Fpath.(t.dir / "layer.log") 16 + 17 + let pp f t = 18 + Fmt.pf f "%s" (String.sub t.hash 0 (min 12 (String.length t.hash))) 19 + 20 + let exists t = 21 + Bos.OS.File.exists (meta_path t) |> Result.get_ok 22 + 23 + let is_ok t = 24 + match Meta.load (meta_path t) with 25 + | Ok meta -> meta.exit_status = 0 26 + | Error _ -> false
+36
day11/layer/layer.mli
··· 1 + (** A layer on disk. 2 + 3 + A layer is identified by its content hash and lives at a directory 4 + derived from that hash under the os_dir. The directory contains: 5 + - [fs/] — the filesystem tree (overlayfs upper) 6 + - [layer.json] — metadata ({!Meta.t}) 7 + - [layer.log] — build stdout/stderr 8 + - optional sidecar files ([build.json], [doc.json], etc.) *) 9 + 10 + type t = { 11 + hash : string; 12 + dir : Fpath.t; 13 + } 14 + 15 + val of_hash : os_dir:Fpath.t -> string -> t 16 + (** [of_hash ~os_dir hash] constructs a layer reference from a hash. 17 + Does not check whether the layer exists on disk. *) 18 + 19 + val hash : t -> string 20 + val dir : t -> Fpath.t 21 + val fs : t -> Fpath.t 22 + (** [fs t] is [t.dir / "fs"]. *) 23 + 24 + val meta_path : t -> Fpath.t 25 + (** [meta_path t] is [t.dir / "layer.json"]. *) 26 + 27 + val log_path : t -> Fpath.t 28 + (** [log_path t] is [t.dir / "layer.log"]. *) 29 + 30 + val pp : Format.formatter -> t -> unit 31 + 32 + val exists : t -> bool 33 + (** [exists t] returns true if [layer.json] exists on disk. *) 34 + 35 + val is_ok : t -> bool 36 + (** [is_ok t] returns true if the layer exists and has exit_status = 0. *)
-1
day11/layer/scan.ml
··· 32 32 with Unix.Unix_error _ -> None) 33 33 with _ -> [] 34 34 else [] 35 -