···120120repositories = [
121121 { name = "default", path = "/path/to/opam-repository" },
122122]
123123-compiler = "5.4.0"
123123+compiler = "5.2.0"
124124125125-# Optional: override default XDG cache location
126126-# vendor_cache = "/path/to/vendor-cache"
127127-128128-[cargo]
129129-# Future
125125+# Optional: vendor cache location (default: $XDG_CACHE_HOME/unpac/vendor-cache)
126126+vendor_cache = "/path/to/vendor-cache"
130127131128[projects]
132129# Project existence only, no package lists
···136133137134## Vendor Cache
138135139139-A bare git repository that caches fetched packages to avoid hitting upstream remotes.
136136+A bare git repository that caches fetched packages to avoid repeated network fetches across multiple unpac projects.
137137+138138+### Location Priority
139139+140140+1. CLI flag: `--cache /path/to/cache`
141141+2. Environment variable: `UNPAC_VENDOR_CACHE=/path/to/cache`
142142+3. Config file: `vendor_cache = "/path/to/cache"` in unpac.toml
143143+4. Default: `$XDG_CACHE_HOME/unpac/vendor-cache` (or `~/.cache/unpac/vendor-cache`)
144144+145145+### Cache Structure
140146141141-**Default location**: `$XDG_CACHE_HOME/unpac/vendor-cache/` (via xdge)
147147+The vendor cache is a bare git repository with remotes named after their URLs:
142148143143-**Override**: Set `vendor_cache` in config or pass `--vendor-cache` on CLI.
149149+```
150150+/path/to/vendor-cache/ # Bare git repository
151151+├── config
152152+├── objects/ # Shared git objects
153153+├── refs/
154154+│ └── remotes/
155155+│ ├── github.com/dbuenzli/astring/
156156+│ │ └── master
157157+│ ├── github.com/dbuenzli/cmdliner/
158158+│ │ └── master
159159+│ └── github.com/ocaml/dune/
160160+│ └── main
161161+└── ...
162162+```
144163145145-The cache holds:
146146-- `opam/upstream/*` branches (pristine upstream)
147147-- `opam/vendor/*` branches (pre-built with vendor prefix)
164164+Each remote is named using URL-based naming:
165165+- `https://github.com/dbuenzli/astring.git` → remote `github.com/dbuenzli/astring`
166166+- `https://github.com/ocaml/dune.git` → remote `github.com/ocaml/dune`
167167+168168+### Cache Workflow
169169+170170+When adding a package:
171171+172172+1. **Check cache**: Look for remote `github.com/owner/repo` in cache
173173+2. **Fetch to cache**: If not present (or stale), fetch from upstream to cache
174174+3. **Fetch to project**: Fetch from cache to project's `git/` bare repo
175175+4. **Create branches**: Create `opam/upstream/<pkg>` etc. in project
176176+177177+```
178178+ Upstream Vendor Cache Project git/
179179+ (GitHub) (bare repo) (bare repo)
180180+ │ │ │
181181+ │ fetch (if needed) │ │
182182+ ├──────────────────────────►│ │
183183+ │ │ fetch from cache │
184184+ │ ├───────────────────────────►│
185185+ │ │ │
186186+```
187187+188188+### Cache Commands
189189+190190+```bash
191191+# Add package using cache
192192+unpac opam add --cache /path/to/cache https://github.com/foo/bar.git
193193+194194+# Using environment variable
195195+export UNPAC_VENDOR_CACHE=/path/to/cache
196196+unpac opam add astring
197197+198198+# Configure in unpac.toml
199199+vendor_cache = "/path/to/cache"
200200+```
201201+202202+### Cache Benefits
148203149149-Projects fetch from the cache as a git remote.
204204+- **Shared across projects**: Multiple unpac projects share the same cache
205205+- **Offline support**: Once cached, packages can be added without network
206206+- **Faster operations**: No redundant downloads for common packages
207207+- **Persistent**: Cache survives project deletion
150208151209## Derivable State
152210···1812391. Create orphan branch `project/myapp` with template:
182240 - `dune-project` (lang dune 3.20)
183241 - `dune` with `(vendored_dirs vendor)`
242242+ - `vendor/opam/` directory
1842432. Create worktree `project/myapp/`
1852443. Add to `[projects]` in `main/unpac.toml`
186245187187-### Add Packages
246246+### Add Package (by URL)
247247+248248+```bash
249249+unpac opam add https://github.com/dbuenzli/astring.git
250250+```
251251+252252+1. Detect default branch from remote
253253+2. If cache configured: fetch upstream → cache → project
254254+3. If no cache: fetch upstream → project directly
255255+4. Create `opam/upstream/astring` branch
256256+5. Create `opam/vendor/astring` orphan branch (with vendor/opam/astring/ prefix)
257257+6. Create `opam/patches/astring` branch (from vendor)
258258+7. Cleanup temporary worktrees
259259+260260+### Add Package (by name)
261261+262262+```bash
263263+unpac opam repo add default /path/to/opam-repository
264264+unpac opam config compiler 5.2.0
265265+unpac opam add astring
266266+```
267267+268268+1. Look up package in configured opam repositories
269269+2. Extract dev-repo URL from opam file
270270+3. Proceed as with URL-based add
271271+272272+### Add with Dependency Solving
188273189274```bash
190190-unpac opam add eio lwt --project=myapp
275275+unpac opam add eio --solve
191276```
192277193193-1. Resolve dependencies (eio, lwt → full dependency tree)
194194-2. For each package:
195195- a. Check vendor cache, fetch from upstream if missing
196196- b. Create `opam/upstream/<pkg>` branch
197197- c. Create `opam/vendor/<pkg>` orphan branch (with prefix)
198198- d. Create `opam/patches/<pkg>` branch (from vendor)
199199-3. Merge all `opam/patches/*` into `project/myapp`
278278+1. Solve dependencies using configured compiler version
279279+2. For each package in solution:
280280+ - Look up dev-repo URL
281281+ - Add package (via cache if configured)
282282+3. Report summary
283283+284284+### Merge into Project
285285+286286+```bash
287287+unpac opam merge astring myapp
288288+```
200289201201-Worktrees are created temporarily during operations and cleaned up after.
290290+1. Merge `opam/patches/astring` into `project/myapp` (allow unrelated histories)
291291+2. Package files appear at `project/myapp/vendor/opam/astring/`
202292203293### Update Packages
204294205295```bash
206206-unpac opam update eio --project=myapp
296296+unpac opam update astring
207297```
208298209209-1. Fetch latest upstream into `opam/upstream/eio`
210210-2. Update `opam/vendor/eio` with new content
211211-3. Rebase `opam/patches/eio` onto new vendor
212212- - If conflicts: leave worktree for manual resolution
213213-4. Merge updated patches into `project/myapp`
299299+1. Fetch latest from upstream (via cache if configured)
300300+2. Compare old vs new upstream SHA
301301+3. If changed:
302302+ - Update `opam/upstream/astring` branch
303303+ - Update `opam/vendor/astring` with new content
304304+4. Note: patches branch must be rebased separately
214305215306### Edit Patches
216307···226317unpac opam done astring
227318```
228319229229-1. Remove worktree (keeps branch and commits)
320320+1. Check for uncommitted changes
321321+2. Remove worktree (keeps branch and commits)
322322+323323+### View Changes
230324231325```bash
232232-# Then merge into project
233233-cd project/myapp
234234-git merge opam/patches/astring
326326+unpac opam diff astring
235327```
236328237237-### Vendor Cache Operations
329329+Shows diff between `opam/vendor/astring` and `opam/patches/astring`.
330330+331331+### Remove Package
238332239333```bash
240240-# Fetch packages into cache (without adding to any project)
241241-unpac cache fetch eio lwt --deps
334334+unpac opam remove astring
242335```
243336244244-1. Resolve dependencies
245245-2. Fetch each package into vendor cache
246246-3. Create `opam/upstream/*` and `opam/vendor/*` branches in cache
337337+1. Remove any existing worktrees
338338+2. Delete `opam/upstream/astring`, `opam/vendor/astring`, `opam/patches/astring` branches
339339+3. Remove remote `origin-astring`
247340248341## Module Structure
249342···254347├── init.ml # Project initialization
255348├── config.ml # TOML config parsing
256349├── worktree.ml # Git worktree lifecycle management
350350+├── vendor_cache.ml # Vendor cache (bare git repo)
257351├── git.ml # Low-level git operations
258352├── git_repo_lookup.ml # URL rewriting (erratique → github, etc.)
259259-├── cache.ml # Vendor cache management
260260-│
261353├── backend.ml # Backend module signature
262354│
263263-├── opam/ # Opam backend
264264-│ ├── opam_backend.ml # Backend implementation
265265-│ ├── upstream.ml # opam/upstream/* management
266266-│ ├── vendor.ml # opam/vendor/* management
267267-│ ├── patches.ml # opam/patches/* management
268268-│ ├── solver.ml # Dependency resolution
269269-│ ├── source.ml # Source extraction
270270-│ ├── repo_index.ml # Repository indexing
271271-│ └── dev_repo.ml # Dev-repo normalization
272272-│
273273-└── project.ml # Project branch operations
355355+└── opam/ # Opam backend
356356+ ├── opam.ml # Main backend implementation
357357+ ├── repo.ml # Repository package lookup
358358+ └── solver.ml # Dependency resolution
274359275360bin/
276361└── main.ml # CLI
···281366Manages worktree lifecycle within the unpac directory structure.
282367283368```ocaml
284284-type root
369369+type root = Eio.Fs.dir_ty Eio.Path.t
285370(** The unpac project root (contains git/, main/, etc.) *)
286371287372type kind =
···291376 | Opam_vendor of string
292377 | Opam_patches of string
293378294294-val path : root -> kind -> path
379379+val path : root -> kind -> Eio.Fs.dir_ty Eio.Path.t
295380(** Filesystem path for a worktree kind. *)
296381297382val branch : kind -> string
298383(** Git branch name for a worktree kind. *)
299384300300-val ensure : proc_mgr -> root -> kind -> unit
385385+val ensure : proc_mgr:Git.proc_mgr -> root -> kind -> unit
301386(** Create worktree if it doesn't exist. *)
302387303303-val remove : proc_mgr -> root -> kind -> unit
388388+val ensure_orphan : proc_mgr:Git.proc_mgr -> root -> kind -> unit
389389+(** Create orphan worktree if it doesn't exist. *)
390390+391391+val remove : proc_mgr:Git.proc_mgr -> root -> kind -> unit
304392(** Remove worktree (keeps branch). *)
305393306306-val with_temp : proc_mgr -> root -> kind -> (path -> 'a) -> 'a
394394+val with_temp : proc_mgr:Git.proc_mgr -> root -> kind -> (path -> 'a) -> 'a
307395(** Create worktree, run function, remove worktree. *)
308396```
309397398398+### Key Module: `vendor_cache.ml`
399399+400400+Manages the persistent vendor cache bare repository.
401401+402402+```ocaml
403403+type t = Eio.Fs.dir_ty Eio.Path.t
404404+(** Path to the cache bare repository *)
405405+406406+val init : proc_mgr:Git.proc_mgr -> fs:Eio.Fs.dir_ty Eio.Path.t ->
407407+ ?path:string -> unit -> t
408408+(** Initialize cache, creating bare repo if needed. *)
409409+410410+val url_to_remote_name : string -> string
411411+(** Convert URL to remote name: "https://github.com/foo/bar.git" → "github.com/foo/bar" *)
412412+413413+val fetch : proc_mgr:Git.proc_mgr -> t -> url:string -> string
414414+(** Fetch from URL into cache, returns remote name. *)
415415+416416+val fetch_to_project : proc_mgr:Git.proc_mgr -> cache:t ->
417417+ project_git:Eio.Fs.dir_ty Eio.Path.t ->
418418+ url:string -> branch:string -> string
419419+(** Fetch via cache into project's git repo. Returns cache ref name. *)
420420+```
421421+310422### Key Module: `backend.ml`
311423312424Signature for package manager backends.
313425314426```ocaml
315315-module type S = sig
316316- val name : string
317317- (** "opam", "cargo", etc. *)
427427+type package_info = {
428428+ name : string;
429429+ url : string;
430430+ branch : string option;
431431+}
318432319319- val upstream_branch : string -> string
320320- val vendor_branch : string -> string
321321- val patches_branch : string -> string
322322- val vendor_path : string -> string
433433+type add_result =
434434+ | Added of { name : string; sha : string }
435435+ | Already_exists of string
436436+ | Failed of { name : string; error : string }
323437324324- val add :
325325- proc_mgr -> root -> cache:path -> name:string -> url:string -> branch:string -> unit
438438+type update_result =
439439+ | Updated of { name : string; old_sha : string; new_sha : string }
440440+ | No_changes of string
441441+ | Update_failed of { name : string; error : string }
326442327327- val update :
328328- proc_mgr -> root -> name:string -> unit
329329-end
443443+val merge_to_project : proc_mgr:Git.proc_mgr -> root:Worktree.root ->
444444+ project:string -> patches_branch:string ->
445445+ (unit, [`Conflict of string list]) result
330446```
331447332448## CLI Commands
···341457unpac project list
342458 List projects
343459344344-unpac project remove <name>
345345- Remove project branch and worktree
460460+unpac opam repo add <name> <path>
461461+ Add opam repository for package lookups
462462+463463+unpac opam repo list
464464+ List configured repositories
465465+466466+unpac opam repo remove <name>
467467+ Remove opam repository
468468+469469+unpac opam config compiler [version]
470470+ Get or set OCaml compiler version
471471+472472+unpac opam add <pkg|url> [--name NAME] [--branch BRANCH] [--solve] [--cache PATH]
473473+ Add package by name or URL
474474+ --solve: resolve dependencies and add all
475475+ --cache: use vendor cache at PATH
346476347347-unpac opam add <pkg...> --project=<name> [--deps]
348348- Add packages to project (--deps is default)
477477+unpac opam list
478478+ List vendored packages
349479350350-unpac opam update <pkg...> --project=<name>
351351- Update packages from upstream
480480+unpac opam info <pkg>
481481+ Show package information
352482353353-unpac opam remove <pkg...> --project=<name>
354354- Remove packages from project
483483+unpac opam diff <pkg>
484484+ Show local changes (patches vs vendor)
355485356486unpac opam edit <pkg>
357487 Create patches worktree for editing
358488359489unpac opam done <pkg>
360360- Remove patches worktree
490490+ Close patches worktree
361491362362-unpac opam status [--project=<name>]
363363- Show package status
492492+unpac opam update <pkg>
493493+ Update package from upstream
364494365365-unpac cache fetch <pkg...> [--deps]
366366- Fetch packages into vendor cache
495495+unpac opam merge <pkg> <project>
496496+ Merge package into project
367497368368-unpac cache status
369369- Show cache status
498498+unpac opam remove <pkg>
499499+ Remove vendored package
370500```
501501+502502+## URL Rewriting
503503+504504+The `git_repo_lookup.ml` module rewrites URLs for known mirrors:
505505+506506+| Original | Rewritten |
507507+|----------|-----------|
508508+| `https://erratique.ch/repos/fmt` | `https://github.com/dbuenzli/fmt` |
509509+| `git+https://...` | `https://...` (strip git+ prefix) |
510510+511511+This ensures we fetch from reliable mirrors when available.
371512372513## Future: Cargo Backend
373514
+324
CLI.md
···11+# Unpac CLI Workflow
22+33+## Overview
44+55+Unpac is a vendoring tool that lets you maintain local patches to dependencies while tracking upstream changes. This document describes the ideal CLI workflow.
66+77+## Quick Start
88+99+```bash
1010+# Initialize a new unpac workspace
1111+unpac init myworkspace
1212+cd myworkspace
1313+1414+# Configure an opam repository
1515+unpac opam repo add opam-repository /path/to/opam-repository
1616+1717+# Create a project
1818+unpac project new myapp
1919+2020+# Add a dependency (looks up dev-repo in opam repository)
2121+unpac opam add cmdliner
2222+2323+# Edit the vendored package (opens patches worktree)
2424+unpac opam edit cmdliner
2525+# ... make changes, e.g., port to dune ...
2626+cd opam/patches/cmdliner
2727+git add -A && git commit -m "Port cmdliner to dune"
2828+cd ../../..
2929+3030+# Add the patched package to your project
3131+unpac opam merge cmdliner myapp
3232+3333+# Build your project
3434+cd project/myapp
3535+dune build
3636+```
3737+3838+## Commands
3939+4040+### Initialization
4141+4242+#### `unpac init <path>`
4343+4444+Initialize a new unpac workspace at the given path.
4545+4646+```bash
4747+unpac init myworkspace
4848+```
4949+5050+Creates:
5151+- `git/` - bare git repository (shared object store)
5252+- `main/` - metadata branch with `unpac.toml`
5353+5454+### Project Management
5555+5656+#### `unpac project new <name>`
5757+5858+Create a new project branch.
5959+6060+```bash
6161+unpac project new myapp
6262+```
6363+6464+Creates an orphan branch `project/<name>` with:
6565+- `dune-project` with dune lang 3.20
6666+- `dune` with `(vendored_dirs vendor)`
6767+- `vendor/opam/` directory structure
6868+6969+#### `unpac project list`
7070+7171+List all projects in the workspace.
7272+7373+```bash
7474+unpac project list
7575+```
7676+7777+### Opam Repository Management
7878+7979+#### `unpac opam repo add <name> <path-or-url>`
8080+8181+Add an opam repository for package lookups.
8282+8383+```bash
8484+# Local repository
8585+unpac opam repo add opam-repository /workspace/opam/opam-repository
8686+8787+# Remote repository (planned)
8888+unpac opam repo add opam-repository https://github.com/ocaml/opam-repository
8989+```
9090+9191+#### `unpac opam repo list`
9292+9393+List configured opam repositories.
9494+9595+```bash
9696+unpac opam repo list
9797+```
9898+9999+#### `unpac opam repo remove <name>`
100100+101101+Remove an opam repository.
102102+103103+```bash
104104+unpac opam repo remove opam-repository
105105+```
106106+107107+### Package Vendoring
108108+109109+#### `unpac opam add <package-or-url> [--name <name>] [--version <version>]`
110110+111111+Vendor a package. Can specify either:
112112+- A package name (looks up dev-repo in configured repositories)
113113+- A git URL directly
114114+115115+```bash
116116+# By package name (recommended)
117117+unpac opam add cmdliner
118118+unpac opam add cmdliner --version 1.3.0
119119+120120+# By URL (for packages not in a repository)
121121+unpac opam add https://github.com/dbuenzli/cmdliner.git --name cmdliner
122122+```
123123+124124+This creates three branches:
125125+- `opam/upstream/<pkg>` - pristine upstream code
126126+- `opam/vendor/<pkg>` - code with `vendor/opam/<pkg>/` path prefix
127127+- `opam/patches/<pkg>` - your local modifications (initially same as vendor)
128128+129129+#### `unpac opam list`
130130+131131+List all vendored packages.
132132+133133+```bash
134134+unpac opam list
135135+```
136136+137137+#### `unpac opam edit <package>`
138138+139139+Open the patches worktree for editing a package.
140140+141141+```bash
142142+unpac opam edit cmdliner
143143+```
144144+145145+Creates/checks out `opam/patches/cmdliner/` worktree. Make your changes there, then commit:
146146+147147+```bash
148148+cd opam/patches/cmdliner
149149+# ... edit files ...
150150+git add -A
151151+git commit -m "Port to dune build system"
152152+```
153153+154154+#### `unpac opam done <package>`
155155+156156+Close the patches worktree (cleanup after editing).
157157+158158+```bash
159159+unpac opam done cmdliner
160160+```
161161+162162+#### `unpac opam update <package>`
163163+164164+Update a package from upstream.
165165+166166+```bash
167167+unpac opam update cmdliner
168168+```
169169+170170+This:
171171+1. Fetches latest from upstream
172172+2. Updates `opam/upstream/<pkg>` and `opam/vendor/<pkg>`
173173+3. Prints instructions for rebasing patches if needed
174174+175175+#### `unpac opam rebase <package>`
176176+177177+Rebase your patches onto the updated vendor branch.
178178+179179+```bash
180180+unpac opam rebase cmdliner
181181+```
182182+183183+Opens the patches worktree for conflict resolution if needed.
184184+185185+#### `unpac opam merge <package> <project>`
186186+187187+Merge a vendored package into a project.
188188+189189+```bash
190190+unpac opam merge cmdliner myapp
191191+```
192192+193193+Merges `opam/patches/<pkg>` into `project/<name>`, placing files under `vendor/opam/<pkg>/`.
194194+195195+#### `unpac opam remove <package>`
196196+197197+Remove a vendored package.
198198+199199+```bash
200200+unpac opam remove cmdliner
201201+```
202202+203203+### Package Information
204204+205205+#### `unpac opam info <package>`
206206+207207+Show information about a vendored package.
208208+209209+```bash
210210+unpac opam info cmdliner
211211+```
212212+213213+Shows:
214214+- Upstream URL
215215+- Current upstream SHA
216216+- Current patches SHA
217217+- Number of local commits
218218+- Projects using this package
219219+220220+#### `unpac opam diff <package>`
221221+222222+Show the diff between vendor and patches (your local changes).
223223+224224+```bash
225225+unpac opam diff cmdliner
226226+```
227227+228228+## Workflow Examples
229229+230230+### Porting a Package to Dune
231231+232232+```bash
233233+# Setup
234234+unpac init myworkspace && cd myworkspace
235235+unpac opam repo add opam-repository /workspace/opam/opam-repository
236236+unpac project new myapp
237237+238238+# Vendor and patch
239239+unpac opam add cmdliner
240240+unpac opam edit cmdliner
241241+242242+cd opam/patches/cmdliner
243243+# Add dune files, remove _tags, etc.
244244+git add -A
245245+git commit -m "Port cmdliner to dune"
246246+cd ../../..
247247+248248+unpac opam done cmdliner
249249+250250+# Use in project
251251+unpac opam merge cmdliner myapp
252252+cd project/myapp
253253+dune build
254254+```
255255+256256+### Updating a Patched Package
257257+258258+```bash
259259+# Update from upstream
260260+unpac opam update cmdliner
261261+262262+# Rebase your patches
263263+unpac opam rebase cmdliner
264264+265265+# If conflicts, resolve them:
266266+cd opam/patches/cmdliner
267267+# ... resolve conflicts ...
268268+git add -A
269269+git rebase --continue
270270+cd ../../..
271271+272272+unpac opam done cmdliner
273273+274274+# Re-merge into project
275275+unpac opam merge cmdliner myapp
276276+```
277277+278278+### Adding Multiple Dependencies
279279+280280+```bash
281281+unpac opam add fmt
282282+unpac opam add logs
283283+unpac opam add cmdliner
284284+285285+# Merge all into project
286286+unpac opam merge fmt myapp
287287+unpac opam merge logs myapp
288288+unpac opam merge cmdliner myapp
289289+```
290290+291291+## Directory Structure
292292+293293+After setup, your workspace looks like:
294294+295295+```
296296+myworkspace/
297297+├── git/ # Bare git repo (shared objects)
298298+├── main/ # Metadata worktree
299299+│ └── unpac.toml # Configuration
300300+├── project/
301301+│ └── myapp/ # Project worktree
302302+│ ├── dune-project
303303+│ ├── dune
304304+│ └── vendor/
305305+│ └── opam/
306306+│ ├── cmdliner/ # Merged vendor code
307307+│ └── fmt/
308308+└── opam/ # Package worktrees (on-demand)
309309+ └── patches/
310310+ └── cmdliner/ # When editing
311311+```
312312+313313+## Configuration (unpac.toml)
314314+315315+```toml
316316+[opam]
317317+repositories = [
318318+ { name = "opam-repository", path = "/workspace/opam/opam-repository" }
319319+]
320320+# compiler = "5.4.0" # Optional: pin compiler version
321321+322322+[projects]
323323+myapp = {}
324324+```
-18
TODO.md
···11-# TODO
22-33-## Vendor Cache Git Repository
44-55-Add a persistent vendor cache as a bare git repository in the XDG cache directory
66-(`$XDG_CACHE_HOME/unpac/vendor-cache` or `~/.cache/unpac/vendor-cache`).
77-88-This cache would:
99-- Store fetched upstream repositories as branches (e.g., `github.com/dbuenzli/astring/master`)
1010-- Persist across multiple unpac project runs
1111-- Save network fetches when the same package is used in multiple projects
1212-- Allow offline operations when packages are already cached
1313-1414-Implementation notes:
1515-- Initialize bare git repo on first use
1616-- Add remotes and fetch before cloning into project's upstream branches
1717-- Use `git fetch --all` to update cache
1818-- Store branch names using URL-based naming (e.g., `github.com/owner/repo/branch`)
+601-39
bin/main.ml
···2222 | Some root ->
2323 f ~env ~fs ~proc_mgr ~root
24242525+(* Helper to get config path *)
2626+let config_path root =
2727+ let main_path = Unpac.Worktree.path root Unpac.Worktree.Main in
2828+ Eio.Path.(main_path / "unpac.toml") |> snd
2929+3030+(* Helper to load config *)
3131+let load_config root =
3232+ let path = config_path root in
3333+ match Unpac.Config.load path with
3434+ | Ok cfg -> cfg
3535+ | Error _ -> Unpac.Config.empty
3636+3737+(* Helper to save config and commit *)
3838+let save_config ~proc_mgr root config msg =
3939+ let path = config_path root in
4040+ Unpac.Config.save_exn path config;
4141+ let main_wt = Unpac.Worktree.path root Unpac.Worktree.Main in
4242+ Unpac.Git.run_exn ~proc_mgr ~cwd:main_wt ["add"; "unpac.toml"] |> ignore;
4343+ Unpac.Git.run_exn ~proc_mgr ~cwd:main_wt ["commit"; "-m"; msg] |> ignore
4444+4545+(* Check if string looks like a URL or path (vs a package name) *)
4646+let is_url_or_path s =
4747+ String.starts_with ~prefix:"http://" s ||
4848+ String.starts_with ~prefix:"https://" s ||
4949+ String.starts_with ~prefix:"git@" s ||
5050+ String.starts_with ~prefix:"git://" s ||
5151+ String.starts_with ~prefix:"ssh://" s ||
5252+ String.starts_with ~prefix:"file://" s ||
5353+ String.starts_with ~prefix:"/" s || (* Absolute path *)
5454+ String.starts_with ~prefix:"./" s || (* Relative path *)
5555+ String.starts_with ~prefix:"../" s || (* Relative path *)
5656+ String.contains s ':' (* URL with scheme *)
5757+5858+(* Helper to resolve vendor cache *)
5959+let resolve_cache ~proc_mgr ~fs ~config ~cli_cache =
6060+ match Unpac.Config.resolve_vendor_cache ?cli_override:cli_cache config with
6161+ | None -> None
6262+ | Some path ->
6363+ Some (Unpac.Vendor_cache.init ~proc_mgr ~fs ~path ())
6464+2565(* Init command *)
2666let init_cmd =
2767 let doc = "Initialize a new unpac project." in
···3474 let fs = Eio.Stdenv.fs env in
3575 let proc_mgr = Eio.Stdenv.process_mgr env in
3676 let _root = Unpac.Init.init ~proc_mgr ~fs path in
3737- Format.printf "Initialized unpac project at %s@." path
7777+ Format.printf "Initialized unpac project at %s@." path;
7878+ Format.printf "@.Next steps:@.";
7979+ Format.printf " cd %s@." path;
8080+ Format.printf " unpac opam repo add <name> <path> # configure opam repository@.";
8181+ Format.printf " unpac project new <name> # create a project@."
3882 in
3983 let info = Cmd.info "init" ~doc in
4084 Cmd.v info Term.(const run $ logging_term $ path_arg)
···4993 let run () name =
5094 with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
5195 let _path = Unpac.Init.create_project ~proc_mgr root name in
5252- Format.printf "Created project %s@." name
9696+ Format.printf "Created project %s@." name;
9797+ Format.printf "@.Next steps:@.";
9898+ Format.printf " unpac opam add <package> # vendor a package@.";
9999+ Format.printf " unpac opam merge <package> %s # merge package into project@." name
53100 in
54101 let info = Cmd.info "new" ~doc in
55102 Cmd.v info Term.(const run $ logging_term $ name_arg)
···71118 let info = Cmd.info "project" ~doc in
72119 Cmd.group info [project_new_cmd; project_list_cmd]
731207474-(* Opam add command *)
121121+(* Opam repo add command *)
122122+let opam_repo_add_cmd =
123123+ let doc = "Add an opam repository for package lookups." in
124124+ let name_arg =
125125+ let doc = "Name for the repository." in
126126+ Arg.(required & pos 0 (some string) None & info [] ~docv:"NAME" ~doc)
127127+ in
128128+ let path_arg =
129129+ let doc = "Path to the repository (local directory)." in
130130+ Arg.(required & pos 1 (some string) None & info [] ~docv:"PATH" ~doc)
131131+ in
132132+ let run () name path =
133133+ with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
134134+ let config = load_config root in
135135+ (* Check if already exists *)
136136+ if Unpac.Config.find_repo config name <> None then begin
137137+ Format.eprintf "Repository '%s' already exists@." name;
138138+ exit 1
139139+ end;
140140+ (* Resolve to absolute path *)
141141+ let abs_path =
142142+ if Filename.is_relative path then
143143+ Filename.concat (Sys.getcwd ()) path
144144+ else path
145145+ in
146146+ (* Check path exists *)
147147+ if not (Sys.file_exists abs_path && Sys.is_directory abs_path) then begin
148148+ Format.eprintf "Error: '%s' is not a valid directory@." abs_path;
149149+ exit 1
150150+ end;
151151+ let repo : Unpac.Config.repo_config = {
152152+ repo_name = name;
153153+ source = Local abs_path;
154154+ } in
155155+ let config' = Unpac.Config.add_repo config repo in
156156+ save_config ~proc_mgr root config' (Printf.sprintf "Add repository %s" name);
157157+ Format.printf "Added repository %s at %s@." name abs_path;
158158+ Format.printf "@.Next: unpac opam add <package> # vendor a package by name@."
159159+ in
160160+ let info = Cmd.info "add" ~doc in
161161+ Cmd.v info Term.(const run $ logging_term $ name_arg $ path_arg)
162162+163163+(* Opam repo list command *)
164164+let opam_repo_list_cmd =
165165+ let doc = "List configured opam repositories." in
166166+ let run () =
167167+ with_root @@ fun ~env:_ ~fs:_ ~proc_mgr:_ ~root ->
168168+ let config = load_config root in
169169+ if config.opam.repositories = [] then begin
170170+ Format.printf "No repositories configured@.";
171171+ Format.printf "@.Hint: unpac opam repo add <name> <path>@."
172172+ end else
173173+ List.iter (fun (r : Unpac.Config.repo_config) ->
174174+ let path = match r.source with
175175+ | Local p -> p
176176+ | Remote u -> u
177177+ in
178178+ Format.printf "%s: %s@." r.repo_name path
179179+ ) config.opam.repositories
180180+ in
181181+ let info = Cmd.info "list" ~doc in
182182+ Cmd.v info Term.(const run $ logging_term)
183183+184184+(* Opam repo remove command *)
185185+let opam_repo_remove_cmd =
186186+ let doc = "Remove an opam repository." in
187187+ let name_arg =
188188+ let doc = "Name of the repository to remove." in
189189+ Arg.(required & pos 0 (some string) None & info [] ~docv:"NAME" ~doc)
190190+ in
191191+ let run () name =
192192+ with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
193193+ let config = load_config root in
194194+ if Unpac.Config.find_repo config name = None then begin
195195+ Format.eprintf "Repository '%s' not found@." name;
196196+ exit 1
197197+ end;
198198+ let config' = Unpac.Config.remove_repo config name in
199199+ save_config ~proc_mgr root config' (Printf.sprintf "Remove repository %s" name);
200200+ Format.printf "Removed repository %s@." name
201201+ in
202202+ let info = Cmd.info "remove" ~doc in
203203+ Cmd.v info Term.(const run $ logging_term $ name_arg)
204204+205205+(* Opam repo command group *)
206206+let opam_repo_cmd =
207207+ let doc = "Manage opam repositories." in
208208+ let info = Cmd.info "repo" ~doc in
209209+ Cmd.group info [opam_repo_add_cmd; opam_repo_list_cmd; opam_repo_remove_cmd]
210210+211211+(* Opam config compiler command *)
212212+let opam_config_compiler_cmd =
213213+ let doc = "Set or show the OCaml compiler version for dependency solving." in
214214+ let version_arg =
215215+ let doc = "OCaml version to use (e.g., 5.2.0)." in
216216+ Arg.(value & pos 0 (some string) None & info [] ~docv:"VERSION" ~doc)
217217+ in
218218+ let run () version_opt =
219219+ with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
220220+ let config = load_config root in
221221+ match version_opt with
222222+ | None ->
223223+ (* Show current compiler *)
224224+ (match Unpac.Config.get_compiler config with
225225+ | Some v -> Format.printf "Compiler: %s@." v
226226+ | None -> Format.printf "No compiler configured@.@.Hint: unpac opam config compiler 5.2.0@.")
227227+ | Some version ->
228228+ (* Set compiler *)
229229+ let config' = Unpac.Config.set_compiler config version in
230230+ save_config ~proc_mgr root config' (Printf.sprintf "Set compiler to %s" version);
231231+ Format.printf "Compiler set to %s@." version
232232+ in
233233+ let info = Cmd.info "compiler" ~doc in
234234+ Cmd.v info Term.(const run $ logging_term $ version_arg)
235235+236236+(* Opam config command group *)
237237+let opam_config_cmd =
238238+ let doc = "Configure opam settings." in
239239+ let info = Cmd.info "config" ~doc in
240240+ Cmd.group info [opam_config_compiler_cmd]
241241+242242+(* Helper to add a single package by name *)
243243+let add_single_package ~proc_mgr ~root ?cache ~config ~name ~version_opt ~branch_opt () =
244244+ let repos = config.Unpac.Config.opam.repositories in
245245+ match Unpac_opam.Repo.find_package ~repos ~name ?version:version_opt () with
246246+ | None ->
247247+ Format.eprintf "Package '%s' not found in configured repositories@." name;
248248+ `Failed
249249+ | Some result ->
250250+ match result.metadata.dev_repo with
251251+ | None ->
252252+ Format.eprintf "Package '%s' has no dev-repo field, skipping@." name;
253253+ `Skipped
254254+ | Some dev_repo ->
255255+ (* Strip git+ prefix if present (opam dev-repo format) *)
256256+ let url = if String.starts_with ~prefix:"git+" dev_repo then
257257+ String.sub dev_repo 4 (String.length dev_repo - 4)
258258+ else dev_repo in
259259+ let info : Unpac.Backend.package_info = {
260260+ name;
261261+ url;
262262+ branch = branch_opt;
263263+ } in
264264+ match Unpac_opam.Opam.add_package ~proc_mgr ~root ?cache info with
265265+ | Unpac.Backend.Added { name = pkg_name; sha } ->
266266+ Format.printf "Added %s (%s)@." pkg_name (String.sub sha 0 7);
267267+ `Added
268268+ | Unpac.Backend.Already_exists pkg_name ->
269269+ Format.printf "Package %s already vendored@." pkg_name;
270270+ `Exists
271271+ | Unpac.Backend.Failed { name = pkg_name; error } ->
272272+ Format.eprintf "Error adding %s: %s@." pkg_name error;
273273+ `Failed
274274+275275+(* Opam add command - enhanced to support package names and dependency solving *)
75276let opam_add_cmd =
7676- let doc = "Vendor an opam package from a git URL." in
7777- let url_arg =
7878- let doc = "Git URL of the package repository." in
7979- Arg.(required & pos 0 (some string) None & info [] ~docv:"URL" ~doc)
277277+ let doc = "Vendor an opam package (by name or git URL)." in
278278+ let pkg_arg =
279279+ let doc = "Package name or git URL." in
280280+ Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc)
80281 in
81282 let name_arg =
8282- let doc = "Package name (defaults to repository name)." in
283283+ let doc = "Override package name." in
83284 Arg.(value & opt (some string) None & info ["n"; "name"] ~docv:"NAME" ~doc)
84285 in
286286+ let version_arg =
287287+ let doc = "Package version (when adding by name)." in
288288+ Arg.(value & opt (some string) None & info ["V"; "pkg-version"] ~docv:"VERSION" ~doc)
289289+ in
85290 let branch_arg =
86291 let doc = "Git branch to vendor (defaults to remote default)." in
87292 Arg.(value & opt (some string) None & info ["b"; "branch"] ~docv:"BRANCH" ~doc)
88293 in
8989- let run () url name_opt branch_opt =
9090- with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
9191- let name = match name_opt with
9292- | Some n -> n
9393- | None ->
9494- (* Extract name from URL *)
9595- let base = Filename.basename url in
9696- if String.ends_with ~suffix:".git" base then
9797- String.sub base 0 (String.length base - 4)
9898- else base
9999- in
100100- let info : Unpac.Backend.package_info = {
101101- name;
102102- url;
103103- branch = branch_opt;
104104- } in
105105- match Unpac_opam.Opam.add_package ~proc_mgr ~root info with
106106- | Unpac.Backend.Added { name; sha } ->
107107- Format.printf "Added %s (%s)@." name (String.sub sha 0 7)
108108- | Unpac.Backend.Already_exists name ->
109109- Format.printf "Package %s already vendored@." name
110110- | Unpac.Backend.Failed { name; error } ->
111111- Format.eprintf "Error adding %s: %s@." name error;
294294+ let solve_arg =
295295+ let doc = "Solve dependencies and vendor all required packages." in
296296+ Arg.(value & flag & info ["solve"] ~doc)
297297+ in
298298+ let cache_arg =
299299+ let doc = "Path to vendor cache (overrides config and UNPAC_VENDOR_CACHE env var)." in
300300+ Arg.(value & opt (some string) None & info ["cache"] ~docv:"PATH" ~doc)
301301+ in
302302+ let run () pkg name_opt version_opt branch_opt solve cli_cache =
303303+ with_root @@ fun ~env:_ ~fs ~proc_mgr ~root ->
304304+ let config = load_config root in
305305+ let cache = resolve_cache ~proc_mgr ~fs ~config ~cli_cache in
306306+307307+ if solve then begin
308308+ (* Solve dependencies and add all packages *)
309309+ let repos = config.opam.repositories in
310310+ if repos = [] then begin
311311+ Format.eprintf "No repositories configured. Add one with: unpac opam repo add <name> <path>@.";
112312 exit 1
313313+ end;
314314+ let ocaml_version = match Unpac.Config.get_compiler config with
315315+ | Some v -> v
316316+ | None ->
317317+ Format.eprintf "No compiler version configured.@.";
318318+ Format.eprintf "Set one with: unpac opam config compiler 5.2.0@.";
319319+ exit 1
320320+ in
321321+ (* Get repo paths *)
322322+ let repo_paths = List.map (fun (r : Unpac.Config.repo_config) ->
323323+ match r.source with
324324+ | Unpac.Config.Local p -> p
325325+ | Unpac.Config.Remote u -> u (* TODO: handle remote repos *)
326326+ ) repos in
327327+ Format.printf "Solving dependencies for %s...@." pkg;
328328+ match Unpac_opam.Solver.solve ~repos:repo_paths ~ocaml_version ~packages:[pkg] with
329329+ | Error msg ->
330330+ Format.eprintf "Dependency solving failed:@.%s@." msg;
331331+ exit 1
332332+ | Ok result ->
333333+ let pkgs = result.packages in
334334+ Format.printf "Solution found: %d packages@." (List.length pkgs);
335335+ List.iter (fun p ->
336336+ Format.printf " %s.%s@."
337337+ (OpamPackage.Name.to_string (OpamPackage.name p))
338338+ (OpamPackage.Version.to_string (OpamPackage.version p))
339339+ ) pkgs;
340340+ Format.printf "@.Vendoring packages...@.";
341341+ let added = ref 0 in
342342+ let failed = ref 0 in
343343+ List.iter (fun p ->
344344+ let name = OpamPackage.Name.to_string (OpamPackage.name p) in
345345+ match add_single_package ~proc_mgr ~root ?cache ~config ~name ~version_opt:None ~branch_opt () with
346346+ | `Added -> incr added
347347+ | `Exists -> ()
348348+ | `Skipped -> ()
349349+ | `Failed -> incr failed
350350+ ) pkgs;
351351+ Format.printf "@.Done: %d added, %d failed@." !added !failed;
352352+ if !failed > 0 then exit 1
353353+ end else begin
354354+ (* Single package mode *)
355355+ let url, name =
356356+ if is_url_or_path pkg then begin
357357+ (* It's a URL *)
358358+ let n = match name_opt with
359359+ | Some n -> n
360360+ | None ->
361361+ let base = Filename.basename pkg in
362362+ if String.ends_with ~suffix:".git" base then
363363+ String.sub base 0 (String.length base - 4)
364364+ else base
365365+ in
366366+ (pkg, n)
367367+ end else begin
368368+ (* It's a package name - look up in repositories *)
369369+ let repos = config.opam.repositories in
370370+ if repos = [] then begin
371371+ Format.eprintf "No repositories configured. Add one with: unpac opam repo add <name> <path>@.";
372372+ exit 1
373373+ end;
374374+ match Unpac_opam.Repo.find_package ~repos ~name:pkg ?version:version_opt () with
375375+ | None ->
376376+ Format.eprintf "Package '%s' not found in configured repositories@." pkg;
377377+ exit 1
378378+ | Some result ->
379379+ match result.metadata.dev_repo with
380380+ | None ->
381381+ Format.eprintf "Package '%s' has no dev-repo field@." pkg;
382382+ exit 1
383383+ | Some dev_repo ->
384384+ (* Strip git+ prefix if present (opam dev-repo format) *)
385385+ let url = if String.starts_with ~prefix:"git+" dev_repo then
386386+ String.sub dev_repo 4 (String.length dev_repo - 4)
387387+ else dev_repo in
388388+ let n = match name_opt with Some n -> n | None -> pkg in
389389+ (url, n)
390390+ end
391391+ in
392392+393393+ let info : Unpac.Backend.package_info = {
394394+ name;
395395+ url;
396396+ branch = branch_opt;
397397+ } in
398398+ match Unpac_opam.Opam.add_package ~proc_mgr ~root ?cache info with
399399+ | Unpac.Backend.Added { name = pkg_name; sha } ->
400400+ Format.printf "Added %s (%s)@." pkg_name (String.sub sha 0 7);
401401+ Format.printf "@.Next steps:@.";
402402+ Format.printf " unpac opam edit %s # make local changes@." pkg_name;
403403+ Format.printf " unpac opam merge %s <project> # merge into a project@." pkg_name
404404+ | Unpac.Backend.Already_exists name ->
405405+ Format.printf "Package %s already vendored@." name
406406+ | Unpac.Backend.Failed { name; error } ->
407407+ Format.eprintf "Error adding %s: %s@." name error;
408408+ exit 1
409409+ end
113410 in
114411 let info = Cmd.info "add" ~doc in
115115- Cmd.v info Term.(const run $ logging_term $ url_arg $ name_arg $ branch_arg)
412412+ Cmd.v info Term.(const run $ logging_term $ pkg_arg $ name_arg $ version_arg $ branch_arg $ solve_arg $ cache_arg)
116413117414(* Opam list command *)
118415let opam_list_cmd =
···120417 let run () =
121418 with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
122419 let packages = Unpac_opam.Opam.list_packages ~proc_mgr ~root in
123123- List.iter (Format.printf "%s@.") packages
420420+ if packages = [] then begin
421421+ Format.printf "No packages vendored@.";
422422+ Format.printf "@.Hint: unpac opam add <package>@."
423423+ end else
424424+ List.iter (Format.printf "%s@.") packages
124425 in
125426 let info = Cmd.info "list" ~doc in
126427 Cmd.v info Term.(const run $ logging_term)
127428429429+(* Opam edit command *)
430430+let opam_edit_cmd =
431431+ let doc = "Open a package's patches worktree for editing." in
432432+ let pkg_arg =
433433+ let doc = "Package name to edit." in
434434+ Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc)
435435+ in
436436+ let run () pkg =
437437+ with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
438438+ (* Check package exists *)
439439+ let packages = Unpac_opam.Opam.list_packages ~proc_mgr ~root in
440440+ if not (List.mem pkg packages) then begin
441441+ Format.eprintf "Package '%s' is not vendored@." pkg;
442442+ exit 1
443443+ end;
444444+ (* Ensure patches worktree exists *)
445445+ Unpac.Worktree.ensure ~proc_mgr root (Unpac.Worktree.Opam_patches pkg);
446446+ let wt_path = Unpac.Worktree.path root (Unpac.Worktree.Opam_patches pkg) in
447447+ let path_str = snd wt_path in
448448+ Format.printf "Editing %s@." pkg;
449449+ Format.printf "Worktree: %s@." path_str;
450450+ Format.printf "@.";
451451+ Format.printf "Make your changes, then:@.";
452452+ Format.printf " cd %s@." path_str;
453453+ Format.printf " git add -A && git commit -m 'your message'@.";
454454+ Format.printf "@.";
455455+ Format.printf "When done: unpac opam done %s@." pkg
456456+ in
457457+ let info = Cmd.info "edit" ~doc in
458458+ Cmd.v info Term.(const run $ logging_term $ pkg_arg)
459459+460460+(* Opam done command *)
461461+let opam_done_cmd =
462462+ let doc = "Close a package's patches worktree after editing." in
463463+ let pkg_arg =
464464+ let doc = "Package name." in
465465+ Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc)
466466+ in
467467+ let run () pkg =
468468+ with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
469469+ let kind = Unpac.Worktree.Opam_patches pkg in
470470+ if not (Unpac.Worktree.exists root kind) then begin
471471+ Format.eprintf "No editing session for '%s'@." pkg;
472472+ exit 1
473473+ end;
474474+ (* Check for uncommitted changes *)
475475+ let wt_path = Unpac.Worktree.path root kind in
476476+ let status = Unpac.Git.run_exn ~proc_mgr ~cwd:wt_path ["status"; "--porcelain"] in
477477+ if String.trim status <> "" then begin
478478+ Format.eprintf "Warning: uncommitted changes in %s@." pkg;
479479+ Format.eprintf "Commit or discard them before closing.@.";
480480+ exit 1
481481+ end;
482482+ (* Remove worktree *)
483483+ Unpac.Worktree.remove ~proc_mgr root kind;
484484+ Format.printf "Closed editing session for %s@." pkg;
485485+ Format.printf "@.Next steps:@.";
486486+ Format.printf " unpac opam diff %s # view your changes@." pkg;
487487+ Format.printf " unpac opam merge %s <project> # merge into a project@." pkg
488488+ in
489489+ let info = Cmd.info "done" ~doc in
490490+ Cmd.v info Term.(const run $ logging_term $ pkg_arg)
491491+128492(* Opam update command *)
129493let opam_update_cmd =
130494 let doc = "Update a vendored opam package from upstream." in
···135499 let run () name =
136500 with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
137501 match Unpac_opam.Opam.update_package ~proc_mgr ~root name with
138138- | Unpac.Backend.Updated { name; old_sha; new_sha } ->
139139- Format.printf "Updated %s: %s -> %s@." name
140140- (String.sub old_sha 0 7) (String.sub new_sha 0 7)
502502+ | Unpac.Backend.Updated { name = pkg_name; old_sha; new_sha } ->
503503+ Format.printf "Updated %s: %s -> %s@." pkg_name
504504+ (String.sub old_sha 0 7) (String.sub new_sha 0 7);
505505+ Format.printf "@.Next steps:@.";
506506+ Format.printf " unpac opam diff %s # view changes@." pkg_name;
507507+ Format.printf " unpac opam merge %s <project> # merge into a project@." pkg_name
141508 | Unpac.Backend.No_changes name ->
142509 Format.printf "%s is up to date@." name
143510 | Unpac.Backend.Update_failed { name; error } ->
···163530 let patches_branch = Unpac_opam.Opam.patches_branch pkg in
164531 match Unpac.Backend.merge_to_project ~proc_mgr ~root ~project ~patches_branch with
165532 | Ok () ->
166166- Format.printf "Merged %s into project %s@." pkg project
533533+ Format.printf "Merged %s into project %s@." pkg project;
534534+ Format.printf "@.Next: Build your project in project/%s@." project
167535 | Error (`Conflict files) ->
168536 Format.eprintf "Merge conflict in %s:@." pkg;
169537 List.iter (Format.eprintf " %s@.") files;
···173541 let info = Cmd.info "merge" ~doc in
174542 Cmd.v info Term.(const run $ logging_term $ pkg_arg $ project_arg)
175543544544+(* Opam info command *)
545545+let opam_info_cmd =
546546+ let doc = "Show information about a vendored package." in
547547+ let pkg_arg =
548548+ let doc = "Package name." in
549549+ Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc)
550550+ in
551551+ let run () pkg =
552552+ with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
553553+ let git = Unpac.Worktree.git_dir root in
554554+ (* Check package exists *)
555555+ let packages = Unpac_opam.Opam.list_packages ~proc_mgr ~root in
556556+ if not (List.mem pkg packages) then begin
557557+ Format.eprintf "Package '%s' is not vendored@." pkg;
558558+ exit 1
559559+ end;
560560+ (* Get remote URL *)
561561+ let remote = "origin-" ^ pkg in
562562+ let url = Unpac.Git.remote_url ~proc_mgr ~cwd:git remote in
563563+ Format.printf "Package: %s@." pkg;
564564+ (match url with
565565+ | Some u -> Format.printf "URL: %s@." u
566566+ | None -> ());
567567+ (* Get branch SHAs *)
568568+ let upstream = Unpac_opam.Opam.upstream_branch pkg in
569569+ let vendor = Unpac_opam.Opam.vendor_branch pkg in
570570+ let patches = Unpac_opam.Opam.patches_branch pkg in
571571+ (match Unpac.Git.rev_parse ~proc_mgr ~cwd:git upstream with
572572+ | Some sha -> Format.printf "Upstream: %s@." (String.sub sha 0 7)
573573+ | None -> ());
574574+ (match Unpac.Git.rev_parse ~proc_mgr ~cwd:git vendor with
575575+ | Some sha -> Format.printf "Vendor: %s@." (String.sub sha 0 7)
576576+ | None -> ());
577577+ (match Unpac.Git.rev_parse ~proc_mgr ~cwd:git patches with
578578+ | Some sha -> Format.printf "Patches: %s@." (String.sub sha 0 7)
579579+ | None -> ());
580580+ (* Count commits ahead *)
581581+ let log_output = Unpac.Git.run_exn ~proc_mgr ~cwd:git
582582+ ["log"; "--oneline"; vendor ^ ".." ^ patches] in
583583+ let commits = List.length (String.split_on_char '\n' log_output |>
584584+ List.filter (fun s -> String.trim s <> "")) in
585585+ Format.printf "Local commits: %d@." commits;
586586+ Format.printf "@.Commands:@.";
587587+ Format.printf " unpac opam diff %s # view local changes@." pkg;
588588+ Format.printf " unpac opam edit %s # edit package@." pkg;
589589+ Format.printf " unpac opam update %s # fetch upstream@." pkg
590590+ in
591591+ let info = Cmd.info "info" ~doc in
592592+ Cmd.v info Term.(const run $ logging_term $ pkg_arg)
593593+594594+(* Opam diff command *)
595595+let opam_diff_cmd =
596596+ let doc = "Show diff between vendor and patches branches." in
597597+ let pkg_arg =
598598+ let doc = "Package name." in
599599+ Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc)
600600+ in
601601+ let run () pkg =
602602+ with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
603603+ let git = Unpac.Worktree.git_dir root in
604604+ (* Check package exists *)
605605+ let packages = Unpac_opam.Opam.list_packages ~proc_mgr ~root in
606606+ if not (List.mem pkg packages) then begin
607607+ Format.eprintf "Package '%s' is not vendored@." pkg;
608608+ exit 1
609609+ end;
610610+ let vendor = Unpac_opam.Opam.vendor_branch pkg in
611611+ let patches = Unpac_opam.Opam.patches_branch pkg in
612612+ let diff = Unpac.Git.run_exn ~proc_mgr ~cwd:git
613613+ ["diff"; vendor; patches] in
614614+ if String.trim diff = "" then begin
615615+ Format.printf "No local changes@.";
616616+ Format.printf "@.Hint: unpac opam edit %s # to make changes@." pkg
617617+ end else begin
618618+ print_string diff;
619619+ Format.printf "@.Next: unpac opam merge %s <project>@." pkg
620620+ end
621621+ in
622622+ let info = Cmd.info "diff" ~doc in
623623+ Cmd.v info Term.(const run $ logging_term $ pkg_arg)
624624+625625+(* Opam remove command *)
626626+let opam_remove_cmd =
627627+ let doc = "Remove a vendored package." in
628628+ let pkg_arg =
629629+ let doc = "Package name to remove." in
630630+ Arg.(required & pos 0 (some string) None & info [] ~docv:"PACKAGE" ~doc)
631631+ in
632632+ let run () pkg =
633633+ with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
634634+ let git = Unpac.Worktree.git_dir root in
635635+ (* Check package exists *)
636636+ let packages = Unpac_opam.Opam.list_packages ~proc_mgr ~root in
637637+ if not (List.mem pkg packages) then begin
638638+ Format.eprintf "Package '%s' is not vendored@." pkg;
639639+ exit 1
640640+ end;
641641+ (* Remove worktrees if exist *)
642642+ (try Unpac.Worktree.remove_force ~proc_mgr root (Unpac.Worktree.Opam_upstream pkg) with _ -> ());
643643+ (try Unpac.Worktree.remove_force ~proc_mgr root (Unpac.Worktree.Opam_vendor pkg) with _ -> ());
644644+ (try Unpac.Worktree.remove_force ~proc_mgr root (Unpac.Worktree.Opam_patches pkg) with _ -> ());
645645+ (* Delete branches *)
646646+ let upstream = Unpac_opam.Opam.upstream_branch pkg in
647647+ let vendor = Unpac_opam.Opam.vendor_branch pkg in
648648+ let patches = Unpac_opam.Opam.patches_branch pkg in
649649+ (try Unpac.Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; upstream] |> ignore with _ -> ());
650650+ (try Unpac.Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; vendor] |> ignore with _ -> ());
651651+ (try Unpac.Git.run_exn ~proc_mgr ~cwd:git ["branch"; "-D"; patches] |> ignore with _ -> ());
652652+ (* Remove remote *)
653653+ let remote = "origin-" ^ pkg in
654654+ (try Unpac.Git.run_exn ~proc_mgr ~cwd:git ["remote"; "remove"; remote] |> ignore with _ -> ());
655655+ Format.printf "Removed %s@." pkg;
656656+ Format.printf "@.Hint: unpac opam add <package> # to add another package@."
657657+ in
658658+ let info = Cmd.info "remove" ~doc in
659659+ Cmd.v info Term.(const run $ logging_term $ pkg_arg)
660660+176661(* Opam command group *)
177662let opam_cmd =
178663 let doc = "Opam package vendoring commands." in
179664 let info = Cmd.info "opam" ~doc in
180180- Cmd.group info [opam_add_cmd; opam_list_cmd; opam_update_cmd; opam_merge_cmd]
665665+ Cmd.group info [
666666+ opam_repo_cmd;
667667+ opam_config_cmd;
668668+ opam_add_cmd;
669669+ opam_list_cmd;
670670+ opam_edit_cmd;
671671+ opam_done_cmd;
672672+ opam_update_cmd;
673673+ opam_merge_cmd;
674674+ opam_info_cmd;
675675+ opam_diff_cmd;
676676+ opam_remove_cmd;
677677+ ]
678678+679679+(* Push command - push all unpac branches to a remote *)
680680+let push_cmd =
681681+ let doc = "Push all unpac branches to a remote." in
682682+ let remote_arg =
683683+ let doc = "Remote name (e.g., origin)." in
684684+ Arg.(required & pos 0 (some string) None & info [] ~docv:"REMOTE" ~doc)
685685+ in
686686+ let force_arg =
687687+ let doc = "Force push (use with caution)." in
688688+ Arg.(value & flag & info ["f"; "force"] ~doc)
689689+ in
690690+ let dry_run_arg =
691691+ let doc = "Show what would be pushed without actually pushing." in
692692+ Arg.(value & flag & info ["n"; "dry-run"] ~doc)
693693+ in
694694+ let run () remote force dry_run =
695695+ with_root @@ fun ~env:_ ~fs:_ ~proc_mgr ~root ->
696696+ let git = Unpac.Worktree.git_dir root in
697697+698698+ (* Check if remote exists *)
699699+ (match Unpac.Git.remote_url ~proc_mgr ~cwd:git remote with
700700+ | None ->
701701+ Format.eprintf "Remote '%s' not configured.@." remote;
702702+ Format.eprintf "Add it with: git -C %s remote add %s <url>@." (snd git) remote;
703703+ exit 1
704704+ | Some _ -> ());
705705+706706+ (* Get all branches *)
707707+ let all_branches = Unpac.Git.run_lines ~proc_mgr ~cwd:git ["branch"; "--format=%(refname:short)"] in
708708+709709+ (* Filter to only unpac-managed branches *)
710710+ let unpac_branches = List.filter (fun b ->
711711+ b = "main" ||
712712+ String.starts_with ~prefix:"opam/" b ||
713713+ String.starts_with ~prefix:"project/" b
714714+ ) all_branches in
715715+716716+ if unpac_branches = [] then begin
717717+ Format.printf "No branches to push@.";
718718+ exit 0
719719+ end;
720720+721721+ Format.printf "Branches to push to %s:@." remote;
722722+ List.iter (fun b -> Format.printf " %s@." b) unpac_branches;
723723+ Format.printf "@.";
724724+725725+ if dry_run then begin
726726+ Format.printf "(dry run - no changes made)@."
727727+ end else begin
728728+ (* Build push command *)
729729+ let force_flag = if force then ["--force"] else [] in
730730+ let push_args = ["push"] @ force_flag @ [remote; "--"] @ unpac_branches in
731731+732732+ Format.printf "Pushing %d branches...@." (List.length unpac_branches);
733733+ try
734734+ Unpac.Git.run_exn ~proc_mgr ~cwd:git push_args |> ignore;
735735+ Format.printf "Done.@."
736736+ with e ->
737737+ Format.eprintf "Push failed: %s@." (Printexc.to_string e);
738738+ exit 1
739739+ end
740740+ in
741741+ let info = Cmd.info "push" ~doc in
742742+ Cmd.v info Term.(const run $ logging_term $ remote_arg $ force_arg $ dry_run_arg)
181743182744(* Main command *)
183745let main_cmd =
184746 let doc = "Multi-backend vendoring tool using git worktrees." in
185747 let info = Cmd.info "unpac" ~version:"0.1.0" ~doc in
186186- Cmd.group info [init_cmd; project_cmd; opam_cmd]
748748+ Cmd.group info [init_cmd; project_cmd; opam_cmd; push_cmd]
187749188750let () = exit (Cmd.eval main_cmd)
···11(** Git repository URL lookup and rewriting.
2233 This module handles URL rewriting for git repositories, mapping known
44- slow upstream URLs to faster mirrors. *)
44+ slow upstream URLs to faster mirrors, and branch/tag overrides for
55+ specific packages. *)
5667(** Rewrite a git URL to use a faster mirror if available.
78···3031 in
3132 github_mirror ^ rest
3233 else url
3434+3535+(** Override branch/tag for specific packages.
3636+3737+ Some packages have unstable main branches or we want to pin to specific
3838+ versions. This returns Some ref if an override exists, None otherwise.
3939+4040+ Currently handles:
4141+ - dune: use tag 3.20.2 instead of main branch *)
4242+let branch_override ~name ~url =
4343+ (* Dune's main branch can be unstable; pin to release tag *)
4444+ let is_dune_url =
4545+ String.equal url "https://github.com/ocaml/dune.git" ||
4646+ String.equal url "https://github.com/ocaml/dune" ||
4747+ String.equal url "git://github.com/ocaml/dune.git"
4848+ in
4949+ if name = "dune" || is_dune_url then
5050+ Some "3.20.2"
5151+ else
5252+ None
+7-1
lib/init.ml
···22222323(** Initialize a new unpac project at the given path. *)
2424let init ~proc_mgr ~fs path =
2525- let root = Eio.Path.(fs / path) in
2525+ (* Convert relative paths to absolute *)
2626+ let abs_path =
2727+ if Filename.is_relative path then
2828+ Filename.concat (Sys.getcwd ()) path
2929+ else path
3030+ in
3131+ let root = Eio.Path.(fs / abs_path) in
26322733 (* Create root directory *)
2834 Eio.Path.mkdirs ~exists_ok:false ~perm:0o755 root;
-67
lib/opam/dev_repo.ml
···11-type t = string
22-33-let normalize_url s =
44- let s = String.lowercase_ascii s in
55- (* Remove git+ prefix *)
66- let s =
77- if String.starts_with ~prefix:"git+" s then
88- String.sub s 4 (String.length s - 4)
99- else s
1010- in
1111- (* Remove .git suffix *)
1212- let s =
1313- if String.ends_with ~suffix:".git" s then
1414- String.sub s 0 (String.length s - 4)
1515- else s
1616- in
1717- (* Remove trailing slash *)
1818- let s =
1919- if String.ends_with ~suffix:"/" s then
2020- String.sub s 0 (String.length s - 1)
2121- else s
2222- in
2323- (* Strip #branch fragment *)
2424- let s =
2525- match String.index_opt s '#' with
2626- | Some i -> String.sub s 0 i
2727- | None -> s
2828- in
2929- (* Normalize ssh-style github.com:user/repo to github.com/user/repo *)
3030- let s =
3131- match String.index_opt s ':' with
3232- | Some i when i > 0 ->
3333- let before = String.sub s 0 i in
3434- let after = String.sub s (i + 1) (String.length s - i - 1) in
3535- (* Only convert if it looks like host:path (no // after) *)
3636- if
3737- (not (String.contains before '/'))
3838- && not (String.starts_with ~prefix:"/" after)
3939- && String.contains before '.'
4040- then before ^ "/" ^ after
4141- else s
4242- | _ -> s
4343- in
4444- (* Remove protocol prefix for comparison *)
4545- let s =
4646- let protocols = [ "https://"; "http://"; "ssh://"; "git://"; "file://" ] in
4747- List.fold_left
4848- (fun s proto ->
4949- if String.starts_with ~prefix:proto s then
5050- String.sub s (String.length proto) (String.length s - String.length proto)
5151- else s)
5252- s protocols
5353- in
5454- s
5555-5656-let of_opam_url url = normalize_url (OpamUrl.to_string url)
5757-5858-let of_string s = normalize_url s
5959-6060-let equal = String.equal
6161-let compare = String.compare
6262-let to_string t = t
6363-6464-let pp fmt t = Format.pp_print_string fmt t
6565-6666-module Map = Map.Make (String)
6767-module Set = Set.Make (String)
-47
lib/opam/dev_repo.mli
···11-(** Normalized dev-repo URLs.
22-33- This module provides URL normalization for dev-repo fields to enable
44- matching packages that share the same source repository even when
55- the URLs are written differently.
66-77- Normalization rules:
88- - Strip [.git] suffix
99- - Normalize to lowercase
1010- - Remove [git+] prefix from transport
1111- - Normalize [github.com:user/repo] to [github.com/user/repo]
1212- - Remove trailing slashes
1313- - Strip [#branch] fragment *)
1414-1515-(** {1 Types} *)
1616-1717-type t
1818-(** Normalized dev-repo URL. *)
1919-2020-(** {1 Creation} *)
2121-2222-val of_opam_url : OpamUrl.t -> t
2323-(** [of_opam_url url] creates a normalized dev-repo from an opam URL. *)
2424-2525-val of_string : string -> t
2626-(** [of_string s] parses and normalizes a URL string. *)
2727-2828-(** {1 Comparison} *)
2929-3030-val equal : t -> t -> bool
3131-(** [equal a b] is [true] if [a] and [b] represent the same repository. *)
3232-3333-val compare : t -> t -> int
3434-(** [compare a b] is a total ordering on normalized URLs. *)
3535-3636-(** {1 Conversion} *)
3737-3838-val to_string : t -> string
3939-(** [to_string t] returns the normalized URL string. *)
4040-4141-val pp : Format.formatter -> t -> unit
4242-(** [pp fmt t] pretty-prints the normalized URL. *)
4343-4444-(** {1 Collections} *)
4545-4646-module Map : Map.S with type key = t
4747-module Set : Set.S with type elt = t
···88module Worktree = Unpac.Worktree
99module Git = Unpac.Git
1010module Git_repo_lookup = Unpac.Git_repo_lookup
1111+module Vendor_cache = Unpac.Vendor_cache
1112module Backend = Unpac.Backend
12131314let name = "opam"
···6162 end
6263 )
63646464-let add_package ~proc_mgr ~root (info : Backend.package_info) =
6565+let add_package ~proc_mgr ~root ?cache (info : Backend.package_info) =
6566 let pkg = info.name in
6667 let git = Worktree.git_dir root in
6768···7374 (* Step 1: Create upstream branch and fetch *)
7475 let upstream_wt = Worktree.path root (upstream_kind pkg) in
75767676- (* Add remote for this package *)
7777- let remote = "origin-" ^ pkg in
7777+ (* Rewrite URL for known mirrors *)
7878 let url = Git_repo_lookup.rewrite_url info.url in
7979- ignore (Git.ensure_remote ~proc_mgr ~cwd:git ~name:remote ~url);
80798181- (* Fetch from remote *)
8282- Git.fetch ~proc_mgr ~cwd:git ~remote;
8383-8484- (* Determine the ref to use *)
8080+ (* Determine the ref to use: explicit > override > default *)
8581 let branch = match info.branch with
8682 | Some b -> b
8787- | None -> Git.ls_remote_default_branch ~proc_mgr ~url
8383+ | None ->
8484+ match Git_repo_lookup.branch_override ~name:pkg ~url with
8585+ | Some b -> b
8686+ | None -> Git.ls_remote_default_branch ~proc_mgr ~cwd:git ~url
8887 in
8989- let ref_point = remote ^ "/" ^ branch in
8888+8989+ (* Fetch - either via cache or directly *)
9090+ let ref_point = match cache with
9191+ | Some cache_path ->
9292+ (* Fetch through vendor cache *)
9393+ Vendor_cache.fetch_to_project ~proc_mgr
9494+ ~cache:cache_path ~project_git:git ~url ~branch
9595+ | None ->
9696+ (* Direct fetch *)
9797+ let remote = "origin-" ^ pkg in
9898+ ignore (Git.ensure_remote ~proc_mgr ~cwd:git ~name:remote ~url);
9999+ Git.fetch ~proc_mgr ~cwd:git ~remote;
100100+ remote ^ "/" ^ branch
101101+ in
9010291103 (* Create upstream branch *)
92104 Git.branch_force ~proc_mgr ~cwd:git
···129141 (try Worktree.remove_force ~proc_mgr root (vendor_kind pkg) with _ -> ());
130142 Backend.Failed { name = pkg; error = Printexc.to_string exn }
131143132132-let update_package ~proc_mgr ~root pkg =
144144+let update_package ~proc_mgr ~root ?cache pkg =
133145 let git = Worktree.git_dir root in
134146135147 try
···137149 if not (Worktree.branch_exists ~proc_mgr root (patches_kind pkg)) then
138150 Backend.Update_failed { name = pkg; error = "Package not vendored" }
139151 else begin
140140- (* Get remote URL *)
152152+ (* Get remote URL - check vendor-cache remote first, then origin-<pkg> *)
141153 let remote = "origin-" ^ pkg in
142154 let url = match Git.remote_url ~proc_mgr ~cwd:git remote with
143155 | Some u -> u
144156 | None -> failwith ("Remote not found: " ^ remote)
145157 in
146158147147- (* Fetch latest *)
148148- Git.fetch ~proc_mgr ~cwd:git ~remote;
159159+ (* Fetch latest - either via cache or directly *)
160160+ (match cache with
161161+ | Some cache_path ->
162162+ let branch = Git.ls_remote_default_branch ~proc_mgr ~cwd:git ~url in
163163+ ignore (Vendor_cache.fetch_to_project ~proc_mgr
164164+ ~cache:cache_path ~project_git:git ~url ~branch)
165165+ | None ->
166166+ Git.fetch ~proc_mgr ~cwd:git ~remote);
149167150168 (* Get old SHA *)
151169 let old_sha = match Git.rev_parse ~proc_mgr ~cwd:git (upstream_branch pkg) with
···154172 in
155173156174 (* Determine default branch and update upstream *)
157157- let default_branch = Git.ls_remote_default_branch ~proc_mgr ~url in
175175+ let default_branch = Git.ls_remote_default_branch ~proc_mgr ~cwd:git ~url in
158176 let ref_point = remote ^ "/" ^ default_branch in
159177 Git.branch_force ~proc_mgr ~cwd:git
160178 ~name:(upstream_branch pkg) ~point:ref_point;
+12-6
lib/opam/opam.mli
···3333val add_package :
3434 proc_mgr:Unpac.Git.proc_mgr ->
3535 root:Unpac.Worktree.root ->
3636+ ?cache:Unpac.Vendor_cache.t ->
3637 Unpac.Backend.package_info ->
3738 Unpac.Backend.add_result
3838-(** [add_package ~proc_mgr ~root info] vendors a single package.
3939+(** [add_package ~proc_mgr ~root ?cache info] vendors a single package.
39404040- 1. Fetches upstream into opam/upstream/<pkg>
4141+ 1. Fetches upstream into opam/upstream/<pkg> (via cache if provided)
4142 2. Creates opam/vendor/<pkg> orphan with vendor/opam/<pkg>/ prefix
4242- 3. Creates opam/patches/<pkg> from vendor *)
4343+ 3. Creates opam/patches/<pkg> from vendor
4444+4545+ @param cache Optional vendor cache for shared fetches across projects. *)
43464447val update_package :
4548 proc_mgr:Unpac.Git.proc_mgr ->
4649 root:Unpac.Worktree.root ->
5050+ ?cache:Unpac.Vendor_cache.t ->
4751 string ->
4852 Unpac.Backend.update_result
4949-(** [update_package ~proc_mgr ~root name] updates a package from upstream.
5353+(** [update_package ~proc_mgr ~root ?cache name] updates a package from upstream.
50545151- 1. Fetches latest into opam/upstream/<pkg>
5555+ 1. Fetches latest into opam/upstream/<pkg> (via cache if provided)
5256 2. Updates opam/vendor/<pkg> with new content
53575454- Does NOT rebase patches - call [Backend.rebase_patches] separately. *)
5858+ Does NOT rebase patches - call [Backend.rebase_patches] separately.
5959+6060+ @param cache Optional vendor cache for shared fetches across projects. *)
55615662val list_packages :
5763 proc_mgr:Unpac.Git.proc_mgr ->
+107
lib/opam/opam_file.ml
···11+(** Opam file parsing for extracting package metadata. *)
22+33+type metadata = {
44+ name : string;
55+ version : string;
66+ dev_repo : string option;
77+ synopsis : string option;
88+}
99+1010+let empty_metadata = {
1111+ name = "";
1212+ version = "";
1313+ dev_repo = None;
1414+ synopsis = None;
1515+}
1616+1717+(** Parse an opam file and extract metadata. *)
1818+let parse ~name ~version content =
1919+ try
2020+ let opam = OpamParser.FullPos.string content "<opam>" in
2121+ let items = opam.file_contents in
2222+2323+ let dev_repo = ref None in
2424+ let synopsis = ref None in
2525+2626+ List.iter (fun item ->
2727+ match item.OpamParserTypes.FullPos.pelem with
2828+ | OpamParserTypes.FullPos.Variable (name_pos, value_pos) ->
2929+ let var_name = name_pos.OpamParserTypes.FullPos.pelem in
3030+ (match var_name, value_pos.OpamParserTypes.FullPos.pelem with
3131+ | "dev-repo", OpamParserTypes.FullPos.String s ->
3232+ dev_repo := Some s
3333+ | "synopsis", OpamParserTypes.FullPos.String s ->
3434+ synopsis := Some s
3535+ | _ -> ())
3636+ | _ -> ()
3737+ ) items;
3838+3939+ { name; version; dev_repo = !dev_repo; synopsis = !synopsis }
4040+ with _ ->
4141+ { empty_metadata with name; version }
4242+4343+(** Parse an opam file from a path. *)
4444+let parse_file ~name ~version path =
4545+ let content = In_channel.with_open_text path In_channel.input_all in
4646+ parse ~name ~version content
4747+4848+(** Find a package in an opam repository directory.
4949+ Returns the path to the opam file if found. *)
5050+let find_in_repo ~repo_path ~name ?version () =
5151+ let packages_dir = Filename.concat repo_path "packages" in
5252+ let pkg_dir = Filename.concat packages_dir name in
5353+5454+ if not (Sys.file_exists pkg_dir && Sys.is_directory pkg_dir) then
5555+ None
5656+ else
5757+ (* List version directories *)
5858+ let entries = Sys.readdir pkg_dir |> Array.to_list in
5959+ let version_dirs = List.filter (fun entry ->
6060+ let full = Filename.concat pkg_dir entry in
6161+ Sys.is_directory full && String.starts_with ~prefix:(name ^ ".") entry
6262+ ) entries in
6363+6464+ match version with
6565+ | Some v ->
6666+ (* Look for specific version *)
6767+ let target = name ^ "." ^ v in
6868+ if List.mem target version_dirs then
6969+ let opam_path = Filename.concat (Filename.concat pkg_dir target) "opam" in
7070+ if Sys.file_exists opam_path then Some (opam_path, v)
7171+ else None
7272+ else None
7373+ | None ->
7474+ (* Find latest version (simple string sort, works for semver) *)
7575+ let sorted = List.sort (fun a b -> String.compare b a) version_dirs in
7676+ match sorted with
7777+ | [] -> None
7878+ | latest :: _ ->
7979+ let v = String.sub latest (String.length name + 1)
8080+ (String.length latest - String.length name - 1) in
8181+ let opam_path = Filename.concat (Filename.concat pkg_dir latest) "opam" in
8282+ if Sys.file_exists opam_path then Some (opam_path, v)
8383+ else None
8484+8585+(** Get metadata for a package from an opam repository. *)
8686+let get_metadata ~repo_path ~name ?version () =
8787+ match find_in_repo ~repo_path ~name ?version () with
8888+ | None -> None
8989+ | Some (opam_path, v) ->
9090+ Some (parse_file ~name ~version:v opam_path)
9191+9292+(** List all versions of a package in a repository. *)
9393+let list_versions ~repo_path ~name =
9494+ let packages_dir = Filename.concat repo_path "packages" in
9595+ let pkg_dir = Filename.concat packages_dir name in
9696+9797+ if not (Sys.file_exists pkg_dir && Sys.is_directory pkg_dir) then
9898+ []
9999+ else
100100+ Sys.readdir pkg_dir
101101+ |> Array.to_list
102102+ |> List.filter_map (fun entry ->
103103+ if String.starts_with ~prefix:(name ^ ".") entry then
104104+ Some (String.sub entry (String.length name + 1)
105105+ (String.length entry - String.length name - 1))
106106+ else None)
107107+ |> List.sort String.compare
+24
lib/opam/opam_file.mli
···11+(** Opam file parsing for extracting package metadata. *)
22+33+type metadata = {
44+ name : string;
55+ version : string;
66+ dev_repo : string option;
77+ synopsis : string option;
88+}
99+1010+val parse : name:string -> version:string -> string -> metadata
1111+(** [parse ~name ~version content] parses opam file content. *)
1212+1313+val parse_file : name:string -> version:string -> string -> metadata
1414+(** [parse_file ~name ~version path] parses an opam file from disk. *)
1515+1616+val find_in_repo : repo_path:string -> name:string -> ?version:string -> unit -> (string * string) option
1717+(** [find_in_repo ~repo_path ~name ?version ()] finds a package in an opam repository.
1818+ Returns [Some (opam_file_path, version)] if found. *)
1919+2020+val get_metadata : repo_path:string -> name:string -> ?version:string -> unit -> metadata option
2121+(** [get_metadata ~repo_path ~name ?version ()] gets package metadata from a repository. *)
2222+2323+val list_versions : repo_path:string -> name:string -> string list
2424+(** [list_versions ~repo_path ~name] lists all available versions of a package. *)
+71
lib/opam/repo.ml
···11+(** Opam repository operations. *)
22+33+type repo = {
44+ name : string;
55+ path : string;
66+}
77+88+type search_result = {
99+ repo : repo;
1010+ metadata : Opam_file.metadata;
1111+}
1212+1313+(** Resolve repository path from config. *)
1414+let resolve_repo (cfg : Unpac.Config.repo_config) : repo option =
1515+ match cfg.source with
1616+ | Unpac.Config.Local path ->
1717+ if Sys.file_exists path && Sys.is_directory path then
1818+ Some { name = cfg.repo_name; path }
1919+ else None
2020+ | Unpac.Config.Remote _url ->
2121+ (* Remote repos not yet supported *)
2222+ None
2323+2424+(** Search for a package in configured repositories. *)
2525+let find_package ~repos ~name ?version () : search_result option =
2626+ let rec search = function
2727+ | [] -> None
2828+ | cfg :: rest ->
2929+ match resolve_repo cfg with
3030+ | None -> search rest
3131+ | Some repo ->
3232+ match Opam_file.get_metadata ~repo_path:repo.path ~name ?version () with
3333+ | None -> search rest
3434+ | Some metadata -> Some { repo; metadata }
3535+ in
3636+ search repos
3737+3838+(** List all versions of a package across repositories. *)
3939+let list_versions ~repos ~name : (repo * string list) list =
4040+ List.filter_map (fun cfg ->
4141+ match resolve_repo cfg with
4242+ | None -> None
4343+ | Some repo ->
4444+ let versions = Opam_file.list_versions ~repo_path:repo.path ~name in
4545+ if versions = [] then None
4646+ else Some (repo, versions)
4747+ ) repos
4848+4949+(** Search for packages matching a pattern. *)
5050+let search_packages ~repos ~pattern : (repo * string) list =
5151+ List.concat_map (fun cfg ->
5252+ match resolve_repo cfg with
5353+ | None -> []
5454+ | Some repo ->
5555+ let packages_dir = Filename.concat repo.path "packages" in
5656+ if not (Sys.file_exists packages_dir) then []
5757+ else
5858+ Sys.readdir packages_dir
5959+ |> Array.to_list
6060+ |> List.filter (fun name ->
6161+ (* Simple substring match *)
6262+ let pattern_lower = String.lowercase_ascii pattern in
6363+ let name_lower = String.lowercase_ascii name in
6464+ String.length pattern_lower <= String.length name_lower &&
6565+ (let rec check i =
6666+ if i > String.length name_lower - String.length pattern_lower then false
6767+ else if String.sub name_lower i (String.length pattern_lower) = pattern_lower then true
6868+ else check (i + 1)
6969+ in check 0))
7070+ |> List.map (fun name -> (repo, name))
7171+ ) repos
+32
lib/opam/repo.mli
···11+(** Opam repository operations. *)
22+33+type repo = {
44+ name : string;
55+ path : string;
66+}
77+88+type search_result = {
99+ repo : repo;
1010+ metadata : Opam_file.metadata;
1111+}
1212+1313+val find_package :
1414+ repos:Unpac.Config.repo_config list ->
1515+ name:string ->
1616+ ?version:string ->
1717+ unit ->
1818+ search_result option
1919+(** [find_package ~repos ~name ?version ()] searches for a package in repositories.
2020+ Returns the first match found. *)
2121+2222+val list_versions :
2323+ repos:Unpac.Config.repo_config list ->
2424+ name:string ->
2525+ (repo * string list) list
2626+(** [list_versions ~repos ~name] lists all versions across repositories. *)
2727+2828+val search_packages :
2929+ repos:Unpac.Config.repo_config list ->
3030+ pattern:string ->
3131+ (repo * string) list
3232+(** [search_packages ~repos ~pattern] searches for packages matching a pattern. *)
-120
lib/opam/repo_index.ml
···11-type package_info = {
22- name : OpamPackage.Name.t;
33- version : OpamPackage.Version.t;
44- opam : OpamFile.OPAM.t;
55- dev_repo : Dev_repo.t option;
66- source_repo : string;
77-}
88-99-type t = {
1010- packages : package_info OpamPackage.Map.t;
1111- by_name : OpamPackage.Set.t OpamPackage.Name.Map.t;
1212- by_dev_repo : OpamPackage.Set.t Dev_repo.Map.t;
1313- repos : string list;
1414-}
1515-1616-let empty =
1717- {
1818- packages = OpamPackage.Map.empty;
1919- by_name = OpamPackage.Name.Map.empty;
2020- by_dev_repo = Dev_repo.Map.empty;
2121- repos = [];
2222- }
2323-2424-let add_package nv info t =
2525- let packages = OpamPackage.Map.add nv info t.packages in
2626- let by_name =
2727- let name = OpamPackage.name nv in
2828- let existing =
2929- match OpamPackage.Name.Map.find_opt name t.by_name with
3030- | Some s -> s
3131- | None -> OpamPackage.Set.empty
3232- in
3333- OpamPackage.Name.Map.add name (OpamPackage.Set.add nv existing) t.by_name
3434- in
3535- let by_dev_repo =
3636- match info.dev_repo with
3737- | Some dev_repo ->
3838- let existing =
3939- match Dev_repo.Map.find_opt dev_repo t.by_dev_repo with
4040- | Some s -> s
4141- | None -> OpamPackage.Set.empty
4242- in
4343- Dev_repo.Map.add dev_repo (OpamPackage.Set.add nv existing) t.by_dev_repo
4444- | None -> t.by_dev_repo
4545- in
4646- { t with packages; by_name; by_dev_repo }
4747-4848-let load_local_repo ~name ~path t =
4949- let repo_root = OpamFilename.Dir.of_string path in
5050- let pkg_prefixes = OpamRepository.packages_with_prefixes repo_root in
5151- let t =
5252- if List.mem name t.repos then t else { t with repos = name :: t.repos }
5353- in
5454- OpamPackage.Map.fold
5555- (fun nv prefix acc ->
5656- let opam_file = OpamRepositoryPath.opam repo_root prefix nv in
5757- match OpamFile.OPAM.read_opt opam_file with
5858- | Some opam ->
5959- let dev_repo =
6060- OpamFile.OPAM.dev_repo opam |> Option.map Dev_repo.of_opam_url
6161- in
6262- let info =
6363- {
6464- name = OpamPackage.name nv;
6565- version = OpamPackage.version nv;
6666- opam;
6767- dev_repo;
6868- source_repo = name;
6969- }
7070- in
7171- add_package nv info acc
7272- | None -> acc)
7373- pkg_prefixes t
7474-7575-let all_packages t =
7676- OpamPackage.Map.fold (fun _ info acc -> info :: acc) t.packages []
7777-7878-let find_package name t =
7979- match OpamPackage.Name.Map.find_opt name t.by_name with
8080- | None -> []
8181- | Some nvs ->
8282- OpamPackage.Set.fold
8383- (fun nv acc ->
8484- match OpamPackage.Map.find_opt nv t.packages with
8585- | Some info -> info :: acc
8686- | None -> acc)
8787- nvs []
8888-8989-let find_package_version name version t =
9090- let nv = OpamPackage.create name version in
9191- OpamPackage.Map.find_opt nv t.packages
9292-9393-let packages_by_dev_repo dev_repo t =
9494- match Dev_repo.Map.find_opt dev_repo t.by_dev_repo with
9595- | None -> []
9696- | Some nvs ->
9797- OpamPackage.Set.fold
9898- (fun nv acc ->
9999- match OpamPackage.Map.find_opt nv t.packages with
100100- | Some info -> info :: acc
101101- | None -> acc)
102102- nvs []
103103-104104-let related_packages name t =
105105- let versions = find_package name t in
106106- let dev_repos =
107107- List.filter_map (fun info -> info.dev_repo) versions
108108- |> List.sort_uniq Dev_repo.compare
109109- in
110110- List.concat_map (fun dr -> packages_by_dev_repo dr t) dev_repos
111111- |> List.sort_uniq (fun a b ->
112112- let cmp = OpamPackage.Name.compare a.name b.name in
113113- if cmp <> 0 then cmp
114114- else OpamPackage.Version.compare a.version b.version)
115115-116116-let package_names t =
117117- OpamPackage.Name.Map.fold (fun name _ acc -> name :: acc) t.by_name []
118118-119119-let package_count t = OpamPackage.Map.cardinal t.packages
120120-let repo_count t = List.length t.repos
-59
lib/opam/repo_index.mli
···11-(** Repository index for opam packages.
22-33- This module provides functionality to load and query packages from
44- multiple opam repositories, with support for merging with configurable
55- priority. *)
66-77-(** {1 Types} *)
88-99-type package_info = {
1010- name : OpamPackage.Name.t;
1111- version : OpamPackage.Version.t;
1212- opam : OpamFile.OPAM.t;
1313- dev_repo : Dev_repo.t option;
1414- source_repo : string; (** Name of the repository this package came from *)
1515-}
1616-(** Information about a single package version. *)
1717-1818-type t
1919-(** The repository index containing all loaded packages. *)
2020-2121-(** {1 Creation} *)
2222-2323-val empty : t
2424-(** [empty] is an empty repository index. *)
2525-2626-val load_local_repo : name:string -> path:string -> t -> t
2727-(** [load_local_repo ~name ~path index] loads all packages from the local
2828- opam repository at [path] and adds them to [index]. Packages from this
2929- load will take priority over existing packages with the same name/version. *)
3030-3131-(** {1 Queries} *)
3232-3333-val all_packages : t -> package_info list
3434-(** [all_packages t] returns all packages in the index. *)
3535-3636-val find_package : OpamPackage.Name.t -> t -> package_info list
3737-(** [find_package name t] returns all versions of package [name]. *)
3838-3939-val find_package_version :
4040- OpamPackage.Name.t -> OpamPackage.Version.t -> t -> package_info option
4141-(** [find_package_version name version t] returns the specific package version. *)
4242-4343-val packages_by_dev_repo : Dev_repo.t -> t -> package_info list
4444-(** [packages_by_dev_repo dev_repo t] returns all packages with the given dev-repo. *)
4545-4646-val related_packages : OpamPackage.Name.t -> t -> package_info list
4747-(** [related_packages name t] returns all packages that share a dev-repo with
4848- any version of package [name]. *)
4949-5050-val package_names : t -> OpamPackage.Name.t list
5151-(** [package_names t] returns all unique package names. *)
5252-5353-(** {1 Statistics} *)
5454-5555-val package_count : t -> int
5656-(** [package_count t] returns the total number of package versions. *)
5757-5858-val repo_count : t -> int
5959-(** [repo_count t] returns the number of source repositories loaded. *)
+149-411
lib/opam/solver.ml
···11-open Cmdliner
22-33-type version_constraint = OpamFormula.relop * OpamPackage.Version.t
44-55-type package_spec = {
66- name : OpamPackage.Name.t;
77- constraint_ : version_constraint option;
88-}
99-1010-(* Target platform configuration *)
1111-type platform = {
1212- os : string;
1313- os_family : string;
1414- os_distribution : string;
1515- arch : string;
1616-}
1717-1818-let debian_x86_64 = {
1919- os = "linux";
2020- os_family = "debian";
2121- os_distribution = "debian";
2222- arch = "x86_64";
2323-}
2424-2525-(* Create a filter environment for the target platform *)
2626-let make_filter_env platform : OpamFilter.env =
2727- fun var ->
2828- let open OpamVariable in
2929- let s = to_string (Full.variable var) in
3030- match s with
3131- | "os" -> Some (S platform.os)
3232- | "os-family" -> Some (S platform.os_family)
3333- | "os-distribution" -> Some (S platform.os_distribution)
3434- | "arch" -> Some (S platform.arch)
3535- | "opam-version" -> Some (S "2.1.0")
3636- | "make" -> Some (S "make")
3737- | "jobs" -> Some (S "4")
3838- | "pinned" -> Some (B false)
3939- | "build" -> Some (B true)
4040- | "post" -> Some (B false)
4141- | "dev" -> Some (B false)
4242- | "with-test" -> Some (B false)
4343- | "with-doc" -> Some (B false)
4444- | "with-dev-setup" -> Some (B false)
4545- | _ -> None
4646-4747-(* Check if a package is available on the target platform *)
4848-let is_available_on_platform env (opam : OpamFile.OPAM.t) : bool =
4949- let available = OpamFile.OPAM.available opam in
5050- OpamFilter.opt_eval_to_bool env (Some available)
5151-5252-(* Check if a package has the compiler flag or is a compiler-related package *)
5353-let is_compiler_package (opam : OpamFile.OPAM.t) (name : OpamPackage.Name.t) : bool =
5454- let name_s = OpamPackage.Name.to_string name in
5555- (* Check for flags:compiler *)
5656- let has_compiler_flag =
5757- List.mem OpamTypes.Pkgflag_Compiler (OpamFile.OPAM.flags opam)
5858- in
5959- (* Also filter out known compiler-related packages by name pattern *)
6060- let is_compiler_name =
6161- name_s = "ocaml" ||
6262- String.starts_with ~prefix:"ocaml-base-compiler" name_s ||
6363- String.starts_with ~prefix:"ocaml-variants" name_s ||
6464- String.starts_with ~prefix:"ocaml-system" name_s ||
6565- String.starts_with ~prefix:"ocaml-option-" name_s ||
6666- String.starts_with ~prefix:"ocaml-config" name_s ||
6767- String.starts_with ~prefix:"ocaml-compiler" name_s ||
6868- String.starts_with ~prefix:"base-" name_s || (* base-threads, base-unix, etc. *)
6969- String.starts_with ~prefix:"dkml-base-compiler" name_s ||
7070- String.starts_with ~prefix:"dkml-runtime" name_s
7171- in
7272- has_compiler_flag || is_compiler_name
7373-7474-(* Filter dependencies to remove platform-filtered ones.
7575- Uses OpamFilter.filter_formula which evaluates filters and simplifies. *)
7676-let filter_depends env (formula : OpamTypes.filtered_formula) : OpamTypes.formula =
7777- OpamFilter.filter_formula ~default:false env formula
7878-7979-(* Parse version constraint from string like ">=1.0.0" *)
8080-let parse_constraint s =
8181- let s = String.trim s in
8282- if String.length s = 0 then None
8383- else
8484- let try_parse prefix relop =
8585- if String.starts_with ~prefix s then
8686- let v = String.sub s (String.length prefix) (String.length s - String.length prefix) in
8787- Some (relop, OpamPackage.Version.of_string v)
8888- else None
8989- in
9090- match try_parse ">=" `Geq with
9191- | Some c -> Some c
9292- | None -> (
9393- match try_parse "<=" `Leq with
9494- | Some c -> Some c
9595- | None -> (
9696- match try_parse ">" `Gt with
9797- | Some c -> Some c
9898- | None -> (
9999- match try_parse "<" `Lt with
100100- | Some c -> Some c
101101- | None -> (
102102- match try_parse "!=" `Neq with
103103- | Some c -> Some c
104104- | None -> (
105105- match try_parse "=" `Eq with
106106- | Some c -> Some c
107107- | None ->
108108- (* Treat bare version as exact match *)
109109- Some (`Eq, OpamPackage.Version.of_string s))))))
110110-111111-let parse_package_spec s =
112112- try
113113- let s = String.trim s in
114114- (* Check for constraint operators *)
115115- let has_constraint =
116116- String.contains s '>' || String.contains s '<'
117117- || String.contains s '=' || String.contains s '!'
118118- in
119119- if has_constraint then
120120- (* Find where constraint starts *)
121121- let constraint_start =
122122- let find_op c = try Some (String.index s c) with Not_found -> None in
123123- [ find_op '>'; find_op '<'; find_op '='; find_op '!' ]
124124- |> List.filter_map Fun.id
125125- |> List.fold_left min (String.length s)
126126- in
127127- let name_part = String.sub s 0 constraint_start in
128128- let constraint_part =
129129- String.sub s constraint_start (String.length s - constraint_start)
130130- in
131131- let name = OpamPackage.Name.of_string name_part in
132132- let constraint_ = parse_constraint constraint_part in
133133- Ok { name; constraint_ }
134134- else
135135- (* Check for pkg.version format *)
136136- match String.rindex_opt s '.' with
137137- | Some i when i > 0 ->
138138- let name_part = String.sub s 0 i in
139139- let version_part = String.sub s (i + 1) (String.length s - i - 1) in
140140- (* Validate that version_part looks like a version *)
141141- if String.length version_part > 0
142142- && (version_part.[0] >= '0' && version_part.[0] <= '9' || version_part.[0] = 'v')
143143- then
144144- let name = OpamPackage.Name.of_string name_part in
145145- let version = OpamPackage.Version.of_string version_part in
146146- Ok { name; constraint_ = Some (`Eq, version) }
147147- else
148148- (* Treat as package name without constraint *)
149149- let name = OpamPackage.Name.of_string s in
150150- Ok { name; constraint_ = None }
151151- | _ ->
152152- let name = OpamPackage.Name.of_string s in
153153- Ok { name; constraint_ = None }
154154- with e -> Error (Printexc.to_string e)
155155-156156-let package_spec_to_string spec =
157157- let name = OpamPackage.Name.to_string spec.name in
158158- match spec.constraint_ with
159159- | None -> name
160160- | Some (op, v) ->
161161- let op_s =
162162- match op with
163163- | `Eq -> "="
164164- | `Neq -> "!="
165165- | `Geq -> ">="
166166- | `Gt -> ">"
167167- | `Leq -> "<="
168168- | `Lt -> "<"
169169- in
170170- name ^ op_s ^ OpamPackage.Version.to_string v
171171-172172-(* Parse compiler spec string like "ocaml.5.4.0" or "5.4.0" *)
173173-let parse_compiler_spec (s : string) : package_spec option =
174174- let s = String.trim s in
175175- if s = "" then None
176176- else
177177- (* Handle formats: "ocaml.5.4.0", "5.4.0", "ocaml>=5.0" *)
178178- let spec_str =
179179- if String.starts_with ~prefix:"ocaml" s then s
180180- else if s.[0] >= '0' && s.[0] <= '9' then "ocaml." ^ s
181181- else s
182182- in
183183- match parse_package_spec spec_str with
184184- | Ok spec -> Some spec
185185- | Error _ -> None
186186-187187-(* Selection results *)
188188-type selection_result = { packages : Repo_index.package_info list }
189189-190190-(* Get latest version of each package that is available on the platform *)
191191-let latest_versions ?(platform=debian_x86_64) (index : Repo_index.t) : Repo_index.package_info list =
192192- let env = make_filter_env platform in
193193- let names = Repo_index.package_names index in
194194- List.filter_map
195195- (fun name ->
196196- let versions = Repo_index.find_package name index in
197197- (* Filter by availability and sort by version descending *)
198198- let available_versions =
199199- List.filter (fun (info : Repo_index.package_info) ->
200200- is_available_on_platform env info.opam) versions
201201- in
202202- match
203203- List.sort
204204- (fun (a : Repo_index.package_info) b ->
205205- OpamPackage.Version.compare b.version a.version)
206206- available_versions
207207- with
208208- | latest :: _ -> Some latest
209209- | [] -> None)
210210- names
211211-212212-let select_all index = { packages = latest_versions index }
213213-214214-(* Check if a package version satisfies a constraint *)
215215-let satisfies_constraint version = function
216216- | None -> true
217217- | Some (op, cv) -> (
218218- let cmp = OpamPackage.Version.compare version cv in
219219- match op with
220220- | `Eq -> cmp = 0
221221- | `Neq -> cmp <> 0
222222- | `Geq -> cmp >= 0
223223- | `Gt -> cmp > 0
224224- | `Leq -> cmp <= 0
225225- | `Lt -> cmp < 0)
11+(** Dependency solver using 0install algorithm. *)
2262227227-let select_packages ?(platform=debian_x86_64) index specs =
228228- if specs = [] then Ok (select_all index)
229229- else
230230- let env = make_filter_env platform in
231231- let selected =
232232- List.filter_map
233233- (fun spec ->
234234- let versions = Repo_index.find_package spec.name index in
235235- (* Filter by constraint, availability, and get latest matching *)
236236- let matching =
237237- List.filter
238238- (fun (info : Repo_index.package_info) ->
239239- satisfies_constraint info.version spec.constraint_
240240- && is_available_on_platform env info.opam)
241241- versions
242242- in
243243- match
244244- List.sort
245245- (fun (a : Repo_index.package_info) b ->
246246- OpamPackage.Version.compare b.version a.version)
247247- matching
248248- with
249249- | latest :: _ -> Some latest
250250- | [] -> None)
251251- specs
252252- in
253253- Ok { packages = selected }
33+let ( / ) = Filename.concat
2544255255-(* Build a version map for CUDF conversion *)
256256-let build_version_map (packages : Repo_index.package_info list) : int OpamPackage.Map.t =
257257- (* Group by name and sort versions *)
258258- let by_name = Hashtbl.create 256 in
259259- List.iter (fun (info : Repo_index.package_info) ->
260260- let name = info.name in
261261- let versions = try Hashtbl.find by_name name with Not_found -> [] in
262262- Hashtbl.replace by_name name (info.version :: versions))
263263- packages;
264264- (* Assign version numbers *)
265265- let version_map = ref OpamPackage.Map.empty in
266266- Hashtbl.iter (fun name versions ->
267267- let sorted = List.sort OpamPackage.Version.compare versions in
268268- List.iteri (fun i v ->
269269- let nv = OpamPackage.create name v in
270270- version_map := OpamPackage.Map.add nv (i + 1) !version_map)
271271- sorted)
272272- by_name;
273273- !version_map
55+(** List directory entries, returns empty list if directory doesn't exist. *)
66+let list_dir path =
77+ try Sys.readdir path |> Array.to_list
88+ with Sys_error _ -> []
2749275275-(* Convert opam formula to CUDF vpkgformula (list of disjunctions for AND semantics)
276276- Simplified: we ignore version constraints and just require the package exists.
277277- This allows the 0install solver to pick the best version. *)
278278-let formula_to_vpkgformula available_names (formula : OpamTypes.formula) : Cudf_types.vpkgformula =
279279- let atoms = OpamFormula.atoms formula in
280280- List.filter_map (fun (name, _version_constraint) ->
281281- let name_s = OpamPackage.Name.to_string name in
282282- (* Only include dependency if package exists in our available set *)
283283- if not (Hashtbl.mem available_names name_s) then None
284284- else Some [(name_s, None)]) (* No version constraint - solver picks best *)
285285- atoms
1010+(** Known compiler packages to filter out. *)
1111+let is_compiler_package name =
1212+ let s = OpamPackage.Name.to_string name in
1313+ String.starts_with ~prefix:"ocaml-base-compiler" s ||
1414+ String.starts_with ~prefix:"ocaml-variants" s ||
1515+ String.starts_with ~prefix:"ocaml-system" s ||
1616+ String.starts_with ~prefix:"ocaml-config" s ||
1717+ s = "ocaml" ||
1818+ s = "base-unix" ||
1919+ s = "base-threads" ||
2020+ s = "base-bigarray" ||
2121+ s = "base-domains" ||
2222+ s = "base-nnp"
28623287287-(* For conflicts, we ignore them in CUDF since proper conflict handling requires
288288- complex version mapping. The 0install solver will still produce valid results
289289- since we filter packages by platform availability. *)
290290-let formula_to_vpkglist (_formula : OpamTypes.formula) : Cudf_types.vpkglist =
291291- [] (* Ignore conflicts for simplicity *)
2424+(** Check if a package has the compiler flag. *)
2525+let has_compiler_flag opam =
2626+ let flags = OpamFile.OPAM.flags opam in
2727+ List.mem OpamTypes.Pkgflag_Compiler flags
29228293293-(* Build CUDF universe from packages *)
294294-let build_cudf_universe ?(platform=debian_x86_64) (packages : Repo_index.package_info list) =
295295- let env = make_filter_env platform in
296296- let version_map = build_version_map packages in
2929+(** Multi-repo context that searches multiple opam repository directories. *)
3030+module Multi_context : sig
3131+ include Opam_0install.S.CONTEXT
29732298298- (* First, collect all available package names *)
299299- let available_names = Hashtbl.create 256 in
300300- List.iter (fun (info : Repo_index.package_info) ->
301301- Hashtbl.replace available_names (OpamPackage.Name.to_string info.name) ())
302302- packages;
3333+ val create :
3434+ ?constraints:OpamFormula.version_constraint OpamTypes.name_map ->
3535+ repos:string list ->
3636+ ocaml_version:string ->
3737+ unit -> t
3838+end = struct
3939+ type rejection =
4040+ | UserConstraint of OpamFormula.atom
4141+ | Unavailable
4242+ | CompilerPackage
30343304304- let cudf_packages = List.filter_map (fun (info : Repo_index.package_info) ->
305305- let nv = OpamPackage.create info.name info.version in
306306- match OpamPackage.Map.find_opt nv version_map with
307307- | None -> None
308308- | Some cudf_version ->
309309- (* Get and filter dependencies *)
310310- let depends_formula = OpamFile.OPAM.depends info.opam in
311311- let filtered_depends = filter_depends env depends_formula in
312312- let depends = formula_to_vpkgformula available_names filtered_depends in
4444+ let pp_rejection f = function
4545+ | UserConstraint x -> Fmt.pf f "Rejected by user-specified constraint %s" (OpamFormula.string_of_atom x)
4646+ | Unavailable -> Fmt.pf f "Availability condition not satisfied"
4747+ | CompilerPackage -> Fmt.pf f "Compiler package (filtered out)"
31348314314- (* Get conflicts - simplified to empty for now *)
315315- let conflicts_formula = OpamFile.OPAM.conflicts info.opam in
316316- let filtered_conflicts = filter_depends env conflicts_formula in
317317- let conflicts = formula_to_vpkglist filtered_conflicts in
4949+ type t = {
5050+ repos : string list; (* List of packages/ directories *)
5151+ constraints : OpamFormula.version_constraint OpamTypes.name_map;
5252+ ocaml_version : string;
5353+ }
31854319319- Some {
320320- Cudf.default_package with
321321- package = OpamPackage.Name.to_string info.name;
322322- version = cudf_version;
323323- depends = depends;
324324- conflicts = conflicts;
325325- installed = false;
326326- pkg_extra = [
327327- (OpamCudf.s_source, `String (OpamPackage.Name.to_string info.name));
328328- (OpamCudf.s_source_number, `String (OpamPackage.Version.to_string info.version));
329329- ];
330330- })
331331- packages
332332- in
5555+ let env t _pkg v =
5656+ match OpamVariable.Full.to_string v with
5757+ | "arch" -> Some (OpamTypes.S "x86_64")
5858+ | "os" -> Some (OpamTypes.S "linux")
5959+ | "os-distribution" -> Some (OpamTypes.S "debian")
6060+ | "os-version" -> Some (OpamTypes.S "12")
6161+ | "os-family" -> Some (OpamTypes.S "debian")
6262+ | "opam-version" -> Some (OpamTypes.S "2.2.0")
6363+ | "sys-ocaml-version" -> Some (OpamTypes.S t.ocaml_version)
6464+ | "ocaml:native" -> Some (OpamTypes.B true)
6565+ | _ -> None
33366334334- let universe = Cudf.load_universe cudf_packages in
335335- (universe, version_map)
6767+ let filter_deps t pkg f =
6868+ f
6969+ |> OpamFilter.partial_filter_formula (env t pkg)
7070+ |> OpamFilter.filter_deps ~build:true ~post:true ~test:false ~doc:false ~dev:false ~dev_setup:false ~default:false
33671337337-(* Resolve dependencies using 0install solver *)
338338-let resolve_deps ?(platform=debian_x86_64) ?compiler index (root_specs : package_spec list) =
339339- let env = make_filter_env platform in
7272+ let user_restrictions t name =
7373+ OpamPackage.Name.Map.find_opt name t.constraints
34074341341- (* Get all available packages *)
342342- let all_packages =
343343- List.filter (fun (info : Repo_index.package_info) ->
344344- is_available_on_platform env info.opam)
345345- (Repo_index.all_packages index)
346346- in
7575+ (** Load opam file from path. *)
7676+ let load_opam path =
7777+ try Some (OpamFile.OPAM.read (OpamFile.make (OpamFilename.raw path)))
7878+ with _ -> None
34779348348- (* Build CUDF universe *)
349349- let universe, version_map = build_cudf_universe ~platform all_packages in
8080+ (** Create a minimal virtual opam file for base packages. *)
8181+ let virtual_opam () =
8282+ OpamFile.OPAM.empty
35083351351- (* Build request - add compiler if specified *)
352352- let all_specs = match compiler with
353353- | Some compiler_spec -> compiler_spec :: root_specs
354354- | None -> root_specs
355355- in
8484+ (** Find all versions of a package across all repos. *)
8585+ let find_versions t name =
8686+ let name_str = OpamPackage.Name.to_string name in
8787+ (* Collect versions from all repos, first repo wins for duplicates *)
8888+ let seen = Hashtbl.create 16 in
8989+ List.iter (fun packages_dir ->
9090+ let pkg_dir = packages_dir / name_str in
9191+ list_dir pkg_dir |> List.iter (fun entry ->
9292+ match OpamPackage.of_string_opt entry with
9393+ | Some pkg when OpamPackage.name pkg = name ->
9494+ let v = OpamPackage.version pkg in
9595+ if not (Hashtbl.mem seen v) then begin
9696+ let opam_path = pkg_dir / entry / "opam" in
9797+ Hashtbl.add seen v opam_path
9898+ end
9999+ | _ -> ()
100100+ )
101101+ ) t.repos;
102102+ Hashtbl.fold (fun v path acc -> (v, path) :: acc) seen []
356103357357- let requested = List.filter_map (fun spec ->
358358- let name_s = OpamPackage.Name.to_string spec.name in
359359- (* Check if package exists in universe *)
360360- if Cudf.mem_package universe (name_s, 1) ||
361361- List.exists (fun p -> p.Cudf.package = name_s) (Cudf.get_packages universe)
362362- then Some (name_s, `Essential)
363363- else begin
364364- Format.eprintf "Warning: Package %s not found in universe@." name_s;
365365- None
366366- end)
367367- all_specs
368368- in
104104+ let candidates t name =
105105+ let name_str = OpamPackage.Name.to_string name in
106106+ (* Provide virtual packages for compiler/base packages at the configured version *)
107107+ if name_str = "ocaml" then
108108+ let v = OpamPackage.Version.of_string t.ocaml_version in
109109+ [v, Ok (virtual_opam ())]
110110+ else if name_str = "base-unix" || name_str = "base-threads" ||
111111+ name_str = "base-bigarray" || name_str = "base-domains" ||
112112+ name_str = "base-nnp" then
113113+ let v = OpamPackage.Version.of_string "base" in
114114+ [v, Ok (virtual_opam ())]
115115+ else if is_compiler_package name then
116116+ (* Other compiler packages - not available *)
117117+ []
118118+ else
119119+ let user_constraints = user_restrictions t name in
120120+ find_versions t name
121121+ |> List.sort (fun (v1, _) (v2, _) -> OpamPackage.Version.compare v2 v1) (* Prefer newest *)
122122+ |> List.map (fun (v, opam_path) ->
123123+ match user_constraints with
124124+ | Some test when not (OpamFormula.check_version_formula (OpamFormula.Atom test) v) ->
125125+ v, Error (UserConstraint (name, Some test))
126126+ | _ ->
127127+ match load_opam opam_path with
128128+ | None -> v, Error Unavailable
129129+ | Some opam ->
130130+ (* Check flags:compiler *)
131131+ if has_compiler_flag opam then
132132+ v, Error CompilerPackage
133133+ else
134134+ (* Check available filter *)
135135+ let pkg = OpamPackage.create name v in
136136+ let available = OpamFile.OPAM.available opam in
137137+ match OpamFilter.eval ~default:(OpamTypes.B false) (env t pkg) available with
138138+ | B true -> v, Ok opam
139139+ | _ -> v, Error Unavailable
140140+ )
369141370370- if requested = [] then
371371- Error "No valid packages to resolve"
372372- else
373373- (* Create solver and solve *)
374374- let solver = Opam_0install_cudf.create ~constraints:[] universe in
375375- match Opam_0install_cudf.solve solver requested with
376376- | Error diag ->
377377- Error (Opam_0install_cudf.diagnostics diag)
378378- | Ok selections ->
379379- (* Convert results back to package info *)
380380- let selected_cudf = Opam_0install_cudf.packages_of_result selections in
381381- let selected_packages = List.filter_map (fun (name, cudf_version) ->
382382- (* Find the opam package *)
383383- let opam_name = OpamPackage.Name.of_string name in
384384- let versions = Repo_index.find_package opam_name index in
385385- (* Get the version that matches *)
386386- List.find_opt (fun (info : Repo_index.package_info) ->
387387- let nv = OpamPackage.create info.name info.version in
388388- match OpamPackage.Map.find_opt nv version_map with
389389- | Some v -> v = cudf_version
390390- | None -> false)
391391- versions)
392392- selected_cudf
393393- in
394394- (* Deduplicate by package name+version *)
395395- let seen = Hashtbl.create 64 in
396396- let unique_packages = List.filter (fun (info : Repo_index.package_info) ->
397397- let key = OpamPackage.to_string (OpamPackage.create info.name info.version) in
398398- if Hashtbl.mem seen key then false
399399- else begin Hashtbl.add seen key (); true end)
400400- selected_packages
401401- in
402402- (* Filter out compiler packages from results *)
403403- let non_compiler_packages = List.filter (fun (info : Repo_index.package_info) ->
404404- not (is_compiler_package info.opam info.name))
405405- unique_packages
406406- in
407407- Ok { packages = non_compiler_packages }
142142+ let create ?(constraints=OpamPackage.Name.Map.empty) ~repos ~ocaml_version () =
143143+ (* Convert repo roots to packages/ directories *)
144144+ let packages_dirs = List.map (fun r -> r / "packages") repos in
145145+ { repos = packages_dirs; constraints; ocaml_version }
146146+end
408147409409-let select_with_deps ?(platform=debian_x86_64) ?compiler index specs =
410410- if specs = [] then Ok (select_all index)
411411- else
412412- resolve_deps ~platform ?compiler index specs
148148+module Solver = Opam_0install.Solver.Make(Multi_context)
413149414414-(* Cmdliner integration *)
150150+type solve_result = {
151151+ packages : OpamPackage.t list;
152152+}
415153416416-let package_specs_conv : package_spec Arg.conv =
417417- let parse s =
418418- match parse_package_spec s with
419419- | Ok spec -> Ok spec
420420- | Error msg -> Error (`Msg msg)
421421- in
422422- let print fmt spec = Format.pp_print_string fmt (package_spec_to_string spec) in
423423- Arg.conv (parse, print)
154154+type solve_error = string
424155425425-let package_specs_term : package_spec list Term.t =
426426- let doc =
427427- "Package specification. Can be a package name (any version), \
428428- name.version (exact version), or name>=version (constraint). \
429429- Examples: cmdliner, lwt.5.6.0, dune>=3.0"
430430- in
431431- Arg.(value & pos_all package_specs_conv [] & info [] ~docv:"PACKAGE" ~doc)
156156+(** Solve dependencies for a list of package names. *)
157157+let solve ~repos ~ocaml_version ~packages : (solve_result, solve_error) result =
158158+ let context = Multi_context.create ~repos ~ocaml_version () in
159159+ let names = List.map OpamPackage.Name.of_string packages in
160160+ match Solver.solve context names with
161161+ | Ok selections ->
162162+ let pkgs = Solver.packages_of_result selections in
163163+ (* Filter out compiler packages from result *)
164164+ let pkgs = List.filter (fun pkg ->
165165+ not (is_compiler_package (OpamPackage.name pkg))
166166+ ) pkgs in
167167+ Ok { packages = pkgs }
168168+ | Error diagnostics ->
169169+ Error (Solver.diagnostics diagnostics)
+24-79
lib/opam/solver.mli
···11-(** Package selection with constraint solving.
11+(** Dependency solver using 0install algorithm.
2233- Uses the 0install solver (via opam-0install-cudf) to select
44- a consistent set of packages based on constraints, filtered
55- for Debian x86_64 platform. *)
33+ Solves package dependencies across multiple configured opam repositories,
44+ filtering out compiler packages and respecting availability constraints. *)
6577-(** {1 Platform Configuration} *)
88-99-type platform = {
1010- os : string;
1111- os_family : string;
1212- os_distribution : string;
1313- arch : string;
66+type solve_result = {
77+ packages : OpamPackage.t list;
88+ (** List of packages that need to be installed, including transitive deps. *)
149}
1515-(** Target platform for filtering packages. *)
16101717-val debian_x86_64 : platform
1818-(** Default platform: Debian Linux on x86_64. *)
1111+type solve_error = string
1212+(** Human-readable error message explaining why solving failed. *)
19132020-(** {1 Package Specifications} *)
1414+val solve :
1515+ repos:string list ->
1616+ ocaml_version:string ->
1717+ packages:string list ->
1818+ (solve_result, solve_error) result
1919+(** [solve ~repos ~ocaml_version ~packages] solves dependencies for [packages].
21202222-type version_constraint = OpamFormula.relop * OpamPackage.Version.t
2323-(** A version constraint like [>=, 1.0.0]. *)
2121+ @param repos List of opam repository root directories (containing packages/)
2222+ @param ocaml_version The OCaml compiler version to solve for (e.g. "5.2.0")
2323+ @param packages List of package names to solve for
24242525-type package_spec = {
2626- name : OpamPackage.Name.t;
2727- constraint_ : version_constraint option;
2828-}
2929-(** A package specification with optional version constraint. *)
2525+ Returns the full list of packages (including transitive dependencies) that
2626+ need to be installed, or an error message if solving failed.
30273131-val parse_package_spec : string -> (package_spec, string) result
3232-(** [parse_package_spec s] parses a package spec string like:
3333- - "pkg" (any version)
3434- - "pkg.1.0.0" (exact version)
3535- - "pkg>=1.0.0" (version constraint)
3636- - "pkg<2.0" (version constraint) *)
2828+ Compiler packages (ocaml-base-compiler, base-*, etc.) are automatically
2929+ filtered out since they are assumed to be pre-installed. *)
37303838-val package_spec_to_string : package_spec -> string
3939-(** [package_spec_to_string spec] converts a spec back to string form. *)
4040-4141-val parse_compiler_spec : string -> package_spec option
4242-(** [parse_compiler_spec s] parses a compiler version string like:
4343- - "5.4.0" (parsed as ocaml.5.4.0)
4444- - "ocaml.5.4.0" (exact version)
4545- - "ocaml>=5.0" (version constraint)
4646- Returns None if the string is empty or invalid. *)
4747-4848-(** {1 Selection} *)
4949-5050-type selection_result = {
5151- packages : Repo_index.package_info list;
5252- (** Selected packages that satisfy all constraints. *)
5353-}
5454-(** Result of package selection. *)
5555-5656-val select_all : Repo_index.t -> selection_result
5757-(** [select_all index] returns all packages (latest version of each)
5858- that are available on the target platform (Debian x86_64). *)
5959-6060-val select_packages :
6161- ?platform:platform ->
6262- Repo_index.t -> package_spec list -> (selection_result, string) result
6363-(** [select_packages index specs] finds packages matching the given
6464- specifications, filtered by platform availability. Returns the
6565- latest compatible version of each package. *)
6666-6767-val select_with_deps :
6868- ?platform:platform ->
6969- ?compiler:package_spec ->
7070- Repo_index.t -> package_spec list -> (selection_result, string) result
7171-(** [select_with_deps ?platform ?compiler index specs] selects packages and
7272- their transitive dependencies using the 0install solver.
7373-7474- - Dependencies are filtered by platform (Debian x86_64 by default)
7575- - If [compiler] is specified, it is added as a constraint and all
7676- compiler-related packages (those with flags:compiler or matching
7777- known compiler package patterns) are filtered from the results
7878- - The solver finds a consistent installation set *)
7979-8080-(** {1 Cmdliner Integration} *)
8181-8282-val package_specs_term : package_spec list Cmdliner.Term.t
8383-(** Cmdliner term for parsing package specifications from command line.
8484- Accepts zero or more package specs as positional arguments.
8585- If no packages specified, returns empty list (meaning "all packages"). *)
8686-8787-val package_specs_conv : package_spec Cmdliner.Arg.conv
8888-(** Cmdliner converter for a single package spec. *)
3131+val is_compiler_package : OpamPackage.Name.t -> bool
3232+(** [is_compiler_package name] returns true if [name] is a known compiler
3333+ or base package that should be filtered out. *)
-237
lib/opam/source.ml
···11-type source_kind = Archive | Git
22-33-type archive_source = {
44- url : string;
55- checksums : string list;
66- mirrors : string list;
77-}
88-99-type git_source = {
1010- url : string;
1111- branch : string option;
1212-}
1313-1414-type source = ArchiveSource of archive_source | GitSource of git_source | NoSource
1515-1616-type package_source = {
1717- name : string;
1818- version : string;
1919- source : source;
2020- dev_repo : Dev_repo.t option;
2121-}
2222-2323-type grouped_sources = {
2424- dev_repo : Dev_repo.t option;
2525- packages : package_source list;
2626-}
2727-2828-(* Helper to check if URL is git-like *)
2929-let is_git_url url =
3030- let s = OpamUrl.to_string url in
3131- String.starts_with ~prefix:"git" s
3232- || String.ends_with ~suffix:".git" s
3333- || url.OpamUrl.backend = `git
3434-3535-(* Extract archive source from opam URL.t *)
3636-let extract_archive_from_url (url_t : OpamFile.URL.t) : archive_source =
3737- let main_url = OpamFile.URL.url url_t in
3838- let checksums =
3939- OpamFile.URL.checksum url_t
4040- |> List.map OpamHash.to_string
4141- in
4242- let mirrors =
4343- OpamFile.URL.mirrors url_t
4444- |> List.map OpamUrl.to_string
4545- in
4646- { url = OpamUrl.to_string main_url; checksums; mirrors }
4747-4848-(* Extract git source from OpamUrl.t *)
4949-let normalize_git_url s =
5050- (* Strip git+ prefix so URLs work directly with git clone *)
5151- if String.starts_with ~prefix:"git+" s then
5252- String.sub s 4 (String.length s - 4)
5353- else s
5454-5555-let extract_git_from_url (url : OpamUrl.t) : git_source =
5656- { url = normalize_git_url (OpamUrl.to_string url); branch = url.OpamUrl.hash }
5757-5858-let extract kind (info : Repo_index.package_info) : package_source =
5959- let name = OpamPackage.Name.to_string info.name in
6060- let version = OpamPackage.Version.to_string info.version in
6161- let dev_repo = info.dev_repo in
6262- let source =
6363- match kind with
6464- | Archive -> (
6565- match OpamFile.OPAM.url info.opam with
6666- | Some url_t -> ArchiveSource (extract_archive_from_url url_t)
6767- | None -> NoSource)
6868- | Git -> (
6969- (* Prefer dev-repo for git, fall back to url if it's a git URL *)
7070- match OpamFile.OPAM.dev_repo info.opam with
7171- | Some url -> GitSource (extract_git_from_url url)
7272- | None -> (
7373- match OpamFile.OPAM.url info.opam with
7474- | Some url_t ->
7575- let main_url = OpamFile.URL.url url_t in
7676- if is_git_url main_url then
7777- GitSource (extract_git_from_url main_url)
7878- else NoSource
7979- | None -> NoSource))
8080- in
8181- { name; version; source; dev_repo }
8282-8383-let extract_all kind packages = List.map (extract kind) packages
8484-8585-let group_by_dev_repo (sources : package_source list) : grouped_sources list =
8686- (* Build separate lists: one map for packages with dev_repo, one list for those without *)
8787- let with_repo, without_repo =
8888- List.partition (fun (src : package_source) -> Option.is_some src.dev_repo) sources
8989- in
9090- (* Group packages with dev_repo *)
9191- let add_to_map map (src : package_source) =
9292- match src.dev_repo with
9393- | Some dr ->
9494- let existing =
9595- match Dev_repo.Map.find_opt dr map with
9696- | Some l -> l
9797- | None -> []
9898- in
9999- Dev_repo.Map.add dr (src :: existing) map
100100- | None -> map
101101- in
102102- let map = List.fold_left add_to_map Dev_repo.Map.empty with_repo in
103103- (* Convert map to list of grouped_sources *)
104104- let grouped_with_repo =
105105- Dev_repo.Map.fold
106106- (fun dr pkgs acc ->
107107- let packages = List.rev pkgs in (* Preserve original order *)
108108- { dev_repo = Some dr; packages } :: acc)
109109- map []
110110- |> List.sort (fun a b ->
111111- match (a.dev_repo, b.dev_repo) with
112112- | Some a, Some b -> Dev_repo.compare a b
113113- | _ -> 0)
114114- in
115115- (* Add packages without dev_repo at the end *)
116116- if without_repo = [] then grouped_with_repo
117117- else grouped_with_repo @ [{ dev_repo = None; packages = without_repo }]
118118-119119-(* JSON Codecs - simplified with tagged object *)
120120-121121-let source_jsont : source Jsont.t =
122122- let open Jsont in
123123- Object.map ~kind:"source"
124124- (fun source_type url checksums mirrors branch ->
125125- match source_type with
126126- | "archive" ->
127127- let checksums = match checksums with Some cs -> cs | None -> [] in
128128- ArchiveSource { url; checksums; mirrors }
129129- | "git" -> GitSource { url; branch }
130130- | _ -> NoSource)
131131- |> Object.mem "type" string ~enc:(function
132132- | ArchiveSource _ -> "archive"
133133- | GitSource _ -> "git"
134134- | NoSource -> "none")
135135- |> Object.mem "url" string ~dec_absent:"" ~enc:(function
136136- | ArchiveSource a -> a.url
137137- | GitSource g -> g.url
138138- | NoSource -> "")
139139- |> Object.opt_mem "checksums" (list string) ~enc:(function
140140- | ArchiveSource a -> Some a.checksums
141141- | _ -> None)
142142- |> Object.mem "mirrors" (list string) ~dec_absent:[] ~enc:(function
143143- | ArchiveSource a -> a.mirrors
144144- | _ -> [])
145145- |> Object.opt_mem "branch" string ~enc:(function
146146- | GitSource g -> g.branch
147147- | _ -> None)
148148- |> Object.finish
149149-150150-let dev_repo_jsont =
151151- Jsont.(
152152- map
153153- ~dec:(fun s -> Dev_repo.of_string s)
154154- ~enc:Dev_repo.to_string string)
155155-156156-let package_source_jsont : package_source Jsont.t =
157157- let open Jsont in
158158- Object.map ~kind:"package_source"
159159- (fun name version source dev_repo ->
160160- ({ name; version; source; dev_repo } : package_source))
161161- |> Object.mem "name" string ~enc:(fun (p : package_source) -> p.name)
162162- |> Object.mem "version" string ~enc:(fun (p : package_source) -> p.version)
163163- |> Object.mem "source" source_jsont ~enc:(fun (p : package_source) -> p.source)
164164- |> Object.opt_mem "dev_repo" dev_repo_jsont ~enc:(fun (p : package_source) -> p.dev_repo)
165165- |> Object.finish
166166-167167-let package_sources_jsont = Jsont.list package_source_jsont
168168-169169-let grouped_sources_jsont : grouped_sources Jsont.t =
170170- let open Jsont in
171171- Object.map ~kind:"grouped_sources"
172172- (fun dev_repo packages -> ({ dev_repo; packages } : grouped_sources))
173173- |> Object.opt_mem "dev_repo" dev_repo_jsont ~enc:(fun (g : grouped_sources) -> g.dev_repo)
174174- |> Object.mem "packages" (list package_source_jsont) ~enc:(fun (g : grouped_sources) -> g.packages)
175175- |> Object.finish
176176-177177-let grouped_sources_list_jsont = Jsont.list grouped_sources_jsont
178178-179179-(* TOML Codecs *)
180180-181181-let source_tomlt : source Tomlt.t =
182182- let open Tomlt in
183183- let open Table in
184184- obj (fun source_type url checksums mirrors branch ->
185185- match source_type with
186186- | "archive" ->
187187- let checksums = match checksums with Some cs -> cs | None -> [] in
188188- ArchiveSource { url; checksums; mirrors }
189189- | "git" -> GitSource { url; branch }
190190- | "none" | _ -> NoSource)
191191- |> mem "type" string ~enc:(function
192192- | ArchiveSource _ -> "archive"
193193- | GitSource _ -> "git"
194194- | NoSource -> "none")
195195- |> mem "url" string ~dec_absent:"" ~enc:(function
196196- | ArchiveSource a -> a.url
197197- | GitSource g -> g.url
198198- | NoSource -> "")
199199- |> opt_mem "checksums" (list string) ~enc:(function
200200- | ArchiveSource a -> Some a.checksums
201201- | _ -> None)
202202- |> mem "mirrors" (list string) ~dec_absent:[] ~enc:(function
203203- | ArchiveSource a -> a.mirrors
204204- | _ -> [])
205205- |> opt_mem "branch" string ~enc:(function
206206- | GitSource g -> g.branch
207207- | _ -> None)
208208- |> finish
209209-210210-let dev_repo_tomlt =
211211- Tomlt.(
212212- map
213213- ~dec:(fun s -> Dev_repo.of_string s)
214214- ~enc:Dev_repo.to_string string)
215215-216216-let package_source_tomlt : package_source Tomlt.t =
217217- let open Tomlt in
218218- let open Table in
219219- obj (fun name version source dev_repo ->
220220- ({ name; version; source; dev_repo } : package_source))
221221- |> mem "name" string ~enc:(fun (p : package_source) -> p.name)
222222- |> mem "version" string ~enc:(fun (p : package_source) -> p.version)
223223- |> mem "source" source_tomlt ~enc:(fun (p : package_source) -> p.source)
224224- |> opt_mem "dev_repo" dev_repo_tomlt ~enc:(fun (p : package_source) -> p.dev_repo)
225225- |> finish
226226-227227-let package_sources_tomlt = Tomlt.array_of_tables package_source_tomlt
228228-229229-let grouped_sources_tomlt : grouped_sources Tomlt.t =
230230- let open Tomlt in
231231- let open Table in
232232- obj (fun dev_repo packages -> ({ dev_repo; packages } : grouped_sources))
233233- |> opt_mem "dev_repo" dev_repo_tomlt ~enc:(fun (g : grouped_sources) -> g.dev_repo)
234234- |> mem "packages" (array_of_tables package_source_tomlt) ~enc:(fun (g : grouped_sources) -> g.packages)
235235- |> finish
236236-237237-let grouped_sources_list_tomlt = Tomlt.array_of_tables grouped_sources_tomlt
-85
lib/opam/source.mli
···11-(** Package source URL extraction.
22-33- Extracts download URLs or git remotes from opam package metadata. *)
44-55-(** {1 Source Types} *)
66-77-type source_kind =
88- | Archive (** Tarball/archive URL with optional checksums *)
99- | Git (** Git repository URL *)
1010-(** The kind of source to extract. *)
1111-1212-type archive_source = {
1313- url : string;
1414- checksums : string list; (** SHA256, MD5, etc. *)
1515- mirrors : string list;
1616-}
1717-(** An archive source with URL and integrity info. *)
1818-1919-type git_source = {
2020- url : string;
2121- branch : string option; (** Branch/tag/ref if specified *)
2222-}
2323-(** A git repository source. *)
2424-2525-type source =
2626- | ArchiveSource of archive_source
2727- | GitSource of git_source
2828- | NoSource
2929-(** A package source. *)
3030-3131-type package_source = {
3232- name : string;
3333- version : string;
3434- source : source;
3535- dev_repo : Dev_repo.t option;
3636-}
3737-(** A package with its source and dev-repo for grouping. *)
3838-3939-type grouped_sources = {
4040- dev_repo : Dev_repo.t option;
4141- packages : package_source list;
4242-}
4343-(** Packages grouped by their shared dev-repo. *)
4444-4545-(** {1 Extraction} *)
4646-4747-val extract : source_kind -> Repo_index.package_info -> package_source
4848-(** [extract kind info] extracts the source of the specified kind from
4949- package [info]. For [Archive], uses the url field. For [Git], uses
5050- dev-repo or falls back to url if it's a git URL. *)
5151-5252-val extract_all : source_kind -> Repo_index.package_info list -> package_source list
5353-(** [extract_all kind packages] extracts sources for all packages. *)
5454-5555-val group_by_dev_repo : package_source list -> grouped_sources list
5656-(** [group_by_dev_repo sources] groups packages by their dev-repo.
5757- Packages with the same dev-repo are grouped together since they
5858- come from the same repository. Groups with dev-repo are sorted first,
5959- followed by packages without dev-repo. *)
6060-6161-(** {1 Codecs} *)
6262-6363-val package_source_jsont : package_source Jsont.t
6464-(** JSON codec for a package source. *)
6565-6666-val package_sources_jsont : package_source list Jsont.t
6767-(** JSON codec for a list of package sources. *)
6868-6969-val grouped_sources_jsont : grouped_sources Jsont.t
7070-(** JSON codec for grouped sources. *)
7171-7272-val grouped_sources_list_jsont : grouped_sources list Jsont.t
7373-(** JSON codec for a list of grouped sources. *)
7474-7575-val package_source_tomlt : package_source Tomlt.t
7676-(** TOML codec for a package source. *)
7777-7878-val package_sources_tomlt : package_source list Tomlt.t
7979-(** TOML codec for a list of package sources (as array of tables). *)
8080-8181-val grouped_sources_tomlt : grouped_sources Tomlt.t
8282-(** TOML codec for grouped sources. *)
8383-8484-val grouped_sources_list_tomlt : grouped_sources list Tomlt.t
8585-(** TOML codec for a list of grouped sources (as array of tables). *)