A fork of mtelver's day10 project

Add batch mode with pre-computed per-version blessings

Replace the lock-based first-come-first-served blessing system with a
heuristic-based pre-computed blessing map. The new `batch` command:

Phase 1: Solves all target packages up front
Phase 2: Computes blessings using ocaml-docs-ci's heuristic
- Primary: maximize deps_count (prefer richer universes)
- Secondary: maximize revdeps_count (stability tiebreaker)
Phase 3: Forks workers with pre-computed blessing maps

Key changes:
- blessing.ml: Rewritten from flock-based locking to compute_blessings
algorithm that picks the best universe per (package, version) pair
- config.ml: Added blessed_map field for passing blessing decisions
- linux.ml: generate_docs reads blessing from config map instead of
filesystem locks
- s.ml: doc_layer_hash now includes blessed status so blessed/non-blessed
builds produce different doc layers with correct HTML paths
- main.ml: New run_batch function, batch CLI command, --blessed-map flag
on health-check for external worker support

Every version of every package now gets its own blessed documentation,
not just the first version to finish building.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+237 -101
+114 -81
bin/blessing.ml
··· 1 - (** First-come-first-blessed locking mechanism for package documentation. 1 + (** Pre-computed blessing system for package documentation. 2 2 3 - When multiple builds produce documentation for the same package (possibly with 4 - different dependency universes), only the first one to complete gets "blessed" 5 - as the canonical version. This uses flock-based locking to ensure atomicity. *) 3 + Given solutions for multiple target packages, determines which universe 4 + (dependency set) is "blessed" for each (package, version) pair. 6 5 7 - type blessing_result = 8 - | Blessed (** This build won the blessing *) 9 - | Already_blessed of string (** Another universe already blessed; returns existing universe hash *) 6 + Heuristic (from ocaml-docs-ci): 7 + 1. Maximize deps_count: prefer universes with more dependencies 8 + (favors optional deps being resolved → richer documentation) 9 + 2. Maximize revdeps_count: prefer universes where this package has 10 + more reverse dependencies (stability: changing blessings cascades) 10 11 11 - (** Get the lock file path for a package *) 12 - let lock_path ~blessed_dir ~pkg = 13 - let pkg_name = OpamPackage.name_to_string pkg in 14 - Path.(blessed_dir / pkg_name / "lock") 12 + The result is a per-target blessing map: for each package in a target's 13 + solution, whether that package is blessed in that solution's universe. *) 15 14 16 - (** Get the universe file path for a package *) 17 - let universe_path ~blessed_dir ~pkg = 18 - let pkg_name = OpamPackage.name_to_string pkg in 19 - Path.(blessed_dir / pkg_name / "universe") 15 + (** Hash a set of transitive dependencies to produce a universe identifier. 16 + Uses sorted package names to ensure determinism. *) 17 + let universe_hash_of_deps deps = 18 + deps 19 + |> OpamPackage.Set.elements 20 + |> List.map OpamPackage.to_string 21 + |> String.concat "\n" 22 + |> Digest.string 23 + |> Digest.to_hex 20 24 21 - (** Try to claim blessing for a package. 25 + (** Compute blessing maps for a set of solved targets. 22 26 23 - Uses non-blocking flock to attempt to acquire the lock. 24 - If successful, writes the universe hash and returns [Blessed]. 25 - If another process already blessed this package, returns [Already_blessed universe]. *) 26 - let try_bless ~blessed_dir ~pkg ~universe : blessing_result = 27 - let pkg_name = OpamPackage.name_to_string pkg in 28 - let pkg_dir = Path.(blessed_dir / pkg_name) in 29 - let lock_file = lock_path ~blessed_dir ~pkg in 30 - let universe_file = universe_path ~blessed_dir ~pkg in 31 - (* Ensure the package directory exists *) 32 - Os.mkdir ~parents:true pkg_dir; 33 - (* Open lock file *) 34 - let lock_fd = Unix.openfile lock_file [ O_CREAT; O_RDWR ] 0o644 in 35 - try 36 - (* Try non-blocking lock *) 37 - Unix.lockf lock_fd F_TLOCK 0; 38 - (* We got the lock - check if already blessed *) 39 - if Sys.file_exists universe_file then begin 40 - (* Already blessed by someone else before we got the lock *) 41 - let existing_universe = Os.read_from_file universe_file |> String.trim in 42 - Unix.lockf lock_fd F_ULOCK 0; 43 - Unix.close lock_fd; 44 - Already_blessed existing_universe 45 - end 46 - else begin 47 - (* We're first - write our universe and keep blessed status *) 48 - Os.write_to_file universe_file universe; 49 - Unix.lockf lock_fd F_ULOCK 0; 50 - Unix.close lock_fd; 51 - Blessed 52 - end 53 - with 54 - | Unix.Unix_error (Unix.EAGAIN, _, _) 55 - | Unix.Unix_error (Unix.EACCES, _, _) -> 56 - (* Lock is held by another process - wait briefly then check *) 57 - Unix.close lock_fd; 58 - (* Re-open and do a blocking lock to wait for the other process *) 59 - let lock_fd = Unix.openfile lock_file [ O_CREAT; O_RDWR ] 0o644 in 60 - Unix.lockf lock_fd F_LOCK 0; 61 - (* Now check what universe was blessed *) 62 - let result = 63 - if Sys.file_exists universe_file then 64 - Already_blessed (Os.read_from_file universe_file |> String.trim) 65 - else begin 66 - (* Rare case: other process released lock without blessing *) 67 - Os.write_to_file universe_file universe; 68 - Blessed 69 - end 27 + Input: list of (target_package, transitive_deps_map) where 28 + transitive_deps_map maps each package in the solution to its full 29 + transitive dependency set. 30 + 31 + Output: list of (target_package, blessing_map) where blessing_map 32 + maps each package to [true] if blessed in this solution, [false] otherwise. *) 33 + let compute_blessings 34 + (solutions : (OpamPackage.t * OpamPackage.Set.t OpamPackage.Map.t) list) = 35 + (* Step 1: Compute revdeps counts across all solutions. 36 + For each package P, count how many packages across all solutions 37 + have P as a transitive dependency. *) 38 + let revdeps_counts : (OpamPackage.t, int) Hashtbl.t = Hashtbl.create 1000 in 39 + List.iter (fun (_target, trans_deps) -> 40 + OpamPackage.Map.iter (fun _pkg deps -> 41 + OpamPackage.Set.iter (fun dep -> 42 + let c = try Hashtbl.find revdeps_counts dep with Not_found -> 0 in 43 + Hashtbl.replace revdeps_counts dep (c + 1) 44 + ) deps 45 + ) trans_deps 46 + ) solutions; 47 + 48 + (* Step 2: For each unique OpamPackage.t, collect all distinct universes 49 + it appears in, along with their quality metrics. *) 50 + let pkg_universes : (OpamPackage.t, (string * int * int) list) Hashtbl.t = 51 + Hashtbl.create 1000 52 + in 53 + List.iter (fun (_target, trans_deps) -> 54 + OpamPackage.Map.iter (fun pkg deps -> 55 + let uhash = universe_hash_of_deps deps in 56 + let deps_count = OpamPackage.Set.cardinal deps in 57 + let revdeps_count = 58 + try Hashtbl.find revdeps_counts pkg with Not_found -> 0 59 + in 60 + let existing = 61 + try Hashtbl.find pkg_universes pkg with Not_found -> [] 70 62 in 71 - Unix.lockf lock_fd F_ULOCK 0; 72 - Unix.close lock_fd; 73 - result 63 + (* Only add if this universe hash is new for this package *) 64 + if not (List.exists (fun (h, _, _) -> String.equal h uhash) existing) then 65 + Hashtbl.replace pkg_universes pkg 66 + ((uhash, deps_count, revdeps_count) :: existing) 67 + ) trans_deps 68 + ) solutions; 74 69 75 - (** Check current blessing status for a package. 70 + (* Step 3: For each package, pick the best universe. 71 + Primary: maximize deps_count. Secondary: maximize revdeps_count. *) 72 + let blessed_universe : (OpamPackage.t, string) Hashtbl.t = 73 + Hashtbl.create 1000 74 + in 75 + Hashtbl.iter (fun pkg entries -> 76 + let best_hash, _, _ = 77 + List.fold_left 78 + (fun ((_, bdc, brc) as best) ((_, dc, rc) as entry) -> 79 + if dc > bdc || (dc = bdc && rc > brc) then entry else best) 80 + (List.hd entries) (List.tl entries) 81 + in 82 + Hashtbl.replace blessed_universe pkg best_hash 83 + ) pkg_universes; 76 84 77 - Returns [Some universe_hash] if blessed, [None] otherwise. *) 78 - let get_blessed_universe ~blessed_dir ~pkg : string option = 79 - let universe_file = universe_path ~blessed_dir ~pkg in 80 - if Sys.file_exists universe_file then 81 - Some (Os.read_from_file universe_file |> String.trim) 82 - else 83 - None 85 + (* Step 4: For each target, generate a blessing map. 86 + A package is blessed if its universe in this solution matches 87 + the globally-chosen best universe. *) 88 + List.map (fun (target, trans_deps) -> 89 + let map = 90 + OpamPackage.Map.mapi (fun pkg deps -> 91 + let uhash = universe_hash_of_deps deps in 92 + let blessed_uhash = Hashtbl.find blessed_universe pkg in 93 + String.equal uhash blessed_uhash 94 + ) trans_deps 95 + in 96 + (target, map) 97 + ) solutions 84 98 85 - (** Check if a package is blessed with a specific universe *) 86 - let is_blessed_with ~blessed_dir ~pkg ~universe : bool = 87 - match get_blessed_universe ~blessed_dir ~pkg with 88 - | Some existing -> String.equal existing universe 99 + (** Look up whether a package is blessed in the given map. *) 100 + let is_blessed map pkg = 101 + match OpamPackage.Map.find_opt pkg map with 102 + | Some b -> b 89 103 | None -> false 90 104 91 - (** Get the blessed directory path for a config *) 92 - let blessed_dir ~(config : Config.t) = 93 - Path.(config.dir / "blessed") 105 + (** Save a blessing map to a JSON file. 106 + Format: {"package.version": true/false, ...} *) 107 + let save_blessed_map filename map = 108 + let entries = 109 + OpamPackage.Map.fold (fun pkg blessed acc -> 110 + (OpamPackage.to_string pkg, `Bool blessed) :: acc 111 + ) map [] 112 + in 113 + Yojson.Safe.to_file filename (`Assoc entries) 114 + 115 + (** Load a blessing map from a JSON file. *) 116 + let load_blessed_map filename = 117 + let json = Yojson.Safe.from_file filename in 118 + let open Yojson.Safe.Util in 119 + match json with 120 + | `Assoc entries -> 121 + List.fold_left (fun map (pkg_str, v) -> 122 + let pkg = OpamPackage.of_string pkg_str in 123 + let blessed = to_bool v in 124 + OpamPackage.Map.add pkg blessed map 125 + ) OpamPackage.Map.empty entries 126 + | _ -> failwith "Invalid blessed map JSON: expected object"
+1
bin/config.ml
··· 22 22 dry_run : bool; 23 23 fork : int option; 24 24 prune_layers : bool; (* Delete target layer after docs extracted to html_output *) 25 + blessed_map : bool OpamPackage.Map.t option; (* Pre-computed blessing map from batch mode *) 25 26 } 26 27 27 28 let std_env ~(config : t) =
+1 -1
bin/dummy.ml
··· 29 29 let _rootfs = Path.(temp_dir / "fs") in 30 30 0 31 31 32 - let doc_layer_hash ~t:_ ~build_hash:_ ~dep_doc_hashes:_ ~ocaml_version:_ = "" 32 + let doc_layer_hash ~t:_ ~build_hash:_ ~dep_doc_hashes:_ ~ocaml_version:_ ~blessed:_ = "" 33 33 34 34 (* Documentation generation not supported in dummy container *) 35 35 let generate_docs ~t:_ ~build_layer_dir:_ ~doc_layer_dir:_ ~dep_doc_hashes:_ ~pkg:_ ~installed_libs:_ ~installed_docs:_ ~phase:_ ~ocaml_version:_ = None
+1 -1
bin/freebsd.ml
··· 248 248 let _ = Os.sudo [ "sh"; "-c"; ("rm -f " ^ Path.(upperdir / "home" / "opam" / ".opam" / "repo" / "state-*.cache")) ] in 249 249 result 250 250 251 - let doc_layer_hash ~t:_ ~build_hash:_ ~dep_doc_hashes:_ ~ocaml_version:_ = "" 251 + let doc_layer_hash ~t:_ ~build_hash:_ ~dep_doc_hashes:_ ~ocaml_version:_ ~blessed:_ = "" 252 252 253 253 (* Documentation generation not supported on FreeBSD *) 254 254 let generate_docs ~t:_ ~build_layer_dir:_ ~doc_layer_dir:_ ~dep_doc_hashes:_ ~pkg:_ ~installed_libs:_ ~installed_docs:_ ~phase:_ ~ocaml_version:_ = None
+7 -10
bin/linux.ml
··· 161 161 in 162 162 String.concat " " hashes |> Digest.string |> Digest.to_hex 163 163 164 - let doc_layer_hash ~t ~build_hash ~dep_doc_hashes ~ocaml_version = 164 + let doc_layer_hash ~t ~build_hash ~dep_doc_hashes ~ocaml_version ~blessed = 165 165 let config = t.config in 166 166 let driver_hash = Doc_tools.get_driver_hash ~config in 167 167 let odoc_hash = Doc_tools.get_odoc_hash ~config ~ocaml_version in 168 - let components = build_hash :: dep_doc_hashes @ [ driver_hash; odoc_hash ] in 168 + let blessed_str = if blessed then "blessed" else "universe" in 169 + let components = build_hash :: dep_doc_hashes @ [ driver_hash; odoc_hash; blessed_str ] in 169 170 String.concat " " components |> Digest.string |> Digest.to_hex 170 171 171 172 let run ~t ~temp_dir opam_repository build_log = ··· 574 575 let prep_dir = Path.(doc_layer_dir / "prep") in 575 576 if Sys.file_exists prep_dir then 576 577 ignore (Os.sudo [ "chown"; "-R"; uid_gid; prep_dir ]); 577 - (* Determine blessing status *) 578 - let blessed_dir = Blessing.blessed_dir ~config in 579 - let blessing_result = Blessing.try_bless ~blessed_dir ~pkg ~universe in 578 + (* Determine blessing status from pre-computed map *) 580 579 let blessed = 581 - match blessing_result with 582 - | Blessing.Blessed -> true 583 - | Blessing.Already_blessed existing_universe -> 584 - (* If already blessed with the SAME universe, we're still blessed *) 585 - String.equal existing_universe universe 580 + match config.blessed_map with 581 + | Some map -> Blessing.is_blessed map pkg 582 + | None -> false 586 583 in 587 584 (* Determine HTML output directory - use shared if specified, else per-layer *) 588 585 let html_output_dir = match config.html_output with
+110 -6
bin/main.ml
··· 291 291 let doc_layer t pkg build_layer_name dep_doc_hashes ~ocaml_version = 292 292 let config = Container.config ~t in 293 293 let os_key = Config.os_key ~config in 294 - let doc_hash = Container.doc_layer_hash ~t ~build_hash:build_layer_name ~dep_doc_hashes ~ocaml_version in 294 + let blessed = match config.blessed_map with 295 + | Some map -> Blessing.is_blessed map pkg 296 + | None -> false 297 + in 298 + let doc_hash = Container.doc_layer_hash ~t ~build_hash:build_layer_name ~dep_doc_hashes ~ocaml_version ~blessed in 295 299 let doc_layer_name = "doc-" ^ doc_hash in 296 300 let doc_layer_dir = Path.(config.dir / os_key / doc_layer_name) in 297 301 let build_layer_dir = Path.(config.dir / os_key / build_layer_name) in ··· 675 679 | None -> List.iter run_with_package packages 676 680 | Some n -> Os.fork ~np:n run_with_package packages 677 681 682 + let run_batch (config : Config.t) package_arg = 683 + let packages = 684 + if String.length package_arg > 0 && package_arg.[0] = '@' then 685 + let filename = String.sub package_arg 1 (String.length package_arg - 1) in 686 + Json_packages.read_packages filename 687 + else 688 + [ package_arg ] 689 + in 690 + if packages = [] then begin 691 + Printf.eprintf "No packages to process\n%!"; 692 + exit 1 693 + end; 694 + 695 + (* Phase 1: Solve all targets *) 696 + Printf.printf "Phase 1: Solving %d targets...\n%!" (List.length packages); 697 + let solutions = List.filter_map (fun pkg_name -> 698 + let package = OpamPackage.of_string pkg_name in 699 + let pkg_config = { config with package = pkg_name } in 700 + match solve pkg_config package with 701 + | Ok solution -> 702 + Printf.printf " Solved %s (%d packages)\n%!" pkg_name (OpamPackage.Map.cardinal solution); 703 + Some (package, solution) 704 + | Error msg -> 705 + Printf.eprintf " Failed to solve %s: %s\n%!" pkg_name msg; 706 + None 707 + ) packages in 708 + 709 + if solutions = [] then begin 710 + Printf.eprintf "No solutions found, nothing to build\n%!"; 711 + exit 1 712 + end; 713 + 714 + (* Phase 2: Compute blessings *) 715 + Printf.printf "Phase 2: Computing blessings for %d targets...\n%!" (List.length solutions); 716 + let trans_deps_per_target = List.map (fun (target, solution) -> 717 + let ordered = topological_sort solution in 718 + let trans = pkg_deps solution ordered in 719 + (target, trans) 720 + ) solutions in 721 + let blessing_maps = Blessing.compute_blessings trans_deps_per_target in 722 + 723 + (* Report blessing stats *) 724 + let total_blessed = List.fold_left (fun acc (_, map) -> 725 + acc + OpamPackage.Map.fold (fun _ b c -> if b then c + 1 else c) map 0 726 + ) 0 blessing_maps in 727 + let total_packages = List.fold_left (fun acc (_, map) -> 728 + acc + OpamPackage.Map.cardinal map 729 + ) 0 blessing_maps in 730 + Printf.printf " %d/%d package instances blessed across %d targets\n%!" 731 + total_blessed total_packages (List.length solutions); 732 + 733 + (* Phase 3: Build with blessings *) 734 + Printf.printf "Phase 3: Building %d targets...\n%!" (List.length solutions); 735 + (* Create output directories if they're treated as directories (batch mode) *) 736 + let () = Option.iter (fun path -> Os.mkdir ~parents:true path) config.json in 737 + let () = Option.iter (fun path -> Os.mkdir ~parents:true path) config.md in 738 + let () = Option.iter (fun path -> Os.mkdir ~parents:true path) config.dot in 739 + 740 + let run_with_target (pkg, blessed_map) = 741 + let pkg_name = OpamPackage.to_string pkg in 742 + let json = Option.map (fun path -> Path.(path / pkg_name ^ ".json")) config.json in 743 + let md = Option.map (fun path -> Path.(path / pkg_name ^ ".md")) config.md in 744 + let dot = Option.map (fun path -> Path.(path / pkg_name ^ ".dot")) config.dot in 745 + let config = { config with 746 + package = pkg_name; 747 + blessed_map = Some blessed_map; 748 + json; md; dot; 749 + } in 750 + run_health_check config 751 + in 752 + let items = List.filter_map (fun (target, _solution) -> 753 + List.find_opt (fun (t, _) -> OpamPackage.equal t target) blessing_maps 754 + ) solutions in 755 + match config.fork with 756 + | Some 1 | None -> List.iter run_with_target items 757 + | Some n -> Os.fork ~np:n run_with_target items 758 + 678 759 let cache_dir_term = 679 760 let doc = "Directory to use for caching (required)" in 680 761 Arg.(required & opt (some string) None & info [ "cache-dir" ] ~docv:"DIR" ~doc) ··· 768 849 let doc = "Delete package layers after docs are extracted to html-output (saves disk space)" in 769 850 Arg.(value & flag & info [ "prune-layers" ] ~doc) 770 851 852 + let blessed_map_term = 853 + let doc = "Path to a pre-computed blessing map JSON file (from batch mode)" in 854 + Arg.(value & opt (some string) None & info [ "blessed-map" ] ~docv:"FILE" ~doc) 855 + 771 856 let find_opam_files dir = 772 857 try 773 858 Sys.readdir dir |> Array.to_list |> List.filter_map (fun name -> if Filename.check_suffix name ".opam" then Some (Filename.remove_extension name) else None) ··· 808 893 dry_run; 809 894 fork; 810 895 prune_layers; 896 + blessed_map = None; 811 897 }) 812 898 $ cache_dir_term $ ocaml_version_term $ opam_repository_term $ directory_arg $ md_term $ json_term $ dot_term $ with_test_term $ with_doc_term $ doc_tools_repo_term $ doc_tools_branch_term $ html_output_term $ log_term $ dry_run_term $ arch_term $ os_term $ os_distribution_term $ os_family_term $ os_version_term $ fork_term $ prune_layers_term) 813 899 in ··· 821 907 in 822 908 let health_check_term = 823 909 Term.( 824 - const (fun dir ocaml_version opam_repositories package_arg md json dot with_test with_doc doc_tools_repo doc_tools_branch html_output log dry_run tag arch os os_distribution os_family os_version fork prune_layers -> 910 + const (fun dir ocaml_version opam_repositories package_arg md json dot with_test with_doc doc_tools_repo doc_tools_branch html_output log dry_run tag arch os os_distribution os_family os_version fork prune_layers blessed_map_file -> 825 911 let ocaml_version = Option.map OpamPackage.of_string ocaml_version in 826 - run_health_check_multi { dir; ocaml_version; opam_repositories; package = ""; arch; os; os_distribution; os_family; os_version; directory = None; md; json; dot; with_test; with_doc; doc_tools_repo; doc_tools_branch; html_output; tag; log; dry_run; fork; prune_layers } package_arg) 827 - $ cache_dir_term $ ocaml_version_term $ opam_repository_term $ package_arg $ md_term $ json_term $ dot_term $ with_test_term $ with_doc_term $ doc_tools_repo_term $ doc_tools_branch_term $ html_output_term $ log_term $ dry_run_term $ tag_term $ arch_term $ os_term $ os_distribution_term $ os_family_term $ os_version_term $ fork_term $ prune_layers_term) 912 + let blessed_map = Option.map Blessing.load_blessed_map blessed_map_file in 913 + run_health_check_multi { dir; ocaml_version; opam_repositories; package = ""; arch; os; os_distribution; os_family; os_version; directory = None; md; json; dot; with_test; with_doc; doc_tools_repo; doc_tools_branch; html_output; tag; log; dry_run; fork; prune_layers; blessed_map } package_arg) 914 + $ cache_dir_term $ ocaml_version_term $ opam_repository_term $ package_arg $ md_term $ json_term $ dot_term $ with_test_term $ with_doc_term $ doc_tools_repo_term $ doc_tools_branch_term $ html_output_term $ log_term $ dry_run_term $ tag_term $ arch_term $ os_term $ os_distribution_term $ os_family_term $ os_version_term $ fork_term $ prune_layers_term $ blessed_map_term) 828 915 in 829 916 let health_check_info = Cmd.info "health-check" ~doc:"Run health check on a package or list of packages" in 830 917 Cmd.v health_check_info health_check_term ··· 835 922 const (fun ocaml_version opam_repositories all_versions json arch os os_distribution os_family os_version -> 836 923 let ocaml_version = Option.map OpamPackage.of_string ocaml_version in 837 924 run_list 838 - { dir = ""; ocaml_version; opam_repositories; package = ""; arch; os; os_distribution; os_family; os_version; directory = None; md = None; json; dot = None; with_test = false; with_doc = false; doc_tools_repo = ""; doc_tools_branch = ""; html_output = None; tag = None; log = false; dry_run = false; fork = None; prune_layers = false } 925 + { dir = ""; ocaml_version; opam_repositories; package = ""; arch; os; os_distribution; os_family; os_version; directory = None; md = None; json; dot = None; with_test = false; with_doc = false; doc_tools_repo = ""; doc_tools_branch = ""; html_output = None; tag = None; log = false; dry_run = false; fork = None; prune_layers = false; blessed_map = None } 839 926 all_versions) 840 927 $ ocaml_version_term $ opam_repository_term $ all_versions_term $ json_term $ arch_term $ os_term $ os_distribution_term $ os_family_term $ os_version_term) 841 928 in ··· 922 1009 let combine_docs_info = Cmd.info "combine-docs" ~doc:"Combine documentation layers using overlayfs" in 923 1010 Cmd.v combine_docs_info combine_docs_term 924 1011 1012 + let batch_cmd = 1013 + let package_arg = 1014 + let doc = "Package name or @filename to read package list from file (JSON format: {\"packages\":[...]})" in 1015 + Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc) 1016 + in 1017 + let batch_term = 1018 + Term.( 1019 + const (fun dir ocaml_version opam_repositories package_arg md json dot with_test with_doc doc_tools_repo doc_tools_branch html_output log dry_run tag arch os os_distribution os_family os_version fork prune_layers -> 1020 + let ocaml_version = Option.map OpamPackage.of_string ocaml_version in 1021 + run_batch { dir; ocaml_version; opam_repositories; package = ""; arch; os; os_distribution; os_family; os_version; directory = None; md; json; dot; with_test; with_doc; doc_tools_repo; doc_tools_branch; html_output; tag; log; dry_run; fork; prune_layers; blessed_map = None } package_arg) 1022 + $ cache_dir_term $ ocaml_version_term $ opam_repository_term $ package_arg $ md_term $ json_term $ dot_term $ with_test_term $ with_doc_term $ doc_tools_repo_term $ doc_tools_branch_term $ html_output_term $ log_term $ dry_run_term $ tag_term $ arch_term $ os_term $ os_distribution_term $ os_family_term $ os_version_term $ fork_term $ prune_layers_term) 1023 + in 1024 + let batch_info = Cmd.info "batch" ~doc:"Solve all targets, compute blessings, then build with pre-computed blessing maps" in 1025 + Cmd.v batch_info batch_term 1026 + 925 1027 let main_info = 926 1028 let doc = "A tool for running CI and health checks" in 927 1029 let man = ··· 931 1033 `P "Use '$(mname) ci DIRECTORY' to run CI tests on a directory."; 932 1034 `P "Use '$(mname) health-check PACKAGE' to run health checks on a package."; 933 1035 `P "Use '$(mname) health-check @FILENAME' to run health checks on multiple packages listed in FILENAME (JSON format: {\"packages\":[...]})"; 1036 + `P "Use '$(mname) batch PACKAGE' to solve, compute blessings, and build in batch mode."; 934 1037 `P "Use '$(mname) list' list packages in opam repository."; 935 1038 `P "Use '$(mname) sync-docs DESTINATION' to sync documentation to a destination."; 936 1039 `P "Use '$(mname) combine-docs MOUNT_POINT' to combine all doc layers into an overlay mount."; ··· 939 1042 `P "$(mname) ci --cache-dir /tmp/cache --opam-repository /tmp/opam-repository /path/to/project"; 940 1043 `P "$(mname) health-check --cache-dir /tmp/cache --opam-repositories /tmp/opam-repository package --md"; 941 1044 `P "$(mname) health-check --cache-dir /tmp/cache --opam-repositories /tmp/opam-repository @packages.json"; 1045 + `P "$(mname) batch --cache-dir /tmp/cache --opam-repository /tmp/opam-repository --with-doc --html-output /tmp/docs @packages.json"; 942 1046 `P "$(mname) list --opam-repositories /tmp/opam-repository"; 943 1047 `P "$(mname) sync-docs --cache-dir /tmp/cache /var/www/docs --index"; 944 1048 `P "$(mname) sync-docs --cache-dir /tmp/cache user@host:/var/www/docs"; ··· 948 1052 949 1053 let () = 950 1054 let default_term = Term.(ret (const (`Help (`Pager, None)))) in 951 - let cmd = Cmd.group ~default:default_term main_info [ ci_cmd; health_check_cmd; list_cmd; sync_docs_cmd; combine_docs_cmd ] in 1055 + let cmd = Cmd.group ~default:default_term main_info [ ci_cmd; health_check_cmd; batch_cmd; list_cmd; sync_docs_cmd; combine_docs_cmd ] in 952 1056 exit (Cmd.eval cmd)
+2 -1
bin/s.ml
··· 16 16 17 17 (** Compute hash for a doc layer. 18 18 The doc hash depends on the build hash, dependency doc hashes, 19 - driver layer hash, and odoc layer hash. *) 19 + driver layer hash, odoc layer hash, and blessing status. *) 20 20 val doc_layer_hash : 21 21 t:t -> 22 22 build_hash:string -> 23 23 dep_doc_hashes:string list -> 24 24 ocaml_version:OpamPackage.t -> 25 + blessed:bool -> 25 26 string 26 27 27 28 (** Documentation generation support.
+1 -1
bin/windows.ml
··· 172 172 let () = List.iter (fun hash -> Os.clense_tree ~source:Path.(config.dir / os_key / hash / "fs") ~target) sources in 173 173 result 174 174 175 - let doc_layer_hash ~t:_ ~build_hash:_ ~dep_doc_hashes:_ ~ocaml_version:_ = "" 175 + let doc_layer_hash ~t:_ ~build_hash:_ ~dep_doc_hashes:_ ~ocaml_version:_ ~blessed:_ = "" 176 176 177 177 (* Documentation generation not supported on Windows *) 178 178 let generate_docs ~t:_ ~build_layer_dir:_ ~doc_layer_dir:_ ~dep_doc_hashes:_ ~pkg:_ ~installed_libs:_ ~installed_docs:_ ~phase:_ ~ocaml_version:_ = None