(** Read layer info for packages from day10's cache directory. Uses the [packages//] directory structure with symlinks: {[ build- -> ../../build- (all builds) doc- -> ../../doc- (all docs) blessed-build -> ../../build- (canonical build if blessed) blessed-docs -> ../../doc- (canonical docs if blessed) ]} Falls back to scanning [build-*] directories if no symlinks exist. *) type layer_info = { package: string; deps: string list; created: float; exit_status: int; } (** Read layer.json from a directory and parse it *) let read_layer_json path = if Sys.file_exists path then try let content = In_channel.with_open_text path In_channel.input_all in let json = Yojson.Safe.from_string content in let open Yojson.Safe.Util in (* Handle deps which may have OpamPackage objects or strings *) let deps_list = json |> member "deps" |> to_list in let deps = deps_list |> List.filter_map (fun d -> match d with | `String s -> Some s | _ -> None (* Skip non-string deps *) ) in Some { package = json |> member "package" |> to_string; deps; created = json |> member "created" |> to_float; exit_status = json |> member "exit_status" |> to_int; } with _ -> None else None (** Follow a symlink and read layer.json from the target directory *) let read_layer_via_symlink symlink_path = if Sys.file_exists symlink_path then try let target = Unix.readlink symlink_path in (* Target is relative like "../../build-abc123" *) let layer_dir = Filename.concat (Filename.dirname symlink_path) target in let layer_json = Filename.concat layer_dir "layer.json" in read_layer_json layer_json with Unix.Unix_error _ -> None else None (** Get layer info for a package. Checks blessed-build first, then falls back to most recent build symlink, then falls back to scanning build-* directories. *) let get_package_layer ~cache_dir ~platform ~package = let pkg_dir = Filename.concat cache_dir (Filename.concat platform (Filename.concat "packages" package)) in (* Try blessed-build first *) let blessed_build = Filename.concat pkg_dir "blessed-build" in match read_layer_via_symlink blessed_build with | Some info -> Some info | None -> (* Try to find any build-* symlink in the package directory *) if Sys.file_exists pkg_dir && Sys.is_directory pkg_dir then let build_symlinks = Sys.readdir pkg_dir |> Array.to_list |> List.filter (fun name -> String.length name > 6 && String.sub name 0 6 = "build-") |> List.sort (fun a b -> String.compare b a) (* Most recent first by hash *) in match build_symlinks with | first :: _ -> read_layer_via_symlink (Filename.concat pkg_dir first) | [] -> None else (* No package directory - fall back to scanning build-* directories *) let platform_dir = Filename.concat cache_dir platform in if Sys.file_exists platform_dir && Sys.is_directory platform_dir then Sys.readdir platform_dir |> Array.to_list |> List.filter (fun name -> String.length name > 6 && String.sub name 0 6 = "build-") |> List.find_map (fun build_dir -> let layer_json = Filename.concat platform_dir (Filename.concat build_dir "layer.json") in match read_layer_json layer_json with | Some info when info.package = package -> Some info | _ -> None) else None (** List all packages with layer info (for computing reverse deps). Returns list of (package_name, layer_info) pairs. Uses packages/ directory structure - each subdirectory is a package. *) let list_all_packages ~cache_dir ~platform = let packages_dir = Filename.concat cache_dir (Filename.concat platform "packages") in if Sys.file_exists packages_dir && Sys.is_directory packages_dir then Sys.readdir packages_dir |> Array.to_list |> List.filter (fun name -> (* Each entry should be a directory (package.version) *) let path = Filename.concat packages_dir name in Sys.is_directory path) |> List.filter_map (fun package -> match get_package_layer ~cache_dir ~platform ~package with | Some info -> Some (package, info) | None -> None) else (* Fall back to scanning build-* directories *) let platform_dir = Filename.concat cache_dir platform in if Sys.file_exists platform_dir && Sys.is_directory platform_dir then Sys.readdir platform_dir |> Array.to_list |> List.filter (fun name -> String.length name > 6 && String.sub name 0 6 = "build-") |> List.filter_map (fun build_dir -> let layer_json = Filename.concat platform_dir (Filename.concat build_dir "layer.json") in match read_layer_json layer_json with | Some info -> Some (info.package, info) | None -> None) else [] (** Compute reverse dependencies: which packages depend on the given package. Returns a list of package names that have this package in their deps. *) let get_reverse_deps ~cache_dir ~platform ~package = list_all_packages ~cache_dir ~platform |> List.filter_map (fun (pkg_name, info) -> if List.mem package info.deps then Some pkg_name else None) |> List.sort String.compare