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 | install/python.sh | All |
| Global npm | packages/npm.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 |
| 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.
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
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→@openai/codex(version-pinned) inpackages/npm.txt; managed config 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_config.toml changes.
3. pip — Python packages
# packages/pip.txt
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/.
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.txt | Apple Silicon Metal inference; on-demand 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.
Don’t duplicate across layers
Do not install the same tool in both cargo.txt and Brewfile. PLAT paths (~/.local/$PLAT/) 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 2.35, making binaries fully self-contained regardless of the host’s glibc version.
Custom prefix tradeoff: Installing to ~/.local/$PLAT/brew/ instead of the standard
/home/linuxbrew/.linuxbrew enables per-CPU isolation on shared NFS homes, 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.