cd ~/>cat multi-boot-partitioning-cache-isolation.md

Multi-boot partitioning / Cache isolation with btrfs

#linux#btrfs#void#dotfiles#zsh#npm#rust

Why ~/.cache deserves its own subvolume

~/.cache is write-heavy transient data: npm packages, Rust build artifacts, Cargo registry, fnm Node.js installations, browser caches. CoW is the wrong semantic for all of it.

Every cache write under a CoW subvolume causes btrfs to:

  1. Write the new data block at a new location
  2. Update the extent tree to point to the new location
  3. Keep the old block referenced until it is no longer needed

For files that are rewritten frequently and are never worth snapshotting or checksumming, this is pure metadata overhead. It also inflates snapshot sizes — a snapshot of @void that includes a heavily written cache directory captures every CoW version of those cache files.

The @home-cache subvolume solves this cleanly: nodatacow, no checksums, no snapshot inclusion, mounted at ~/.cache.

Creating the subvolume

# mount the pool root
mount -o subvolid=5 /dev/nvme0n1p5 /mnt/pool-root

# create the subvolume
btrfs subvolume create /mnt/pool-root/@home-cache

# set nodatacow on the subvolume directory
chattr +C /mnt/pool-root/@home-cache

umount /mnt/pool-root

chattr +C sets the nodatacow attribute on the subvolume root directory. New files created inside inherit the flag. Files copied in do not retroactively lose CoW — they need to be re-created or have chattr +C set on them individually.

Verify:

lsattr -d ~/.cache
# ---------------C------- /home/younes/.cache

fstab entry:

UUID=d7a98795-1604-4baa-ae07-05e29a3ff1e6  /home/younes/.cache  btrfs  subvol=@home-cache,nodatacow,noatime  0 0

The nodatacow mount option and the chattr +C attribute are complementary. The mount option sets the default for the mount; chattr +C on the directory ensures the attribute is persistent in the filesystem.

The managed-* sentinel pattern

~/.cache is not mounted as a btrfs subvolume on every machine. The same dotfiles run on:

  • This Void Linux machine (btrfs, @home-cache mounted)
  • macOS
  • Other Linux machines without this btrfs layout

Tools like npm, fnm, and rustup read their cache paths from environment variables. Setting those variables unconditionally in dotfiles would redirect caches to ~/.cache/managed-npm everywhere — including machines where that path is on a regular CoW filesystem or where the intent was not to redirect.

The solution: a sentinel file. If ~/.cache/managed-npm exists, set the npm cache to it. If it does not exist, set nothing and let the tool use its default.

# ~/.zshenv

# npm cache
[[ -d "$HOME/.cache/managed-npm" ]] && export npm_config_cache="$HOME/.cache/managed-npm"

# fnm
[[ -d "$HOME/.cache/managed-fnm" ]] && export FNM_DIR="$HOME/.cache/managed-fnm"

# rustup
[[ -d "$HOME/.cache/managed-rustup" ]] && export RUSTUP_HOME="$HOME/.cache/managed-rustup"

On the Void machine, create the sentinel directories once:

mkdir -p ~/.cache/managed-npm ~/.cache/managed-fnm ~/.cache/managed-rustup

The directories must exist for the condition to be true. They double as the actual cache directories — no separate sentinel file is needed. On any other machine without these directories, the environment variables are never set, and the tools fall back to their defaults.

Why zshenv and not zshrc

zshenv is sourced for every zsh instance: interactive shells, non-interactive shells, scripts, and subshells. zshrc is sourced only for interactive shells.

Build tools, language runtimes, and package managers often spawn non-interactive subprocesses. npm install may fork child processes. Cargo spawns linkers and compilers. If npm_config_cache is set only in zshrc, those subprocesses do not inherit the cache path when launched non-interactively.

zshenv ensures the cache paths are set in all contexts. Keep zshenv minimal — it is executed on every shell spawn, including short-lived scripts. Heavy computations or slow commands in zshenv add latency to every shell invocation.

Tools migrated

Tool Environment variable Sentinel directory
npm npm_config_cache ~/.cache/managed-npm
fnm FNM_DIR ~/.cache/managed-fnm
rustup RUSTUP_HOME ~/.cache/managed-rustup

FNM_DIR controls where fnm stores Node.js installations, not just its cache. Migrating it to @home-cache means Node.js binaries live on the nodatacow subvolume. This is appropriate — Node.js installations under fnm are re-downloadable and not worth checksumming.

RUSTUP_HOME controls the full rustup installation: toolchains, components, and the registry. Toolchains are large (several GB per target), write-heavy during installation, and completely re-downloadable. nodatacow is the right home for them.

What stays outside @home-cache

Not everything in ~/.cache benefits from nodatacow. Browser profile data — session history, cookies, form autofill — is sometimes written but is also read frequently and benefits from CoW’s data integrity guarantees. The split is pragmatic: redirect tools with explicit environment variables; let everything else fall through to the default ~/.cache behavior.

If the browser’s cache directory specifically (as distinct from its profile directory) becomes a concern, chattr +C can be set on ~/.cache/chromium/Cache or ~/.cache/mozilla/firefox/*/cache2 individually without moving the entire browser state.

// END OF TRANSMISSION
See you, Space Cowboy.

Connection closed. Returning to terminal...