Monorepo management for opam overlays
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

monopam: workspace auto-detection by walking up from CWD

Before this change a developer with two product monorepos on one
machine had to swap $HOME (or rewrite ~/.config/monopam/opamverse.toml)
between them, because Config.load only looked at the XDG path. That
made the single-developer-multi-app workflow effectively unusable.

Config.load now walks up from CWD looking for an opamverse.toml at
each parent directory, and uses the first one it finds. The XDG
path stays as a fallback for users who only have one workspace and
prefer the global config. No env variable, no flag, no per-app
swap.

Marker file: opamverse.toml at the workspace root. The same file
the user already authors with [workspace] / [identity] sections —
just placed inside the workspace directory instead of $HOME.

Test: monopam/test/multi_app.t walks through two app workspaces
plus a shared upstream library, asserts that monopam status reads
the right workspace whether the user is at the workspace root, in
mono/, or in a deeply nested subdirectory under mono/.

+146 -2
+34 -2
lib/config.ml
··· 256 256 | Io_error { path; msg } -> 257 257 Fmt.pf ppf "Error reading config at %a: %s" Fpath.pp path msg 258 258 259 - let load ~fs () = 260 - let path = file () in 259 + (** Walk up from [start] looking for an [opamverse.toml] file. Returns the first 260 + one found, or [None] when we hit the filesystem root. Used so a developer 261 + with multiple monopam workspaces on one machine can [cd] into any of them 262 + and have the tool pick up THAT workspace's config without needing to swap 263 + [$HOME] or any global state. *) 264 + let find_workspace_config ~fs start = 265 + let exists path = 266 + let eio_path = Eio.Path.(fs / Fpath.to_string path) in 267 + match Eio.Path.kind ~follow:true eio_path with 268 + | `Regular_file -> true 269 + | _ -> false 270 + | exception _ -> false 271 + in 272 + let rec walk dir = 273 + let candidate = Fpath.(dir / "opamverse.toml") in 274 + if exists candidate then Some candidate 275 + else 276 + let parent = Fpath.parent dir in 277 + if Fpath.equal parent dir then None else walk parent 278 + in 279 + walk (Fpath.normalize start) 280 + 281 + let load_path ~fs path = 261 282 let path_str = Fpath.to_string path in 262 283 let eio_path = Eio.Path.(fs / path_str) in 263 284 match Eio.Path.kind ~follow:true eio_path with ··· 267 288 | exn -> Error (Io_error { path; msg = Printexc.to_string exn })) 268 289 | _ -> Error (Not_found path) 269 290 | exception _ -> Error (Not_found path) 291 + 292 + let load ~fs () = 293 + (* First, walk up from CWD looking for a per-workspace 294 + opamverse.toml. This is the multi-workspace path: each app has 295 + its own config and its own root, with no need to swap HOME or 296 + XDG. If nothing is found we fall back to the XDG global config 297 + so single-workspace users keep working as before. *) 298 + let cwd = Fpath.v (Sys.getcwd ()) in 299 + match find_workspace_config ~fs cwd with 300 + | Some path -> load_path ~fs path 301 + | None -> load_path ~fs (file ()) 270 302 271 303 let save ~fs t = 272 304 let dir = dir () in
+112
test/multi_app.t/run.t
··· 1 + monopam: multiple workspaces on one machine 2 + ============================================= 3 + 4 + A single developer hosts two product apps on one machine. Each app 5 + is its own monopam workspace with its own mono/, opam-repo/, and 6 + sources.toml. Both apps consume libraries from a shared global 7 + library monorepo. 8 + 9 + The papercut this test pins down: when the developer cd's into a 10 + specific app and runs `monopam status`, the tool must operate on 11 + THAT app's workspace, not on whichever was registered last in 12 + $HOME/.config/monopam/opamverse.toml. 13 + 14 + Mechanism: monopam walks up from CWD looking for a workspace 15 + marker. The marker is `sources.toml` at the workspace root (or any 16 + of the standard monopam dirs: `mono/`, `opam-repo/`). The XDG 17 + config path stays as a fallback for the case where no workspace is 18 + detected. 19 + 20 + Setup 21 + ----- 22 + 23 + $ export NO_COLOR=1 24 + $ export GIT_AUTHOR_NAME="Alice" 25 + $ export GIT_AUTHOR_EMAIL="alice@example.com" 26 + $ export GIT_AUTHOR_DATE="2025-01-01T00:00:00+00:00" 27 + $ export GIT_COMMITTER_NAME="Alice" 28 + $ export GIT_COMMITTER_EMAIL="alice@example.com" 29 + $ export GIT_COMMITTER_DATE="2025-01-01T00:00:00+00:00" 30 + $ export HOME="$PWD/home" 31 + $ mkdir -p "$HOME" 32 + $ export GIT_CONFIG_GLOBAL="$HOME/.gitconfig" 33 + $ printf '[init]\n\tdefaultBranch = main\n[user]\n\tname = Alice\n\temail = alice@example.com\n' > "$GIT_CONFIG_GLOBAL" 34 + $ TROOT=$(pwd) 35 + 36 + A shared library upstream: 37 + 38 + $ git init -q --bare lib.git 39 + $ git clone -q lib.git lib-work 2>/dev/null 40 + $ cd lib-work 41 + $ cat > lib.opam << OPAM 42 + > opam-version: "2.0" 43 + > name: "lib" 44 + > version: "dev" 45 + > synopsis: "Shared library" 46 + > dev-repo: "git+file://$TROOT/lib.git" 47 + > OPAM 48 + $ mkdir -p src && echo "let v () = ()" > src/main.ml 49 + $ git add . && git commit -q -m "initial lib" 50 + $ git push -q origin main 2>/dev/null 51 + $ cd "$TROOT" 52 + 53 + Two product apps, each with its own opam-repo overlay registering lib: 54 + 55 + $ for app in app-a app-b; do 56 + > mkdir -p "$app/opam-repo/packages/lib/lib.dev" 57 + > cp lib-work/lib.opam "$app/opam-repo/packages/lib/lib.dev/opam" 58 + > cd "$app/opam-repo" && git init -q && git add . && git commit -q -m "init" && cd "$TROOT" 59 + > mkdir -p "$app/mono" && cd "$app/mono" && git init -q && git commit -q --allow-empty -m "init" && cd "$TROOT" 60 + > done 61 + 62 + App A's workspace config: 63 + 64 + $ mkdir -p "$HOME/.config/monopam" 65 + $ cat > "$TROOT/app-a/opamverse.toml" << EOF 66 + > [workspace] 67 + > root = "$TROOT/app-a" 68 + > [identity] 69 + > handle = "alice.example.org" 70 + > knot = "git.example.org" 71 + > EOF 72 + 73 + App B's workspace config: 74 + 75 + $ cat > "$TROOT/app-b/opamverse.toml" << EOF 76 + > [workspace] 77 + > root = "$TROOT/app-b" 78 + > [identity] 79 + > handle = "alice.example.org" 80 + > knot = "git.example.org" 81 + > EOF 82 + 83 + Crucially, the global XDG config at $HOME/.config/monopam/opamverse.toml 84 + is NEVER created. monopam must find each app's config by walking up 85 + from CWD. 86 + 87 + Stage 1: cd into app-a, status reads app-a's workspace 88 + -------------------------------------------------------- 89 + 90 + $ cd "$TROOT/app-a/mono" 91 + $ monopam status 2>&1 | grep -E "Packages:" | head -1 92 + Packages: 1 total, 0 synced 93 + 94 + The app-b workspace is untouched: 95 + 96 + $ test -d "$TROOT/app-b/src" && echo "app-b checkouts present" || echo "app-b checkouts absent" 97 + app-b checkouts absent 98 + 99 + Stage 2: cd into app-b, status reads app-b's workspace 100 + -------------------------------------------------------- 101 + 102 + $ cd "$TROOT/app-b/mono" 103 + $ monopam status 2>&1 | grep -E "Packages:" | head -1 104 + Packages: 1 total, 0 synced 105 + 106 + Stage 3: walking up from a deeper subdirectory still finds the right workspace 107 + ------------------------------------------------------------------------------- 108 + 109 + $ mkdir -p "$TROOT/app-a/mono/lib/deep/nested" 110 + $ cd "$TROOT/app-a/mono/lib/deep/nested" 111 + $ monopam status 2>&1 | grep -E "Packages:" | head -1 112 + Packages: 1 total, 0 synced