Monorepo management for opam overlays
1(** Package discovery from monorepo subtrees. *)
2
3let src = Logs.Src.create "monopam.monorepo_pkg"
4
5module Log = (val Logs.src_log src)
6
7type t = {
8 pkg_name : string;
9 subtree : string;
10 dev_repo : string;
11 url_src : string;
12 opam_content : string;
13}
14
15let v ~pkg_name ~subtree ~dev_repo ~url_src ~opam_content =
16 { pkg_name; subtree; dev_repo; url_src; opam_content }
17
18let pp ppf t = Fmt.pf ppf "@[<v>%s (subtree: %s)@]" t.pkg_name t.subtree
19let name t = t.pkg_name
20let subtree t = t.subtree
21let matches_name name t = t.pkg_name = name || t.subtree = name
22let dev_repo t = t.dev_repo
23let url_src t = t.url_src
24let opam_content t = t.opam_content
25
26(** Derive dev_repo and url_src for a subtree from various sources. Priority:
27 sources.toml override > dune-project source > default_url_base *)
28let derive_dev_repo ~sources ~subtree dune_proj =
29 let sources_override = Sources_registry.find sources ~subtree in
30 let derive_from_dune () =
31 match
32 ( Dune_project.dev_repo_url dune_proj,
33 Dune_project.url_with_branch dune_proj )
34 with
35 | Ok dev_repo, Ok url_src -> Some (dev_repo, url_src)
36 | _ -> None
37 in
38 let derive_from_origin () =
39 match Sources_registry.origin sources with
40 | Some base ->
41 let base =
42 if String.ends_with ~suffix:"/" base then
43 String.sub base 0 (String.length base - 1)
44 else base
45 in
46 Some (base ^ "/" ^ subtree)
47 | None -> None
48 in
49 match sources_override with
50 | Some entry ->
51 (* Use the public HTTPS URL (from origin) for opam,
52 falling back to the entry source if no origin is configured. *)
53 let dev_repo, url_src =
54 match derive_from_origin () with
55 | Some url ->
56 (* Origin URL points to where monopam pushes — don't specify
57 branch, let git pick the default *)
58 (url, url)
59 | None ->
60 (* Fallback to entry source — use its branch *)
61 let branch =
62 match entry.Sources_registry.branch with
63 | Some b -> b
64 | None -> (
65 match dune_proj.Dune_project.source with
66 | Some (Dune_project.Uri { branch = Some b; _ }) -> b
67 | _ -> "main")
68 in
69 ( entry.Sources_registry.source,
70 entry.Sources_registry.source ^ "#" ^ branch )
71 in
72 Log.debug (fun m ->
73 m "Using sources.toml entry for %s: %s" subtree dev_repo);
74 Some (dev_repo, url_src)
75 | None -> (
76 match derive_from_dune () with
77 | Some result -> Some result
78 | None -> (
79 match derive_from_origin () with
80 | Some (dev_repo as url) ->
81 Log.debug (fun m -> m "Using origin for %s: %s" subtree dev_repo);
82 Some (url, url ^ "#main")
83 | None ->
84 Log.warn (fun m ->
85 m
86 "Cannot derive dev-repo for %s (no source in dune-project \
87 or sources.toml)"
88 subtree);
89 None))
90
91(** Load opam packages from a subtree directory. *)
92let load_opam_packages ~subtree_path ~subtree ~dev_repo ~url_src =
93 let opam_files =
94 try
95 Eio.Path.read_dir subtree_path
96 |> List.filter (fun name -> Filename.check_suffix name ".opam")
97 with Eio.Io _ -> []
98 in
99 List.filter_map
100 (fun opam_file ->
101 let pkg_name = Filename.chop_suffix opam_file ".opam" in
102 let opam_path = Eio.Path.(subtree_path / opam_file) in
103 try
104 let raw_content = Eio.Path.load opam_path in
105 let opam_content =
106 Opam_transform.transform ~content:raw_content ~dev_repo ~url_src
107 in
108 Some { pkg_name; subtree; dev_repo; url_src; opam_content }
109 with Eio.Io _ -> None)
110 opam_files
111
112let discover ~fs ~config ?(sources = Sources_registry.empty) () =
113 let monorepo = Config.Paths.monorepo config in
114 let monorepo_eio = Eio.Path.(fs / Fpath.to_string monorepo) in
115 let subdirs =
116 try
117 Eio.Path.read_dir monorepo_eio
118 |> List.filter (fun name ->
119 let child = Eio.Path.(monorepo_eio / name) in
120 match Eio.Path.kind ~follow:false child with
121 | `Directory -> true
122 | _ -> false)
123 with Eio.Io _ -> []
124 in
125 Log.debug (fun m ->
126 m "Found %d subdirectories in monorepo" (List.length subdirs));
127 let packages, errors =
128 List.fold_left
129 (fun (pkgs, errs) subtree ->
130 let subtree_path = Eio.Path.(monorepo_eio / subtree) in
131 let dune_project_path = Eio.Path.(subtree_path / "dune-project") in
132 match Eio.Path.kind ~follow:false dune_project_path with
133 | `Regular_file -> (
134 let content =
135 try Some (Eio.Path.load dune_project_path) with Eio.Io _ -> None
136 in
137 match content with
138 | None -> (pkgs, errs)
139 | Some content -> (
140 match Dune_project.parse content with
141 | Error msg ->
142 Log.warn (fun m ->
143 m "Failed to parse %s/dune-project: %s" subtree msg);
144 (pkgs, msg :: errs)
145 | Ok dune_proj -> (
146 match derive_dev_repo ~sources ~subtree dune_proj with
147 | None -> (pkgs, "Cannot derive dev-repo" :: errs)
148 | Some (dev_repo, url_src) ->
149 let new_pkgs =
150 load_opam_packages ~subtree_path ~subtree ~dev_repo
151 ~url_src
152 in
153 Log.debug (fun m ->
154 m "Found %d opam files in %s" (List.length new_pkgs)
155 subtree);
156 (new_pkgs @ pkgs, errs))))
157 | _ ->
158 Log.debug (fun m -> m "No dune-project in %s, skipping" subtree);
159 (pkgs, errs)
160 | exception Eio.Io _ -> (pkgs, errs))
161 ([], []) subdirs
162 in
163 if errors <> [] then
164 Log.warn (fun m ->
165 m "Encountered %d errors during monorepo discovery" (List.length errors));
166 Log.info (fun m ->
167 m "Discovered %d packages from monorepo" (List.length packages));
168 Ok (List.rev packages)