Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

LayerFileInstall scriptPlatform
System packagespackages/Brewfileinstall/homebrew.sh / install/linux-packages.shmacOS (bottles) / Linux (native, no container)
Rust toolspackages/cargo.txtinstall/rust.shAll
Python packagespackages/pip.txtinstall/python.shAll
Global npmpackages/npm.txtinstall/node.shAll
Go CLI toolspackages/go.txtinstall/go.shAll (respects # linux-only / # macos-only)
Claude pluginspackages/claude-plugins.txtinstall/claude.shAll
MCP servers (Claude + Codex)packages/mcp-servers.txtinstall/claude.sh, install/codex.shAll
Codex CLI/confighome/dot_codex/install/codex.shAll
Cursor extensionspackages/cursor-extensions.txtinstall/cursor.shAll
VS Code extensionspackages/vscode-extensions.txtinstall/vscode.shAll

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.provenance on 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-codeinstall/claude.sh (Anthropic GCS binary)
  • codex@openai/codex (version-pinned) in packages/npm.txt; managed config via install/codex.sh
  • opencodebrew "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-lm requires Apple Metal/MLX framework)
  • # python=X.Y — pins to a specific Python version for that tool (e.g. mlx-openai-server needs 3.12 because outlines-core has 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.json is not tracked (contains embedded credentials in some setups). Cursor’s settings ARE tracked via symlinks under home/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:

ToolLayerNotes
ollamapackages/Brewfile (macOS only)Inference server; installed as Homebrew formula, managed as a LaunchAgent
opencodepackages/BrewfileTUI coding agent by the SST team
mlx-lmpackages/pip.txtApple Silicon Metal inference; on-demand only
justpackages/cargo.txtCommand 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 free
  • cargo-binstall downloads 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 clangllvm@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 variableValue
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_RANLIBllvm-ar, llvm-ranlib (LTO needs the matching tool)
CMAKE_LINKER_TYPEMOLD > LLD (Linux only; macOS uses Apple’s ld)
CMAKE_CUDA_COMPILER$_LOCAL_PLAT/.cuda/bin/nvcc (only if symlink set up)
CMAKE_CUDA_HOST_COMPILERclang++ (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_PATH and CUDAToolkit_ROOT — picked up by CMake’s find_package(CUDAToolkit) and most other build systems
  • Prepends $CUDA_PATH/bin to PATH so nvcc is 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:

SettingValueWhy
CCACHE_BASEDIRscratch root or $HOMERewrites absolute paths to relative before hashing — builds in different directories share cache hits
CCACHE_COMPILERCHECKcontentHash compiler by content, not mtime — survives brew reinstalls and module swaps
CCACHE_SLOPPINESSfile_stat_matches,time_macrosUse mtime+size for include checks; cache TUs with __DATE__/__TIME__
CCACHE_HARDLINK1Hardlink cached objects instead of copying — halves I/O on cache hits
CCACHE_MAXSIZE2% of partition, clamped [10G, 100G]Auto-sized to scratch partition
RUSTC_WRAPPERsccacheRust compiler caching
SCCACHE_CACHE_SIZE2% 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.