okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
74cf623
main
shims: nix and cargo
the model's jobs (run(), job.pipe(), and remote kernels' alike) find two
shims first on PATH, each under $ALBEDO_HOME/shims/<needs>/ and prepended
only when the program it needs is on the job's PATH (albedo_shims.prepend):
shims/nix/nix when nix is, shims/mbx/cargo when mbx is. the kernel writes
them where it runs (only .py files travel to a remote kernel), and every
nix call a job makes goes through the shim, however deep in a script it is.
Before lookup, prepend removes inherited */shims/nix and */shims/mbx
directories, then installs only this kernel's shims for real tools on PATH.
Nested daemon jobs must not resolve one shim generation through another.
there is no nix API for the model to learn: it types nix develop -c cargo test or nix build .#x and the shim makes that safe and fast.
a daemon started with a thin PATH (a GUI or launchd start never ran the user's
profile) would see no nix at all, so prepend first looks for each program of
FALLBACKS that PATH lacks and appends the first directory holding it
(/nix/var/nix/profiles/default/bin, then ~/.nix-profile/bin). it goes last,
so it never shadows the user's PATH, and it joins before the shim check so the
shim still goes in front. it covers job environments only: shutil.which in a
cell reads the kernel process's own PATH.
nix
why
a jj workspace other than the colocated one has .jj/ but no .git, so nix
reads . as a plain path: flake and copies the whole directory into the
store on every evaluation: build output, target/, .jj and all (140 MB per
eval measured on a small checkout, ~850 MB for a full albedo one). a git
checkout or worktree is fine: nix's git fetcher copies tracked files only.
the program
shims/nix/nix is sh: with ALBEDO_SHIMS_PYTHON and ALBEDO_SHIMS_LIB set
(by prepend, the kernel's python and its priv/python) it runs
albedo_nix.py with python -E -s, else, and with ALBEDO_PLAIN_NIX=1, the
next nix on PATH. albedo_nix.py is stdlib only and never imported by the
kernel. what it does with a command line:
- reads it with nix's own flag table,
nix __dump-cli(50 ms, cached in$ALBEDO_HOME/cache/nix-cli/per nix binary), so every flag's arity is known. commands it cannot read (a flag or command not in the table) go to nix unchanged, which refuses them itself.ALIASEScovers nix's renamed commands the table lists only under their new name (nix shellisenv shell,profile installisprofile add,dev-shellisdevelop). nix develop [installable] -c program ...with only output flags (QUIET) runs the program in the cached dev shell (below), with the shims kept first on PATH. any other develop flag (-i,--phase, ...) goes to step 3.- every word that names a local flake of a jj workspace without
.gitis replaced by the workspace's commit ref, following the subcommand's argument labels in the table (installables,installable,flake-url,package,dependency), plus--inputs-fromandflake update --flake. a command that names none defaults to.as nix does (notnix eval, not with--expr/--file/--stdin/--all).nix fmtandnix formatter run|buildbecomenix run|build <ref>#formatter.<system>withPRJ_ROOTset to the flake's directory, as nix fmt sets it. the rewritten command is printed to stderr afteralbedo:, then exec'd. - anything else is exec'd unchanged.
refs and lock files
flake_ref: jj log -r @ snapshots the working copy, so commit @ holds every
edit, and jj git root names the git store. the ref is
git+file://<store>?rev=<commit> (&dir= for a flake below the workspace
root); a colocated .git is named by its work tree (nix reads it in place;
naming the .git directory makes nix clone it into its cache), a bare store as
itself. jj keeps every commit under refs/jj/keep/*, so nix finds it. git
checkouts and directories outside version control are never rewritten.
nix writes a lock file it had to create or update into the repository a
git+file ref names: for a colocated store that is the main checkout's work
tree (measured: nix build <ref> created main/flake.lock). so a rewritten
command gets --output-lock-file <workspace flake>/flake.lock, which writes
where nix would have in a git checkout. that flag covers every flake the
command locks (--inputs-from . nixpkgs#hello wrote nixpkgs' empty lock over
the workspace's), so a command that locks any other flake gets
--no-write-lock-file instead. --commit-lock-file would commit in the main
checkout and is refused. nix rewrites the output lock file even when it is
unchanged; nix-formatted locks keep their content.
dev shells
nix print-dev-env --profile <cache>/<key> <ref> writes the same script
nix develop sources; the shim sources it with the shell's own bash (the
shell variable from the profile's JSON: macOS's /bin/bash 3.2 cannot run it),
which runs the shellHook (its output goes to stderr), removes $NIX_BUILD_TOP,
and dumps env -0. what is kept is the difference from the job's environment
(delta): variables set, prefixes put before a list the shell kept (PATH,
XDG_DATA_DIRS) and variables unset. every variable the script exports is kept
even where the capture's own environment already held its value (mkShell
env.* values, CC, NIX_CFLAGS_COMPILE, ...): what nix develop -c gives a
program does not depend on who ran it, and a capture run inside the very shell
it was capturing kept nothing of it once (nix develop -c env showed no
CGO_ENABLED, and a cgo link failed). the script's function bodies do not run,
so their exports do not count; the paths it composes from the caller's own (it
saves them as nix_saved_<name>) stay prefixes; and what the script leaves
alone (HOME, TERM) is the caller's. TMPDIR, NIX_BUILD_TOP and the like
(VOLATILE) are dropped. applied lays it over each later job's environment.
a build prints albedo: building the dev shell for <dir> first; a cached run
prints nothing of its own.
the cache is $ALBEDO_HOME/cache/nix-develop/<key>.json, and the profile
<key> beside it is a garbage-collector root for the shell's closure. the key
hashes the system, the attribute, the flake's place in its checkout, and the
content of every tracked SHELL_FILES file (*.nix, flake.lock,
rust-toolchain, rust-toolchain.toml; jj file list --ignore-working-copy
or git ls-files, a walk outside version control). so an unchanged flake
costs one jj file list and no nix call (60 ms), and every checkout of the
same repository at the same shell files shares one entry. a build that wrote
the lock file is stored under the key after it too. a record another capture
shape wrote (it may carry too little) is rebuilt when next wanted, not
replayed. the 32 most recently used entries are kept; a flake elsewhere
(nixpkgs#hello) is keyed by its name and kept a day. shims building the same
key at once wait on one flock.
limits: a dev shell made of other files (built from Cargo.lock or
pyproject.toml) is not rebuilt when only those change; a shellHook with side
effects (starting a service) runs once per build, not per command; a stale jj
workspace (jj workspace update-stale) fails with jj's message. nix-shell
and nix-build are other binaries and are not shimmed.
cargo
shims/mbx/cargo runs mbx (jdx's mr-boxington) in its shim mode, so every
checkout shares compiled work through mbx's cache. ALBEDO_NO_MBX=1, or mbx
gone from PATH, runs the next cargo. mbx's own mbx setup is not used: it
edits the user's rust-analyzer config, and its shim exits 127 when mbx is
missing.
what the shim changes for mbx's process, all measured on native/render (a second checkout: 32 s with 55 links bypassed, 2.1 s with 201 hits after):
SDKROOTis unset. mbx names the macOS linker withxcrun --sdk "$SDKROOT" --show-sdk-version, and xcrun rejects a path such as a nix shell's store SDK, which leaves the linker unnamed and every native link uncached: build scripts, proc macros, binaries, tests, and so every crate depending on a proc macro or a cc-built-syscrate.DEVELOPER_DIRstays, so the same SDK is linked.- inside a nix shell (
ALBEDO_NIX_ENV, which the dev-shell capture sets),CCandCXXare unset when they resolve to the same file asccandc++on PATH (the nix cc-wrapper'sccis a symlink to itsclang), so a build script's C compiles through mbx and stays portable: an explicitCCmakes mbx leave that C alone (cc-compiler-override), and a dev build's objects then record the checkout's paths (synthetic workspace, second checkout: 31 hits and 3 misses withCXXset, 34 and 0 without). a shell whoseCCis another compiler (gcc, a cross compiler) keeps it. andHOST_CFLAGS/HOST_CXXFLAGSgain-DALBEDO_NIX_ENV_<digest>. the digest is of the shell's compiler-wrapper variables (NIX_CFLAGS*,NIX_LDFLAGS*,NIX_CC*,NIX_HARDENING_ENABLE), which change C output but are in no mbx key: without it a changed shell was served another checkout's stale object.
never set for cargo here: CARGO_INCREMENTAL, RUSTC_WRAPPER, MBX_CACHE_DIR
(one cache is the point), a --target equal to the host, or -C link-arg.
what stays per checkout: crates whose build-script output or source names the
checkout or target path and their dependents (aws-lc-sys's memcmp probe leaves
a path-bearing .dSYM in OUT_DIR under -g; tikv-jemalloc-sys runs
autotools there unless JEMALLOC_OVERRIDE names a built library), and a
workspace crate being edited. a new toolchain or dev shell is a new key, as it
should be.
mbx replaces a checkout's target/ with a symlink into its cache. an ignore
rule with a trailing slash (target/) matches directories only, so jj would
track that symlink: albedo's own rule is /native/render/target.
1# shims: nix and cargo 2 3the model's jobs (`run()`, `job.pipe()`, and remote kernels' alike) find two 4shims first on PATH, each under `$ALBEDO_HOME/shims/<needs>/` and prepended 5only when the program it needs is on the job's PATH (`albedo_shims.prepend`): 6`shims/nix/nix` when nix is, `shims/mbx/cargo` when mbx is. the kernel writes 7them where it runs (only `.py` files travel to a remote kernel), and every 8nix call a job makes goes through the shim, however deep in a script it is. 9Before lookup, `prepend` removes inherited `*/shims/nix` and `*/shims/mbx` 10directories, then installs only this kernel's shims for real tools on PATH. 11Nested daemon jobs must not resolve one shim generation through another. 12there is no nix API for the model to learn: it types `nix develop -c cargo 13test` or `nix build .#x` and the shim makes that safe and fast. 14 15a daemon started with a thin PATH (a GUI or launchd start never ran the user's 16profile) would see no nix at all, so `prepend` first looks for each program of 17`FALLBACKS` that PATH lacks and appends the first directory holding it 18(`/nix/var/nix/profiles/default/bin`, then `~/.nix-profile/bin`). it goes last, 19so it never shadows the user's PATH, and it joins before the shim check so the 20shim still goes in front. it covers job environments only: `shutil.which` in a 21cell reads the kernel process's own PATH. 22 23## nix 24 25### why 26 27a jj workspace other than the colocated one has `.jj/` but no `.git`, so nix 28reads `.` as a plain `path:` flake and copies the whole directory into the 29store on every evaluation: build output, `target/`, `.jj` and all (140 MB per 30eval measured on a small checkout, ~850 MB for a full albedo one). a git 31checkout or worktree is fine: nix's git fetcher copies tracked files only. 32 33### the program 34 35`shims/nix/nix` is sh: with `ALBEDO_SHIMS_PYTHON` and `ALBEDO_SHIMS_LIB` set 36(by `prepend`, the kernel's python and its `priv/python`) it runs 37`albedo_nix.py` with `python -E -s`, else, and with `ALBEDO_PLAIN_NIX=1`, the 38next nix on PATH. `albedo_nix.py` is stdlib only and never imported by the 39kernel. what it does with a command line: 40 411. reads it with nix's own flag table, `nix __dump-cli` (50 ms, cached in 42`$ALBEDO_HOME/cache/nix-cli/` per nix binary), so every flag's arity is 43 known. commands it cannot read (a flag or command not in the table) go to 44 nix unchanged, which refuses them itself. `ALIASES` covers nix's renamed 45 commands the table lists only under their new name (`nix shell` is 46 `env shell`, `profile install` is `profile add`, `dev-shell` is `develop`). 47 2. `nix develop [installable] -c program ...` with only output flags (`QUIET`) 48runs the program in the cached dev shell (below), with the shims kept first 49 on PATH. any other develop flag (`-i`, `--phase`, ...) goes to step 3. 50 3. every word that names a local flake of a jj workspace without `.git` is 51replaced by the workspace's commit ref, following the subcommand's argument 52 labels in the table (`installables`, `installable`, `flake-url`, `package`, 53 `dependency`), plus `--inputs-from` and `flake update --flake`. a command 54 that names none defaults to `.` as nix does (not `nix eval`, not with 55 `--expr`/`--file`/`--stdin`/`--all`). `nix fmt` and `nix formatter run|build` 56 become `nix run|build <ref>#formatter.<system>` with `PRJ_ROOT` set to the 57 flake's directory, as nix fmt sets it. the rewritten command is printed to 58 stderr after `albedo:`, then exec'd. 59 4. anything else is exec'd unchanged. 60 61### refs and lock files 62 63`flake_ref`: `jj log -r @` snapshots the working copy, so commit `@` holds every 64edit, and `jj git root` names the git store. the ref is 65`git+file://<store>?rev=<commit>` (`&dir=` for a flake below the workspace 66root); a colocated `.git` is named by its work tree (nix reads it in place; 67naming the `.git` directory makes nix clone it into its cache), a bare store as 68itself. jj keeps every commit under `refs/jj/keep/*`, so nix finds it. git 69checkouts and directories outside version control are never rewritten. 70 71nix writes a lock file it had to create or update into the repository a 72`git+file` ref names: for a colocated store that is the main checkout's work 73tree (measured: `nix build <ref>` created `main/flake.lock`). so a rewritten 74command gets `--output-lock-file <workspace flake>/flake.lock`, which writes 75where nix would have in a git checkout. that flag covers every flake the 76command locks (`--inputs-from . nixpkgs#hello` wrote nixpkgs' empty lock over 77the workspace's), so a command that locks any other flake gets 78`--no-write-lock-file` instead. `--commit-lock-file` would commit in the main 79checkout and is refused. nix rewrites the output lock file even when it is 80unchanged; nix-formatted locks keep their content. 81 82### dev shells 83 84`nix print-dev-env --profile <cache>/<key> <ref>` writes the same script 85`nix develop` sources; the shim sources it with the shell's own bash (the 86`shell` variable from the profile's JSON: macOS's /bin/bash 3.2 cannot run it), 87which runs the shellHook (its output goes to stderr), removes `$NIX_BUILD_TOP`, 88and dumps `env -0`. what is kept is the difference from the job's environment 89(`delta`): variables set, prefixes put before a list the shell kept (PATH, 90XDG_DATA_DIRS) and variables unset. every variable the script exports is kept 91even where the capture's own environment already held its value (mkShell 92`env.*` values, `CC`, `NIX_CFLAGS_COMPILE`, ...): what `nix develop -c` gives a 93program does not depend on who ran it, and a capture run inside the very shell 94it was capturing kept nothing of it once (`nix develop -c env` showed no 95`CGO_ENABLED`, and a cgo link failed). the script's function bodies do not run, 96so their exports do not count; the paths it composes from the caller's own (it 97saves them as `nix_saved_<name>`) stay prefixes; and what the script leaves 98alone (`HOME`, `TERM`) is the caller's. `TMPDIR`, `NIX_BUILD_TOP` and the like 99(`VOLATILE`) are dropped. `applied` lays it over each later job's environment. 100a build prints `albedo: building the dev shell for <dir>` first; a cached run 101prints nothing of its own. 102 103the cache is `$ALBEDO_HOME/cache/nix-develop/<key>.json`, and the profile 104`<key>` beside it is a garbage-collector root for the shell's closure. the key 105hashes the system, the attribute, the flake's place in its checkout, and the 106content of every tracked `SHELL_FILES` file (`*.nix`, `flake.lock`, 107`rust-toolchain`, `rust-toolchain.toml`; `jj file list --ignore-working-copy` 108or `git ls-files`, a walk outside version control). so an unchanged flake 109costs one `jj file list` and no nix call (60 ms), and every checkout of the 110same repository at the same shell files shares one entry. a build that wrote 111the lock file is stored under the key after it too. a record another capture 112shape wrote (it may carry too little) is rebuilt when next wanted, not 113replayed. the 32 most recently used entries are kept; a flake elsewhere 114(`nixpkgs#hello`) is keyed by its name and kept a day. shims building the same 115key at once wait on one `flock`. 116 117limits: a dev shell made of other files (built from Cargo.lock or 118pyproject.toml) is not rebuilt when only those change; a shellHook with side 119effects (starting a service) runs once per build, not per command; a stale jj 120workspace (`jj workspace update-stale`) fails with jj's message. `nix-shell` 121and `nix-build` are other binaries and are not shimmed. 122 123## cargo 124 125`shims/mbx/cargo` runs mbx (jdx's mr-boxington) in its shim mode, so every 126checkout shares compiled work through mbx's cache. `ALBEDO_NO_MBX=1`, or mbx 127gone from PATH, runs the next cargo. mbx's own `mbx setup` is not used: it 128edits the user's rust-analyzer config, and its shim exits 127 when mbx is 129missing. 130 131what the shim changes for mbx's process, all measured on native/render (a 132second checkout: 32 s with 55 links bypassed, 2.1 s with 201 hits after): 133 134- `SDKROOT` is unset. mbx names the macOS linker with 135`xcrun --sdk "$SDKROOT" --show-sdk-version`, and xcrun rejects a path such as 136a nix shell's store SDK, which leaves the linker unnamed and every native 137link uncached: build scripts, proc macros, binaries, tests, and so every 138crate depending on a proc macro or a cc-built `-sys` crate. `DEVELOPER_DIR` 139stays, so the same SDK is linked. 140- inside a nix shell (`ALBEDO_NIX_ENV`, which the dev-shell capture sets), `CC` 141and `CXX` are unset when they resolve to the same file as `cc` and `c++` on 142PATH (the nix cc-wrapper's `cc` is a symlink to its `clang`), so a build 143script's C compiles through mbx and stays portable: an explicit `CC` makes 144mbx leave that C alone (`cc-compiler-override`), and a dev build's objects 145then record the checkout's paths (synthetic workspace, second checkout: 31 146hits and 3 misses with `CXX` set, 34 and 0 without). a shell whose `CC` is 147another compiler (gcc, a cross compiler) keeps it. and `HOST_CFLAGS`/`HOST_CXXFLAGS` gain 148`-DALBEDO_NIX_ENV_<digest>`. the digest is of the shell's compiler-wrapper 149variables (`NIX_CFLAGS*`, `NIX_LDFLAGS*`, `NIX_CC*`, 150`NIX_HARDENING_ENABLE`), which change C output but are in no mbx key: 151without it a changed shell was served another checkout's stale object. 152 153never set for cargo here: `CARGO_INCREMENTAL`, `RUSTC_WRAPPER`, `MBX_CACHE_DIR` 154(one cache is the point), a `--target` equal to the host, or `-C link-arg`. 155what stays per checkout: crates whose build-script output or source names the 156checkout or target path and their dependents (aws-lc-sys's memcmp probe leaves 157a path-bearing `.dSYM` in `OUT_DIR` under `-g`; tikv-jemalloc-sys runs 158autotools there unless `JEMALLOC_OVERRIDE` names a built library), and a 159workspace crate being edited. a new toolchain or dev shell is a new key, as it 160should be. 161 162mbx replaces a checkout's `target/` with a symlink into its cache. an ignore 163rule with a trailing slash (`target/`) matches directories only, so jj would 164track that symlink: albedo's own rule is `/native/render/target`.