Package management
Every package layer has a declarative text file and an idempotent install script. All scripts skip already-installed items — safe to re-run at any time.
The layers
| Layer | File | Install script | Platform |
|---|---|---|---|
| System packages | packages/Brewfile | install/homebrew.sh / install/linux-packages.sh | macOS (bottles) / Linux (native, no container) |
| Rust tools | packages/cargo.txt | install/rust.sh | All |
| Python packages | packages/pip.txt, packages/pip-full.txt | install/python.sh | All |
| Global npm | packages/npm.txt, packages/npm-allow-scripts.txt | install/node.sh | All |
| Go CLI tools | packages/go.txt | install/go.sh | All (respects # linux-only / # macos-only) |
| Claude plugins | packages/claude-plugins.txt | install/claude.sh | All |
| Agent skills | packages/agent-skills.txt, packages/agent-skills.lock.json | install/skills-sync.sh | All (shared ~/.claude/skills tree) |
| MCP servers (Claude + Codex) | packages/mcp-servers.txt | install/claude.sh, install/codex.sh | All |
| Codex CLI/config | home/dot_codex/ | install/codex.sh | All |
| Cursor extensions | packages/cursor-extensions.txt | install/cursor.sh | All |
| VS Code extensions | packages/vscode-extensions.txt | install/vscode.sh | All |
Adding a package — priority order
Choose the first layer that applies. Native installers first, Homebrew as fallback:
1. cargo — Rust crates
# Add to packages/cargo.txt
fd-find
ripgrep
bat
typst-cli
my-new-tool
Re-run: bash ~/dotfiles/install/rust.sh
install/rust.sh uses cargo-binstall: it tries to
download a pre-built binary from GitHub releases first (fast, no compilation), and falls back to
cargo install (source compilation) if no binary is available.
On Linux, cargo-binstall avoids the manylinux container round-trip entirely. On macOS, it downloads the same pre-built binary that Homebrew bottles provide — same quality, faster install.
On Linux, musl targets are preferred over gnu (--targets <arch>-unknown-linux-musl,...): static
musl builds have no glibc dependency, while gnu prebuilts from modern CI runners (Ubuntu 24.04 =
glibc 2.39) refuse to load on older hosts. After each install the crate’s binaries are smoke-tested;
one that fails with a dynamic-loader error is force-refetched (musl-first) and, if still broken,
rebuilt from source against the host glibc.
macOS note: Source compilation requires running from a normal terminal. The macOS Sequoia linker enforces
com.apple.provenanceon object files and will block compilation in sandboxed contexts (e.g., certain CI environments). This isn’t an issue for day-to-day use.
2. npm — npm-specific tools
# packages/npm.txt
@earendil-works/pi-coding-agent
Re-run: bash ~/dotfiles/install/node.sh
npm-allow-scripts.txt is the reviewed lifecycle-script allowlist for global
tools. The installer passes it per command instead of persisting a policy in
~/.npmrc.
nvm owns Node, npm, and npm’s global prefix under the PLAT-specific $NVM_DIR.
Keep ~/.npmrc for registry/auth and npm behavior only; do not set prefix or
globalconfig. packages/npm.txt is the source of truth for global CLIs, and
install/node.sh reconciles them into the supported default Node LTS tree.
Currently ships pi — a multi-provider coding agent (Claude / OpenAI / Gemini / etc.). The official pi.dev/install.sh ultimately runs npm install -g @earendil-works/pi-coding-agent, so we list it here directly.
Other CLI agents are installed via their native packagers:
claude-code→install/claude.sh(Anthropic GCS binary)codex→ unpinned@openai/codexinpackages/npm.txt; managed config and healthcheck viainstall/codex.shopencode→brew "opencode"(packages/Brewfile)
Codex CLI config, rules, themes, and MCP servers are managed from home/dot_codex/
(skills live in home/dot_claude/skills/, shared via the ~/.agents/skills symlink).
install/codex.sh sync-config preserves runtime trust/plugin sections while refreshing the
managed config. Chezmoi also runs this sync when home/dot_codex/create_private_config.toml changes.
3. pip — Python packages
# packages/pip.txt (core) or packages/pip-full.txt (full profile)
requests
black
numpy
some-macos-tool # macos-only (requires Metal / only available on macOS)
Re-run: bash ~/dotfiles/install/python.sh
Each tool gets its own isolated venv via uv tool install, with entrypoints in $LOCAL_PLAT/bin/.
DF_PROFILE=full is the default and installs both manifests. Use
DF_PROFILE=core for a small bootstrap or CI environment; the core profile
also keeps the Rust toolchain while skipping optional cargo.txt tools.
Comment conventions parsed by install/python.sh:
# macos-only— skipped on Linux (e.g.mlx-lmrequires Apple Metal/MLX framework)# python=X.Y— pins to a specific Python version for that tool (e.g.mlx-openai-serverneeds 3.12 becauseoutlines-corehas no cp313/cp314 wheels)
4. Homebrew — non-language-specific tools and C libraries
# packages/Brewfile
brew "tool-name"
# macOS-only (casks, GUI apps, macOS-specific services)
if OS.mac?
cask "some-app"
brew "macos-only-tool"
end
Re-run: brew bundle --file=~/dotfiles/packages/Brewfile
if OS.mac? blocks are silently skipped on Linux. Everything outside those blocks runs on both platforms.
Prefer Homebrew for tools that aren’t available via cargo/npm/pip, have complex C dependencies, or are macOS-specific (casks, GUI apps).
5. VS Code / Cursor extensions
Both editors have separate extension lists since marketplace availability differs (Cursor uses OpenVSX, which doesn’t carry every Microsoft-restricted extension).
# packages/vscode-extensions.txt (VS Code marketplace)
# packages/cursor-extensions.txt (OpenVSX, Cursor)
ms-python.python
charliermarsh.ruff
myriad-dreamin.tinymist # Typst LSP — works in both
Re-run: bash ~/dotfiles/install/vscode.sh and/or bash ~/dotfiles/install/cursor.sh
To capture newly installed extensions back into the file (union — never removes):
bash ~/dotfiles/install/vscode.sh sync-extensions
bash ~/dotfiles/install/cursor.sh sync-extensions
Note: VS Code
settings.jsonis not tracked (contains embedded credentials in some setups). Cursor’s settings ARE tracked via symlinks underhome/dot_cursor/.
6. Custom install script
Look at an existing install/ script for patterns and follow them. Add a DF_DO_* flag to bootstrap.sh.
Local AI tools
Local LLM inference and coding agents are split across three layers:
| Tool | Layer | Notes |
|---|---|---|
ollama | packages/Brewfile (macOS only) | Inference server; installed as Homebrew formula, managed as a LaunchAgent |
opencode | packages/Brewfile | TUI coding agent by the SST team |
mlx-lm | packages/pip-full.txt | Apple Silicon Metal inference; full profile only |
just | packages/cargo.txt | Command runner / Makefile alternative |
install/local-llm.sh creates the PLAT-isolated HuggingFace cache directory ($LOCAL_PLAT/.cache/huggingface)
and verifies that the expected binaries are present. install/opencode.sh verifies the opencode binary; opencode’s backend config is pure chezmoi (opencode.json.tmpl, MLX primary).
See Local AI coding for usage details.
Research mathematics
Two of these get their own install script because neither has a usable Homebrew path on no-sudo Linux; the rest are ordinary package-list entries.
| Tool | Layer | Notes |
|---|---|---|
Lean 4 + lake | install/lean.sh | elan (Lean’s rustup) → $LOCAL_PLAT/elan. Toolchains are ~1.5 GB and arch-specific, hence PLAT-isolated. Pin lives in the script; override with DF_LEAN_TOOLCHAIN. |
| TeX | install/latex.sh | macOS: cask "mactex". Linux: TinyTeX under $LOCAL_PLAT/tex/.TinyTeX, with tlmgr sys_bin pointed at $ARCH_BIN. |
| PARI/GP, FLINT, z3, minizinc, cadical, kissat | packages/Brewfile | gp collides with the gp='git push' alias — use command gp. |
| Sage, Zotero | packages/Brewfile casks (macOS) | Homebrew core has no Sage formula; per-project passagemath wheels are the uv-native route. |
Julia / juliaup | packages/Brewfile + install/julia.sh | The rolling release channel and depots are PLAT-isolated; OSCAR.jl stays project-local. |
| R | packages/Brewfile | Statistical runtime; project packages stay reproducible through renv. |
leanblueprint, marimo, paper-qa, papis, … | packages/pip-full.txt | Full-profile uv tool install entries. |
rga (ripgrep-all) | packages/cargo.txt | Full-text search across a PDF/EPUB paper library. |
Agent-side wiring (lean-lsp, arxiv, mathlas, asta MCP servers, and the
verification-first norms in math-common.md) is covered in
Agent guidance.
Don’t duplicate across layers
Do not install the same tool in both cargo.txt and Brewfile. $LOCAL_PLAT paths come first on PATH — the Homebrew copy would install but never be used. If a tool is in cargo.txt, it must not be in Brewfile, and vice versa.
Why cargo over Homebrew for Rust tools
Tools like fd, sd, bat, ripgrep, git-delta, difftastic, procs, bottom,
ast-grep, zoxide, and hyperfine live in cargo.txt because:
$CARGO_HOME/bin/is already under$LOCAL_PLAT/— PLAT isolation is freecargo-binstalldownloads pre-built GitHub release binaries — fast, no compilation
Tools that have no pre-built binary and are painful to compile (or only make sense on macOS) go in
Brewfile under if OS.mac?.
Why Homebrew for Linux
Homebrew on Linux installs natively on the host (no container, no sudo). It bundles its own glibc, making binaries fully self-contained regardless of the host’s glibc version.
The glibc keg tracks the formula. Homebrew’s Linux bottles carry the glibc floor of the
CI image that built them, and a builder move comes with a formula bump (Ubuntu 22.04 → 24.04
and glibc 2.35 → 2.39 in July 2026). Since glibc is installed by linux-packages.sh rather
than the Brewfile, nothing else would ever upgrade it — and a keg left behind makes every
formula poured afterwards die with version `GLIBC_2.38' not found. Each run reconciles the
keg first, then checks the kegs installed since the last run against what the keg provides.
The keg is built for the architecture baseline, not the build host’s CPU, so a prefix built on
an AVX-512 node still runs on every other machine sharing the home.
Custom prefix tradeoff: Installing to $LOCAL_PLAT/brew/ instead of the standard
/home/linuxbrew/.linuxbrew enables a rootless flat prefix by default and per-CPU
isolation when PLAT mode is enabled, but bottles
built for the standard prefix can’t always be relocated:
- Relocatable packages (jq, CLI tools with simple dependencies) pour as bottles — patchelf rewrites RPATH and they work fine
- Deep path embedding (Python, Perl, git, vim, ffmpeg, imagemagick) build from source
on first install. Homebrew uses all available CPU cores (auto-detects
nproc), so builds are fast on modern hardware.
Once built, packages are cached. Subsequent runs and upgrades are bottle-only.
Compilers: gcc and llvm are keg-only (Homebrew doesn’t create unversioned gcc/clang
symlinks to avoid shadowing system compilers). linux-packages.sh creates symlinks in
$LOCAL_PLAT/bin/ so gcc → the highest installed GCC and clang → llvm@21/bin/clang.
See Compiler toolchains below for CMake integration.
Python@3.14 patches: On Linux, install/patch-homebrew-python.sh automatically patches
the python@3.14 formula to fix build issues (uuid module detection, test_datetime PGO hangs).
Patches are applied during bootstrap and protected by HOMEBREW_NO_AUTO_UPDATE=1.
The same Brewfile works on macOS and Linux. if OS.mac? blocks are silently skipped on Linux.
Compiler toolchains
CMake compiler selection is handled by toolchain files deployed per-PLAT, not by raw
CC/CXX env vars. install/cmake.sh copies them from install/cmake/toolchains/
to $LOCAL_PLAT/cmake/toolchains/ on every bootstrap run (always overwrites, so they
stay in sync with the repo).
Default: LLVM (Homebrew clang)
Toolchain files are versioned: llvm-21.cmake, llvm-22.cmake, gcc-13.cmake, gcc-15.cmake, plus a shared _brew.cmake helper.
When Homebrew LLVM is present, ~/.profile auto-sets:
export CMAKE_TOOLCHAIN_FILE="$_LOCAL_PLAT/cmake/toolchains/llvm-22.cmake"
# (highest installed LLVM version wins; falls back to llvm-21)
The toolchain configures:
| CMake variable | Value |
|---|---|
CMAKE_C_COMPILER | $_LOCAL_PLAT/brew/opt/llvm@22/bin/clang (or unversioned opt/llvm/) |
CMAKE_CXX_COMPILER | $_LOCAL_PLAT/brew/opt/llvm@22/bin/clang++ |
CMAKE_AR / CMAKE_RANLIB | llvm-ar, llvm-ranlib (LTO needs the matching tool) |
CMAKE_LINKER_TYPE | MOLD > LLD (Linux only; macOS uses Apple’s ld) |
CMAKE_CUDA_COMPILER | $_LOCAL_PLAT/.cuda/bin/nvcc (only if symlink set up) |
CMAKE_CUDA_HOST_COMPILER | clang++ (when CUDA available) |
CMake auto-detects nm/objcopy/objdump/strip from CC, so the toolchain files only override what actually matters.
Switching toolchains
Per-invocation:
CMAKE_TOOLCHAIN_FILE="$_LOCAL_PLAT/cmake/toolchains/gcc-15.cmake" cmake -B build
Per-session via the tc shell function:
tc # show active
tc list # list available
tc gcc-15 # GCC 15
tc gcc-13 # GCC 13
tc llvm-22 # LLVM 22
tc llvm-21 # LLVM 21
Per-project (CMakePresets.json):
{ "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "/absolute/path/to/gcc-15.cmake" } }
The GCC toolchains use versioned binaries (gcc-15, g++-15, etc.) because Homebrew doesn’t create unversioned gcc symlinks on macOS. Linux gets unversioned symlinks via linux-packages.sh, but the versioned files work on both. Linker priority on Linux: mold → lld → gold → system ld.
Disabling the toolchain
unset CMAKE_TOOLCHAIN_FILE # let CMake auto-detect compilers
CUDA
CUDA is not managed by bootstrap — install the toolkit separately (system package, NVIDIA runfile, or a module system on HPC). Then point the per-PLAT symlink at it:
ln -sfn /usr/local/cuda "$_LOCAL_PLAT/.cuda" # system default
ln -sfn /opt/nvidia/cuda/12.6 "$_LOCAL_PLAT/.cuda" # versioned install
~/.profile resolves the symlink at login and exports:
CUDA_PATHandCUDAToolkit_ROOT— picked up by CMake’sfind_package(CUDAToolkit)and most other build systems- Prepends
$CUDA_PATH/bintoPATHsonvccis on the path
Both toolchain files also set CMAKE_CUDA_COMPILER to $LOCAL_PLAT/.cuda/bin/nvcc when the
symlink exists, so enable_language(CUDA) works without any project-level configuration.
Different machines on a shared NFS home can point their $LOCAL_PLAT/.cuda symlinks at
different toolkit versions — no conflicts.
Switching toolchains at runtime
The tc shell function (defined in .zshrc) switches the active toolchain for the current session:
tc # show active toolchain
tc list # list available toolchain files
tc gcc-15 # switch to GCC 15 (sets CC/CXX/AR/RANLIB/NM + CMAKE_TOOLCHAIN_FILE)
tc gcc-13 # switch to GCC 13
tc llvm-22 # switch to LLVM 22 (clears CC/CXX; CMake file owns compiler selection)
tc llvm-21 # switch to LLVM 21
Compiler caching (ccache / sccache)
~/.profile configures ccache and sccache automatically when they’re installed:
| Setting | Value | Why |
|---|---|---|
CCACHE_BASEDIR | scratch root or $HOME | Rewrites absolute paths to relative before hashing — builds in different directories share cache hits |
CCACHE_COMPILERCHECK | content | Hash compiler by content, not mtime — survives brew reinstalls and module swaps |
CCACHE_SLOPPINESS | file_stat_matches,time_macros | Use mtime+size for include checks; cache TUs with __DATE__/__TIME__ |
CCACHE_HARDLINK | 1 | Hardlink cached objects instead of copying — halves I/O on cache hits |
CCACHE_MAXSIZE | 2% of partition, clamped [10G, 100G] | Auto-sized to scratch partition |
RUSTC_WRAPPER | sccache | Rust compiler caching |
SCCACHE_CACHE_SIZE | 2% of partition, clamped [10G, 100G] | Same auto-sizing as ccache |
CMake integration: CMAKE_C_COMPILER_LAUNCHER=ccache and CMAKE_CXX_COMPILER_LAUNCHER=ccache are exported automatically.
openssh from Homebrew
The Brewfile installs openssh cross-platform (not just macOS) to avoid OpenSSL version
mismatches between the system ssh and Homebrew-linked libraries. On Linux, the system
ssh may link against a different OpenSSL than Homebrew’s, causing git push failures
when Homebrew’s git shells out to ssh. Brew’s openssh uses Homebrew’s OpenSSL consistently.
Source files
Toolchain source files live in install/cmake/toolchains/ — edit them there, not in the
deployed copies under $LOCAL_PLAT/. Re-deploy with:
bash ~/dotfiles/install/cmake.sh
Then wipe the CMake cache (rm -rf build/CMakeCache.txt build/CMakeFiles) for the changes
to take effect in an existing build directory.
Updating all packages
~/dotfiles/bootstrap.sh update # pull + refresh (install missing, skip current)
~/dotfiles/bootstrap.sh upgrade # update + brew upgrade + cargo upgrade
update refreshes tools without upgrading existing versions. upgrade additionally enables Homebrew upgrades and forces cargo-binstall to re-check for newer binaries. Both are idempotent — safe to run at any time.