Global macOS env, the Elpe way — pinned, no drift
An emacs (git, ripgrep, …) available in every terminal on your Mac, installed by Elpe from pinned Homebrew bottles — without a mutable global profile.
This is the “global env” people actually want on a work Mac, minus the thing
that makes nix-env / ~/.nix-profile (and brew install one-at-a-time) age
badly: there is no mutable state that accretes. The whole environment is the
single declarative manifest global.ml. It is re-derived
atomically from the pinned closure every time a shell starts, so it can’t drift.
Setup
-
Put your toolbox in
~/.config/elpe/global.ml(copyglobal.ml, edit theformulaelist). -
Add one line to
~/.zshrc:eval "$(ELPE_TARGET_OS=darwin ELPE_TARGET_ARCH=aarch64 \ elpe shell --export ~/.config/elpe/global.ml)" -
Open a terminal.
emacs,git,rgare on PATH — resolved from the content-addressed store via baked@rpath, so they just run.
To change your global set: edit the manifest, open a new terminal (or
source ~/.zshrc). No install, no uninstall, no generations.
Why this doesn’t drift (and nix-env/brew do)
nix-env -i foo / brew install foo mutate a global profile package-by-package:
generations accrete, PATH order depends on install history, cleanup is a chore.
Here the global env is one file. elpe shell --export prints the export
lines derived from that file’s pinned closure; you eval them. Editing the file
and re-sourcing fully replaces the env — nothing is left behind.
Global vs per-project — you can have both
A single global env cannot give each project its own conflicting versions
(project A wants node 18, project B node 22). That’s the real, irreducible
trade-off — and it’s what the per-project layer solves: a repo’s
.envrc (eval "$(elpe shell --export)" via direnv)
prepends its pinned-divergent tools in front of this global PATH when you cd
in, and drops back to the global set when you leave. Both layers are derived,
not installed, so they compose without fighting: global for ambient tools,
per-project for the ones that must diverge.