tangled
alpha
login
or
join now
anil.recoil.org
/
unpac
0
fork
atom
A monorepo management tool for the agentic ages
0
fork
atom
overview
issues
pulls
pipelines
fetch
anil.recoil.org
3 months ago
8c809cfb
463d46f2
+197
-29
6 changed files
expand all
collapse all
unified
split
.gitignore
bin
main.ml
lib
git.ml
project.ml
unpac.ml
vendor.ml
+1
.gitignore
···
1
1
*.toml
2
2
_build
3
3
*.sh
4
4
+
.unpac.log
+170
-11
bin/main.ml
···
86
86
| None -> None
87
87
with _ -> None
88
88
89
89
+
(* Error formatting helper - strip Eio.Io prefix for cleaner output *)
90
90
+
let format_error exn =
91
91
+
let s = Printexc.to_string exn in
92
92
+
if String.length s > 7 && String.sub s 0 7 = "Eio.Io " then
93
93
+
String.sub s 7 (String.length s - 7)
94
94
+
else
95
95
+
s
96
96
+
97
97
+
(* URL rewriting for known mirrors *)
98
98
+
let rewrite_git_url url =
99
99
+
(* Rewrite erratique.ch repos to GitHub mirrors *)
100
100
+
let erratique_prefix = "https://erratique.ch/repos/" in
101
101
+
if String.length url > String.length erratique_prefix &&
102
102
+
String.sub url 0 (String.length erratique_prefix) = erratique_prefix then
103
103
+
let rest = String.sub url (String.length erratique_prefix)
104
104
+
(String.length url - String.length erratique_prefix) in
105
105
+
"https://github.com/dbuenzli/" ^ rest
106
106
+
else
107
107
+
url
108
108
+
89
109
(* Source kind selection *)
90
110
let source_kind_term =
91
111
let git =
···
234
254
let cwd_path = (cwd :> Eio.Fs.dir_ty Eio.Path.t) in
235
255
236
256
(* Check we're on a project branch *)
237
237
-
let _project = Unpac.Project.require_project_branch ~proc_mgr ~cwd:cwd_path in
257
257
+
let _project =
258
258
+
try Unpac.Project.require_project_branch ~proc_mgr ~cwd:cwd_path
259
259
+
with Failure msg ->
260
260
+
Format.eprintf "%s@." msg;
261
261
+
exit 1
262
262
+
in
238
263
239
264
(* Check for pending recovery *)
240
265
if Unpac.Recovery.has_recovery ~cwd:cwd_path then begin
···
298
323
| [] -> "unknown"
299
324
in
300
325
301
301
-
(* Reconstruct full URL for git clone *)
326
326
+
(* Reconstruct full URL for git clone, with rewrites *)
302
327
let url =
303
303
-
let first_pkg = List.hd group.packages in
304
304
-
match first_pkg.source with
305
305
-
| Unpac.Source.GitSource g -> g.url
306
306
-
| _ -> "https://" ^ url_str (* Fallback *)
328
328
+
let raw_url =
329
329
+
let first_pkg = List.hd group.packages in
330
330
+
match first_pkg.source with
331
331
+
| Unpac.Source.GitSource g -> g.url
332
332
+
| _ -> "https://" ^ url_str (* Fallback *)
333
333
+
in
334
334
+
rewrite_git_url raw_url
307
335
in
308
336
309
337
Format.printf " Adding %s (%d packages: %s)@."
···
322
350
Format.printf " [SKIP] %s already vendored@." name
323
351
| Unpac.Vendor.Failed { step; recovery_hint; error } ->
324
352
Format.eprintf " [FAIL] Failed at step '%s': %s@." step
325
325
-
(Printexc.to_string error);
353
353
+
(format_error error);
326
354
Format.eprintf " %s@." recovery_hint;
327
355
exit 1
328
356
) grouped;
···
411
439
Format.printf "[OK] %s is up to date@." name
412
440
| Unpac.Vendor.Update_failed { step; error; recovery_hint } ->
413
441
Format.eprintf "[FAIL] Failed at step '%s': %s@." step
414
414
-
(Printexc.to_string error);
442
442
+
(format_error error);
415
443
Format.eprintf "%s@." recovery_hint;
416
444
exit 1
417
445
in
···
493
521
Format.printf "[OK] %s already vendored@." name
494
522
| Unpac.Vendor.Failed { step; error; recovery_hint } ->
495
523
Format.eprintf "[FAIL] Failed at step '%s': %s@." step
496
496
-
(Printexc.to_string error);
524
524
+
(format_error error);
497
525
Format.eprintf "%s@." recovery_hint;
498
526
exit 1
499
527
in
···
753
781
const run $ logging_term $ config_file $ cache_dir_term $ output_format_term $ source_kind_term
754
782
$ resolve_deps_term $ Unpac.Solver.package_specs_term)
755
783
784
784
+
let opam_vendor_fetch_cmd =
785
785
+
let doc = "Fetch package sources into a vendor git repository." in
786
786
+
let man =
787
787
+
[
788
788
+
`S Manpage.s_description;
789
789
+
`P
790
790
+
"Fetches git sources for the specified packages into a centralized \
791
791
+
vendor git repository. This repository can then be used as a local \
792
792
+
cache for subsequent 'unpac add' operations.";
793
793
+
`P
794
794
+
"Each package's upstream is stored as a branch named 'upstream/<pkg>' \
795
795
+
in the vendor repository.";
796
796
+
`P "Use --deps to include transitive dependencies.";
797
797
+
`S Manpage.s_examples;
798
798
+
`P "Fetch a package and its dependencies:";
799
799
+
`Pre " unpac opam vendor-fetch --deps lwt --vendor-repo ./vendor-cache";
800
800
+
`P "Fetch multiple packages:";
801
801
+
`Pre " unpac opam vendor-fetch eio cmdliner fmt --vendor-repo ~/vendor";
802
802
+
]
803
803
+
in
804
804
+
let vendor_repo_arg =
805
805
+
let doc = "Path to the vendor git repository." in
806
806
+
Arg.(required & opt (some string) None & info ["vendor-repo"] ~docv:"DIR" ~doc)
807
807
+
in
808
808
+
let run () config_path cache_dir resolve_deps vendor_repo package_specs =
809
809
+
Eio_main.run @@ fun env ->
810
810
+
let fs = Eio.Stdenv.fs env in
811
811
+
let proc_mgr = Eio.Stdenv.process_mgr env in
812
812
+
let vendor_path = Eio.Path.(fs / vendor_repo) in
813
813
+
814
814
+
if package_specs = [] then begin
815
815
+
Format.eprintf "Please specify at least one package.@.";
816
816
+
exit 1
817
817
+
end;
818
818
+
819
819
+
(* Initialize vendor repo if needed *)
820
820
+
if not (Unpac.Git.is_repository vendor_path) then begin
821
821
+
Format.printf "Initializing vendor repository at %s@." vendor_repo;
822
822
+
Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 vendor_path;
823
823
+
Unpac.Git.init ~proc_mgr ~cwd:vendor_path
824
824
+
end;
825
825
+
826
826
+
(* Load opam index and resolve packages *)
827
827
+
let index = load_index ~fs ~cache_dir config_path in
828
828
+
let compiler = get_compiler_spec config_path in
829
829
+
let selection_result =
830
830
+
if resolve_deps then Unpac.Solver.select_with_deps ?compiler index package_specs
831
831
+
else Unpac.Solver.select_packages index package_specs
832
832
+
in
833
833
+
let packages = match selection_result with
834
834
+
| Error msg ->
835
835
+
Format.eprintf "Error selecting packages: %s@." msg;
836
836
+
exit 1
837
837
+
| Ok selection -> selection.packages
838
838
+
in
839
839
+
840
840
+
(* Extract sources and group by dev-repo *)
841
841
+
let sources = Unpac.Source.extract_all Unpac.Source.Git packages in
842
842
+
let grouped = Unpac.Source.group_by_dev_repo sources in
843
843
+
844
844
+
Format.printf "Found %d unique git source(s) to fetch:@." (List.length grouped);
845
845
+
846
846
+
(* Fetch each unique dev-repo *)
847
847
+
let fetched = ref 0 in
848
848
+
let skipped = ref 0 in
849
849
+
List.iter (fun (group : Unpac.Source.grouped_sources) ->
850
850
+
match group.dev_repo with
851
851
+
| None ->
852
852
+
incr skipped;
853
853
+
let pkg_names = List.map (fun (p : Unpac.Source.package_source) -> p.name) group.packages in
854
854
+
Format.printf " [SKIP] No dev-repo: %s@." (String.concat ", " pkg_names)
855
855
+
| Some _dev_repo ->
856
856
+
let opam_packages = List.map (fun (p : Unpac.Source.package_source) -> p.name) group.packages in
857
857
+
let name = match opam_packages with
858
858
+
| first :: _ -> first
859
859
+
| [] -> "unknown"
860
860
+
in
861
861
+
862
862
+
(* Get URL with rewrites *)
863
863
+
let url =
864
864
+
let raw_url =
865
865
+
let first_pkg = List.hd group.packages in
866
866
+
match first_pkg.source with
867
867
+
| Unpac.Source.GitSource g -> g.url
868
868
+
| _ -> ""
869
869
+
in
870
870
+
rewrite_git_url raw_url
871
871
+
in
872
872
+
873
873
+
if url = "" then begin
874
874
+
incr skipped;
875
875
+
Format.printf " [SKIP] No git URL for %s@." name
876
876
+
end else begin
877
877
+
Format.printf " Fetching %s from %s@." name url;
878
878
+
879
879
+
let remote = "origin-" ^ name in
880
880
+
let upstream_branch = "upstream/" ^ name in
881
881
+
882
882
+
try
883
883
+
(* Add/update remote *)
884
884
+
ignore (Unpac.Git.ensure_remote ~proc_mgr ~cwd:vendor_path ~name:remote ~url);
885
885
+
886
886
+
(* Fetch from remote *)
887
887
+
Unpac.Git.fetch ~proc_mgr ~cwd:vendor_path ~remote;
888
888
+
889
889
+
(* Detect default branch *)
890
890
+
let default_branch = Unpac.Git.ls_remote_default_branch ~proc_mgr ~url in
891
891
+
let ref_point = remote ^ "/" ^ default_branch in
892
892
+
893
893
+
(* Create/update upstream branch *)
894
894
+
Unpac.Git.branch_force ~proc_mgr ~cwd:vendor_path ~name:upstream_branch ~point:ref_point;
895
895
+
896
896
+
incr fetched;
897
897
+
Format.printf " [OK] %s -> %s@." name upstream_branch
898
898
+
with exn ->
899
899
+
Format.eprintf " [FAIL] %s: %s@." name (format_error exn)
900
900
+
end
901
901
+
) grouped;
902
902
+
903
903
+
Format.printf "Done: %d fetched, %d skipped@." !fetched !skipped
904
904
+
in
905
905
+
let info = Cmd.info "vendor-fetch" ~doc ~man in
906
906
+
Cmd.v info
907
907
+
Term.(
908
908
+
const run $ logging_term $ config_file $ cache_dir_term
909
909
+
$ resolve_deps_term $ vendor_repo_arg $ Unpac.Solver.package_specs_term)
910
910
+
756
911
(* Opam subcommand group *)
757
912
758
913
let opam_cmd =
···
766
921
]
767
922
in
768
923
let info = Cmd.info "opam" ~doc ~man in
769
769
-
Cmd.group info [ opam_list_cmd; opam_info_cmd; opam_related_cmd; opam_sources_cmd ]
924
924
+
Cmd.group info [ opam_list_cmd; opam_info_cmd; opam_related_cmd; opam_sources_cmd; opam_vendor_fetch_cmd ]
770
925
771
926
(* ============================================================================
772
927
MAIN COMMAND
···
798
953
let info = Cmd.info "unpac" ~version:"0.1.0" ~doc ~man in
799
954
Cmd.group info [ init_cmd; project_cmd; add_cmd; vendor_cmd; opam_cmd ]
800
955
801
801
-
let () = exit (Cmd.eval main_cmd)
956
956
+
let () =
957
957
+
Unpac.Txn_log.start_session ~args:(List.tl (Array.to_list Sys.argv));
958
958
+
let exit_code = Cmd.eval main_cmd in
959
959
+
Unpac.Txn_log.end_session ~exit_code;
960
960
+
exit exit_code
+15
-10
lib/git.ml
···
115
115
let status = Eio.Process.await child in
116
116
let stdout = Buffer.contents stdout_buf in
117
117
let stderr = Buffer.contents stderr_buf in
118
118
-
match status with
119
119
-
| `Exited 0 ->
120
120
-
Log.debug (fun m -> m "Output: %s" (string_trim stdout));
121
121
-
Ok stdout
122
122
-
| `Exited exit_code ->
123
123
-
Log.debug (fun m -> m "Failed (exit %d): %s" exit_code (string_trim stderr));
124
124
-
Error (Command_failed { cmd = args; exit_code; stdout; stderr })
125
125
-
| `Signaled signal ->
126
126
-
Log.debug (fun m -> m "Killed by signal %d" signal);
127
127
-
Error (Command_failed { cmd = args; exit_code = 128 + signal; stdout; stderr })
118
118
+
let exit_code, result = match status with
119
119
+
| `Exited 0 ->
120
120
+
Log.debug (fun m -> m "Output: %s" (string_trim stdout));
121
121
+
0, Ok stdout
122
122
+
| `Exited code ->
123
123
+
Log.debug (fun m -> m "Failed (exit %d): %s" code (string_trim stderr));
124
124
+
code, Error (Command_failed { cmd = args; exit_code = code; stdout; stderr })
125
125
+
| `Signaled signal ->
126
126
+
Log.debug (fun m -> m "Killed by signal %d" signal);
127
127
+
let code = 128 + signal in
128
128
+
code, Error (Command_failed { cmd = args; exit_code = code; stdout; stderr })
129
129
+
in
130
130
+
(* Log to transaction log *)
131
131
+
Txn_log.log_git_command ~args ~exit_code ~stdout ~stderr;
132
132
+
result
128
133
with exn ->
129
134
Log.err (fun m -> m "Exception running git: %a" Fmt.exn exn);
130
135
raise exn
+6
lib/project.ml
···
226
226
*.o
227
227
*.a
228
228
.unpac/
229
229
+
.unpac.log
229
230
|}
230
231
in
231
232
Eio.Path.save ~create:(`Or_truncate 0o644) gitignore_path gitignore_content;
···
275
276
description = "%s"
276
277
277
278
[opam]
279
279
+
# Repositories are listed in priority order (later ones take priority).
280
280
+
# repositories = [
281
281
+
# { name = "default", path = "/path/to/opam-repository" },
282
282
+
# { name = "custom", path = "/path/to/custom-repo" },
283
283
+
# ]
278
284
repositories = []
279
285
# compiler = "ocaml.5.3.0"
280
286
+3
lib/unpac.ml
···
13
13
module Recovery = Recovery
14
14
module Vendor = Vendor
15
15
module Project = Project
16
16
+
17
17
+
(** Logging *)
18
18
+
module Txn_log = Txn_log
+2
-8
lib/vendor.ml
···
169
169
tm.Unix.tm_min
170
170
tm.Unix.tm_sec
171
171
172
172
-
(* Execute a single step *)
172
172
+
(* Execute a single step - Git module handles its own logging *)
173
173
let execute_step ~proc_mgr ~cwd step =
174
174
let open Recovery in
175
175
match step with
176
176
| Remote_add { remote; url } ->
177
177
-
Log.info (fun m -> m "Adding remote %s -> %s" remote url);
178
177
ignore (Git.ensure_remote ~proc_mgr ~cwd ~name:remote ~url)
179
178
180
179
| Fetch { remote } ->
181
181
-
Log.info (fun m -> m "Fetching from %s..." remote);
182
180
Git.fetch ~proc_mgr ~cwd ~remote
183
181
184
182
| Create_upstream { branch; start_point } ->
185
185
-
Log.info (fun m -> m "Creating pristine branch: %s" branch);
186
183
ignore (Git.ensure_branch ~proc_mgr ~cwd ~name:branch ~start_point)
187
184
188
185
| Create_vendor { name; upstream } ->
189
189
-
Log.info (fun m -> m "Creating vendor branch with path rewrite: %s" (vendor_branch name));
186
186
+
Log.info (fun m -> m "Creating vendor branch: %s" (vendor_branch name));
190
187
let current = Git.current_branch ~proc_mgr ~cwd in
191
188
let vbranch = vendor_branch name in
192
189
···
218
215
end
219
216
220
217
| Create_patches { branch; vendor } ->
221
221
-
Log.info (fun m -> m "Creating patches branch: %s" branch);
222
218
ignore (Git.ensure_branch ~proc_mgr ~cwd ~name:branch ~start_point:vendor)
223
219
224
220
| Merge_to_project { patches } ->
225
225
-
Log.info (fun m -> m "Merging %s into project branch..." patches);
226
221
let pkg_name =
227
222
if String.starts_with ~prefix:"patches/" patches then
228
223
String.sub patches 8 (String.length patches - 8)
···
239
234
end
240
235
241
236
| Update_toml { package_name = _ } ->
242
242
-
Log.info (fun m -> m "Updating unpac.toml...");
243
237
(* TODO: Actually update the TOML file *)
244
238
()
245
239