On Gentoo, fd-find / fd is in sys-apps/fd (similar to eza being in
sys-apps/eza). Both are modern rust-based CLI tools in the sys-apps
category. The template used 'app-misc/fd' which doesn't exist on
gentoo's main tree; emerge failed with 'no ebuilds to satisfy'.
eza lives in sys-apps/ on Gentoo, not app-misc/. The previous
template used 'app-misc/eza' which made emerge fail with:
!!! Error: 'app-misc/eza' not found
Also updated the USE flag block (sys-apps/eza git instead of
app-misc/eza git) so the file written to /etc/portage/package.use
is correct.
Three issues caught during tadbit onboarding:
1. run_once_00-install-bootstrap-tools.sh.tmpl (gentoo branch):
'gnupg' is ambiguous (app-crypt/gnupg vs app-vim/gnupg). Use full
category/package names: app-crypt/gnupg, app-crypt/age, etc.
The previous 'for p in age curl ...' loop fed short names to
emerge which printed '!!! The short ebuild name gnupg is
ambiguous' and exited 1.
2. run_once_20-install-user-packages-gentoo.sh.tmpl:
sys-devel/base-devel doesn't exist on Gentoo (it's an Arch/Fedora
concept; Gentoo's toolchain is the @system set which is always
installed). Removed base-devel from the package list.
3. Universal scripts (run_once_20 + run_once_40) were running on
gentoo and hitting either 'die unsupported os_family' (universal
20) or 'WARNING sway packages not configured for gentoo' (universal
40). Added early-return: 'if os_family == gentoo, exit 0' at the
top of each universal script so the gentoo-specific scripts
handle the box. (Same pattern the chaotic-aur script already had.)
4. run_once_10-add-gentoo-overlays.sh.tmpl:
The cached ~/.cache/eselect-repo/repositories.xml was corrupt on
tadbit (lxml.etree.XMLSyntaxError on every 'eselect repository
list' call). Added a sanity check: parse the XML with
xml.etree.ElementTree, delete if invalid, re-fetch.
After these fixes, the bootstrap on tadbit is expected to run cleanly
with --keep-going (the four failures above all become no-ops).
Tadbit is the 7th machine in the homelab. Generated a per-machine
age key on tadbit, added the pubkey as 8th recipient in
.chezmoi.yaml.tmpl, and re-encrypted the two .age secrets with
all 8 recipients (1 recovery + 7 machines).
Tadbit (tadbit-gentoo, x86_64, gcc 15.2.1, TKG kernel) joins the
homelab as the 7th machine — the first gentoo box. Adds Gentoo
support to the bootstrap alongside arch and debian.
New scripts:
- run_once_10-add-gentoo-overlays.sh.tmpl: enables GURU overlay
via 'eselect repository enable guru' + 'emaint sync -r guru'.
Idempotent — skips if GURU is already at /var/db/repos/guru.
GURU is required because eza, lazygit, topgrade, and most modern
CLI tools only live in GURU (not main).
- run_once_20-install-user-packages-gentoo.sh.tmpl: emerge-based
user package set. Writes USE flags to package.use/ BEFORE
emerging so foot/wofi/waybar get the right features. Detects
missing packages via 'command -v <basename>'. Falls back to
the official curl installer for bun (no gentoo package).
- run_once_40-install-sway-gentoo.sh.tmpl: sway stack via emerge
with USE flags for X+wayland+tray+upower+wireplumber.
Updated scripts:
- .chezmoi.yaml.tmpl: os_family detection now also matches 'gentoo'.
Critical fix: Gentoo's /etc/os-release uses single-quoted values
('gentoo' not 'gentoo' or "gentoo"), and chezmoi's parser doesn't
strip them. Without trimAll "'", .chezmoi.osRelease.id returns
the literal string 'gentoo' with quotes, and the eq test fails.
Symptom: os_family silently becomes 'unknown'.
- run_once_00-install-bootstrap-tools.sh.tmpl: added gentoo branch
that uses emerge --sync + emerge (skipping if tree is < 1 day old).
- run_onchange_30-ensure-cargo.sh.tmpl: added gentoo branch for
bat (already installed by emerge, just verify), topgrade (GURU),
cargo-update (dev-util/cargo-update in main).
README: documented Gentoo-specific quirks (USE flags, GURU,
single-quote parsing, no binary packages).
ROCm installs to /opt/rocm (not /opt/rocm-X.Y versioning). The
upstream /etc/profile.d/rocm.sh tries to add /opt/rocm/bin to PATH
but uses 'append_path' which is a fish-shell function, not a bash/zsh
built-in — so on a zsh login the export is a no-op and the rocm
tools (amd-smi, rocm-smi, rocminfo, hipcc, etc.) are unreachable
without explicit PATH setup.
Add /opt/rocm/bin to the top of the path array in dot_zshrc.tmpl
when the directory exists on the host. Use stat() with the
not-(not-...) bool coercion (pitfall #35 in the chezmoi-bootstrap-
runbook skill) to avoid rendering the stat map's string repr.
Conditional on the directory existing so Pis (no ROCm) don't get a
dead PATH entry. Verified on miche:
PATH=/usr/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/bin/site_perl: /usr/bin/vendor_perl:/usr/bin/core_perl:/opt/rocm/bin
/opt/rocm/bin/amd-smi
/opt/rocm/bin/rocm-smi
/opt/rocm/bin/rocminfo
amd-smi version: AMDSMI Tool: 26.4.0+3309c6114a | ROCm version: 7.13.0
Per user preference (memory 2026-06-22): PMs first (pacman via
chaotic-aur on arch, apt on debian), curl-install only as last-resort
fallback. bun is in [extra] on arch since 1.2.x, so the pacman
package is the right install path.
Three changes to run_once_20-install-user-packages.sh.tmpl:
1. Arch missing-detect: for the bun entry, check Installed From : extra
Name : bun
Version : 1.3.14-1
Description : Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
Architecture : x86_64
URL : https://github.com/oven-sh/bun
Licenses : MIT
Groups : None
Provides : None
Depends On : glibc icu
Optional Deps : None
Required By : None
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 66.08 MiB
Packager : Sven-Hendrik Haase <svenstaro@archlinux.org>
Build Date : Sun 17 May 2026 09:29:39 AM EDT
Install Date : Sun 14 Jun 2026 12:05:09 AM EDT
Install Reason : Explicitly installed
Install Script : No
Validated By : Signature
(system-package tracking) instead of /usr/bin/bun (binary on
PATH). A curl-installed bun at ~/.bun/bin/bun or ~/.local/bin/bun
would otherwise pass the command check, leaving us with a
non-PM-managed install that topgrade wouldn't see.
2. Post-install verification on arch: assert that Installed From : extra
Name : bun
Version : 1.3.14-1
Description : Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
Architecture : x86_64
URL : https://github.com/oven-sh/bun
Licenses : MIT
Groups : None
Provides : None
Depends On : glibc icu
Optional Deps : None
Required By : None
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 66.08 MiB
Packager : Sven-Hendrik Haase <svenstaro@archlinux.org>
Build Date : Sun 17 May 2026 09:29:39 AM EDT
Install Date : Sun 14 Jun 2026 12:05:09 AM EDT
Install Reason : Explicitly installed
Install Script : No
Validated By : Signature
succeeds and report the installed version + repo. Fails loudly
if bun is not tracked by pacman (curl shadow or failed install)
so the operator can clean up and re-run.
3. Same post-install verification on debian: assert that
/usr/bin/bun succeeds after the curl-install, and report
the version. On debian bun is not in apt, so curl is the
last-resort install path. Verify that the resulting binary is
reachable on PATH.
PATH order: in run_once_20, /usr/bin and /bin are now first so any
pacman-managed bun wins over a curl-installed shadow at ~/.bun/bin
or ~/.local/bin. ~/.local/bin, ~/.bun/bin, ~/.cargo/bin still come
next so omp (installed via bun to ~/.bun/bin) is also reachable.
Triggered a re-run of the script on each box to confirm the
verification line shows up: 'bun: 1.3.14-1 (from extra) —
system-package install verified'.
The script runs from a non-interactive context (chezmoi apply over
SSH) where .zshrc isn't sourced. bun installs to ~/.bun/bin and
omp/cargo to ~/.local/bin / ~/.cargo/bin, none of which are on the
script's default PATH. Add them at the top so the post-install
'omp --version' and 'command -v omp' checks work.
Fixes the spurious 'omp: command not found' right after a successful
bun add -g on boxes where omp is freshly installed.
Three real bugs caught during the bit-cachyos deploy:
1. wl-clipboard false-positive: 'command -v wl-clipboard' returns
nothing because the package ships wl-copy/wl-paste, not a
'wl-clipboard' binary. Sudo pacman was being called every apply
even though wl-clipboard is installed. Fix: declare -A PKG_BIN
map in the script.
2. omp segfault aborts the whole bootstrap: on rye, 'bun add -g
@oh-my-pi/pi-coding-agent' segfaults (Pi undervoltage, see
pitfall #30). The 'set -e' caused the whole run_once_20 to abort
before getting to neovim/oh-my-zsh/zshrc etc. Fix: wrap the bun
add in a subshell with error tolerance, log a warning, keep going.
3. rorclar/sourdough reference cleanup: removed the old chaotic-aur
comment which mentioned the wrong sudo pattern (now fixed in the
other run_once scripts).
Re-applied on bit, miche, byte, kaiser, crouton (rye skipped because
of the undervoltage hardware issue).
Several run_once scripts unconditionally called sudo pacman/apt to
install packages — even on boxes where every package was already
present. That triggered a sudo password prompt on every fresh
chezmoi apply for nothing.
Two changes:
1. .chezmoi.yaml.tmpl: fall back to ~/.local/bin/age if /usr/bin/age
isn't installed (matters during initial bootstrap before age is
installed system-wide).
2. run_once_*.sh.tmpl: detect missing packages first; only call sudo
if there's actually something to install. For the LAN hosts script,
detect the existing block and skip if it's already correct.
These changes are transparent on boxes that already had everything
installed (the existing 5): no behavior change. They reduce sudo
prompts on bit (the new box, where most packages are pre-installed)
from ~5 prompts to 1 (just for /etc/hosts).
bit-cachyos is the 6th machine in the hive. Generated a per-machine
age key on bit, added the pubkey to the recipients list in
.chezmoi.yaml.tmpl, and re-encrypted the two .age secrets
(dot_omp/agent/encrypted_.env.age and encrypted_zai.key.age) with all
7 recipients (1 recovery + 6 machines).
Bit's existing partial setup (pre-existing chezmoi source dir, omp
native binary at ~/.local/bin/omp) is backed up during the bootstrap
script to ~/.local/share/chezmoi.bak.<timestamp>.
See onboard-bit.sh on bit:/tmp/onboard-bit.sh for the no-sudo
bootstrap flow.
Replaces the implicit hostname-based enable (miche/byte/kaiser got
sway automatically) with an explicit promptBool asked at first init.
New flow:
1. `chezmoi init` asks: "sway_setup? [y/N]"
2. User says y or N (default N)
3. Answer is captured in data.sway_setup
4. run_once_40-install-sway.sh.tmpl gates on .sway_setup
5. Per-box override via marker files (preserved):
- ~/.config/chezmoi/features/sway → force ON
- ~/.config/chezmoi/features/no-sway → force OFF
Migration for existing boxes:
- Miche/byte/kaiser already have ~/.config/chezmoi/features/sway
marker (from previous hostname-allowlist install) → sway_setup=true
- Rye/crouton have no marker → sway_setup=false (default)
- Both groups pick up the new template on next chezmoi apply
The main `~/.config/sway/config` is intentionally single-file (per
its own header comment: 'MINIMAL — single file, no includes. Tier-1+2+3
in one go.'). The `config.d/*.conf` files were leftovers from an
earlier split and are NOT included by the main config — so they were
dead weight in the repo.
Sway is NOT auto-launched. It's installed as a tool the user runs
manually from a TTY (`sway` from TTY1) for light desktops. The install
and configs sit ready; launching is the user's call.
The previous `has .chezmoi.hostname ` only matched exact
strings. byte's hostname is `byte-arch` (set by the CachyOS installer),
so it didn't match the `byte` entry in the allowlist and got sway=false.
New logic: hostname matches if it equals any entry OR starts with
'<entry>-' or '<entry>.'. So 'byte', 'byte-arch', and 'byte.foo' all
match the 'byte' entry.
New opt-in feature for x86_64 desktops: sway + wofi + foot + swaybg +
swaylock + swayidle + grim + slurp + waybar + wl-clipboard. mako on
arch, dunst on debian (mako isn't packaged for debian).
Files:
- .chezmoi.yaml.tmpl: added data.sway flag (true on miche/byte/kaiser,
false on Pis). Override per host with ~/.config/chezmoi/features/sway
or ~/.config/chezmoi/features/no-sway marker files.
- run_once_40-install-sway.sh.tmpl: installs packages if .sway=true,
exits 0 otherwise. Sets up the marker file.
- dot_config/{sway,foot,wofi,waybar,mako}/: existing configs from miche.
Per-host toggle workflow:
# On any box, enable sway:
touch ~/.config/chezmoi/features/sway
chezmoi apply
# On a sway-enabled box, disable it:
touch ~/.config/chezmoi/features/no-sway
rm ~/.config/chezmoi/features/sway
chezmoi apply
Currently sway packages are already installed on miche (existed before
this commit). Byte will get them via the new run_once_40 script.
Pis (rye, crouton) are unaffected — install script early-returns.
Many Rust crates (cargo-update, anything depending on openssl-sys)
need libssl-dev + pkg-config at build time. Without these,
`cargo install cargo-update` fails on debian with:
Could not find openssl via pkg-config
The system library `openssl` required by crate `openssl-sys`
was not found.
Add them to APT_PKGS in run_once_00-install-bootstrap-tools.sh.tmpl
so new debian boxes have them from the start. Existing Pis
(rye, crouton) need a one-time `sudo apt-get install -y libssl-dev
pkg-config` after pulling this commit.
1. dot_omp/agent/encrypted_.env.age (NEW)
Encrypted shell-sourceable file with all omp provider API keys.
Decrypts to ~/.omp/agent/.env on apply. omp reads .env on startup
per docs/environment-variables.md. All 6 recipients (recovery +
5 boxes) can decrypt. Placeholder values for keys the user hasn't
added yet — fill in real values per-provider.
2. run_onchange_30-ensure-cargo.sh.tmpl (UPDATED)
- Add topgrade install: pacman on arch (via chaotic-aur), cargo on
debian (not in apt)
- Add cargo-update install: pacman on arch, cargo on debian
- Prefer OS package managers over cargo install when both are
available. cargo install only as fallback.
3. dot_omp/agent/config.yml (UNCHANGED)
Per user request: keep .local host endpoints (llama-swap.miche,
kaiser.local:8800). If a box can't reach them, it's not on the
local network and omp will error gracefully at request time.
The previous approach (private_dot_omp/agent/zai.key.age + manual
re-encryption) didn't work because:
1. The 'private_' prefix is for files NOT to push to remote, not for
encrypted files. The 'encrypted_' prefix is what chezmoi recognizes
as an encryption marker.
2. The encrypted file needs to be at dot_<path>/encrypted_<name>.age
so chezmoi can both decrypt on apply AND strip the .age suffix
to write the destination file as <name> (without .age).
Also fix chezmoi age config to actually decrypt non-interactively:
- Add useBuiltinAge: false to force external age binary
- Add age.command: /usr/bin/age (absolute path) so PATH issues
don't matter in non-interactive SSH contexts
The encrypted file is at dot_omp/agent/encrypted_zai.key.age, decrypts
to ~/.omp/agent/zai.key on apply. Encrypted to all 6 recipients
(recovery + miche + byte + kaiser + rye + crouton).
Tested on miche:
- chezmoi apply: rc=0
- live zai.key: 50 bytes (correct content)
- decrypts with miche per-machine key
- would decrypt on other boxes with their respective keys
Each box now has its own per-machine age key at
~/.config/chezmoi/key.txt. The .age file is encrypted to all 6
recipients, so any of them can decrypt zai.key on next chezmoi apply.
Implementation note: chezmoi only honors the LAST --age-recipient
flag when given multiple. Use --age-recipient-file=path/to/file
(one pubkey per line) for multiple recipients in a single call.
Three parts:
1. .chezmoi.yaml.tmpl: reworked age config block
- recipients moved under 'age:' key (correct structure per chezmoi docs)
- identity: ~/.config/chezmoi/key.txt
- recipients list with recovery key + miche per-machine key
- recovery key pubkey: age1yyq42ctqwp5s5yd64week3aav9getk3p8aeyr5n5454d0v59a4dsjljsgs
- miche pubkey: age1eja7trs8mmsgf0qga0h5fsdltaryxgk4ksumshar5xxtdx0exy3q0a5hc5
- placeholders for byte/kaiser/rye/crouton (TODO: generate per-box keys
and add when bootstrapping those boxes)
2. private_dot_omp/agent/: omp/oh-my-pi config from byte
- config.yml (1.7KB) — model roles, fallback chains, theme, tools
- mcp.json (351B) — firecrawl MCP server config
- zai.key.age (540B) — zai-coding provider API key, age-encrypted to
recovery + miche recipients. Decrypts to live ~/.omp/agent/zai.key
on apply.
3. run_once_20: install bun + pi-coding-agent on both OSes
- arch: bun from pacman (now in [extra])
- debian: bun via curl-install to ~/.local (not in apt)
- both: bun add -g @oh-my-pi/pi-coding-agent → omp binary in ~/.bun/bin
- .zshrc.tmpl already adds ~/.bun/bin to PATH
To onboard a new box:
1. ssh into the box
2. age-keygen -o ~/.config/chezmoi/key.txt
3. paste the public key into .chezmoi.yaml.tmpl recipients
4. chezmoi age rekey # rewrites *.age files to include new recipient
5. commit + push
6. chezmoi init --apply # decrypts and writes zai.key live
paru's post-build 'sudo pacman -U' step requires a TTY for sudo,
which fails in non-interactive chezmoi runs (miche doesn't have
NOPASSWD sudo for this case). The font package has no deps, so
the AUR build step adds complexity for no real benefit.
Download MappleMono-NF.zip from subframe7536/Maple-font v7.9 GitHub
release directly on both arch and debian. Same install path, no
paru dependency, no sudo, ~30 second install.
The maplemono-nf-cn AUR package is still available for users who
want it via system package management — this just makes the bootstrap
not depend on AUR helpers working non-interactively.
The bootstrap script has 'set -euo pipefail' at the top. The font
check was 'fc-list 2>/dev/null | grep -qi "Maple Mono NF"'. With
pipefail enabled, this fails:
1. grep -q exits 0 as soon as it finds the first match
2. grep closing its stdin early sends SIGPIPE to fc-list
3. fc-list exits with 141 (SIGPIPE)
4. With pipefail, the pipeline returns 141, not 0
5. The 'if' evaluates 141 as false → NO-MATCH
6. Bootstrap re-installs the font even though it's already there
Discovered on kaiser: bootstrap kept failing at 'paru -S maplemono-nf-cn'
because the font was already installed but the check said it wasn't.
Fix: use bash string matching instead of a pipeline.
[[ "$(fc-list 2>/dev/null)" == *"Maple Mono NF"* ]]
This reads all output into a string (no pipe), then bash's built-in
glob match handles the test. No pipefail issue.
Kaiser bootstrap failed at 'paru -S maplemono-nf-cn' with
'sudo: a terminal is required to read the password'. paru doesn't
need sudo for the build itself (it builds as the user), but it does
call sudo internally for some package operations — and on kaiser
the sudoers config requires a TTY password for those calls.
--sudoloop runs sudo in the background, which avoids the TTY-prompt
issue entirely. The package still builds as the user; only the
internal sudo calls go through the background loop.
Kaiser's existing .zshrc had fzf-tab added to the plugins list —
it gives a navigable Tab completion menu with descriptions for each
match. Adding to the canonical plugin list so kaiser doesn't lose
functionality when the repo version overwrites the box-specific
one. Strictly additive: doesn't affect miche or other boxes.
Adds:
- fzf-tab to dot_zshrc.tmpl plugins array
- install_zsh_plugin Aloxaf/fzf-tab to run_once_20
run_once_20 runs BEFORE run_onchange_30 in the bootstrap chain, so
'command -v cargo' inside run_once_20 was always false on a fresh
box — cargo install bat was skipped, leaving bat missing on debian.
Move the bat install to run_onchange_30 (which runs last, after
rustup is installed). Restructure the script to:
1. Ensure cargo is installed (existing logic)
2. Install bat via cargo on debian only (new logic, gated by os_family)
This way the bootstrap chain becomes:
run_once_00 -> run_once_10 -> run_once_20 (apt packages, neovim, oh-my-zsh, font)
-> run_onchange_30 (rustup, then bat from crates.io)
Crouton currently has rustup installed but no bat (cargo install
in progress in background). Re-running chezmoi init will skip
run_once_20 (state recorded) and re-run run_onchange_30 (content
changed), which will see bat missing and trigger cargo install
automatically.
chsh asks for the user's password by default (PAM authentication).
In an interactive SSH session this works fine; in a non-interactive
chezmoi run it fails with 'PAM: Authentication failure' because
there's no TTY to prompt.
sudo chsh works because the script has already cached sudo
credentials via earlier 'sudo apt-get install' / 'sudo pacman'
calls in the same script run.
Crouton bootstrap was failing here after font install + neovim +
oh-my-zsh all succeeded — wasted ~10 minutes of bootstrap just for
the chsh step.
Discovered on crouton bootstrap: a fresh debian install may not
have fontconfig installed, so fc-cache is missing. The script's
font install step crashed at fc-cache, aborting the rest of the
bootstrap (cargo install bat never ran).
Fixes:
1. Add fontconfig to APT_PKGS so fc-cache and fc-list are present
on debian from the start (matches what's typically pre-installed
on most desktop distros but not on minimal server installs)
2. Wrap fc-cache in 'command -v' guard so if the package somehow
isn't installed, the script logs a warning and continues instead
of aborting the whole chain
Debian's 'bat' package is renamed 'batcat' to avoid clashing with an
unrelated Debian package. The rename makes .zshrc's 'alias cat=bat'
fail.
Install upstream bat via cargo instead — gets the real binary at
$HOME/.cargo/bin/bat, version-aligned with arch's pacman install.
Drop 'bat' from the debian apt install list (no more batcat conflict
to work around).
Same as fdfind -> fd. Debian renames the upstream 'bat' binary to
'batcat' because there's a different unrelated 'bat' package in the
distro. .zshrc aliases 'cat=bat' so without the symlink, the alias
fails on debian.
The tarball filename is nvim-linux-arm64.tar.gz but the extracted
directory inside it is also named nvim-linux-arm64. However, my
original code did $(basename nvim.tar.gz .tar.gz) which returns 'nvim'
(strips both the directory and the suffix), creating a symlink to
/opt/nvim/bin/nvim that pointed to a non-existent path.
Discovered on rye after the bootstrap appeared to succeed but nvim
wasn't findable. Fixed by hardcoding the extracted directory name
based on the arch case:
x86_64: nvim-linux64.tar.gz -> nvim-linux64
aarch64: nvim-linux-arm64.tar.gz -> nvim-linux-arm64
Same fix applied to:
- run_once_20-install-user-packages.sh.tmpl (initial install)
- dot_local/bin/update-neovim.sh (topgrade-time updates)
Verified on rye: /usr/local/bin/nvim -> /opt/nvim-linux-arm64/bin/nvim,
'nvim --version' returns 'NVIM v0.11.4'.
Pacstall tried to BUILD neovim from source (downloaded the v0.12.2
tarball and ran the build chain). On a Pi this is 5+ minutes plus
fragile — pacstall's connection broke during download.
Switch to direct binary tarball install:
1. Pinned to NVIM_TARGET_VERSION='v0.11.4' in two places:
- run_once_20-install-user-packages.sh.tmpl (initial install)
- dot_local/bin/update-neovim.sh (topgrade-time updates)
2. Both use the same install logic: detect arch via uname -m, download
the right tarball (nvim-linux-arm64.tar.gz for aarch64), extract
to /opt, symlink /usr/local/bin/nvim. Idempotent — if installed
version == target, no-op.
3. Topgrade config has a [commands] entry that runs the update script
after system updates. To upgrade neovim across all boxes: edit
NVIM_TARGET_VERSION in dot_local/bin/update-neovim.sh, commit,
push, run topgrade.
4. Removed run_once_05-install-pacstall.sh.tmpl entirely — pacstall
isn't worth the install footprint for one package.
The pacstall installer has its own dep install logic that handles edge
cases (e.g. debian-trixie dropped spdx-licenses from apt, so the
installer has a fallback to fetch the .deb directly from
ftp.debian.org and install via 'apt install /path/to/deb'). My script
was duplicating that logic with a strict apt-get install that failed
because spdx-licenses isn't in trixie apt.
Fix: install only the absolute minimum (curl, wget, ca-certificates,
sudo) so the installer can fetch and verify its own deps. Trust the
installer.
Three changes:
1. NEW run_once_05-install-pacstall.sh.tmpl (debian-only)
Installs pacstall via its official installer. Pacstall is an
AUR-like package manager for debian/ubuntu, with neovim at
0.12.2-1 (current as of bootstrap). The installer requires
root and prompts for 'install axel?', so we run it under
sudo with NON_INTERACTIVE=true and stdin redirected from /dev/null.
2. UPDATE run_once_20-install-user-packages.sh.tmpl
On debian, prefer pacstall over the GitHub tarball when
pacstall is available. The tarball fallback remains for the
case where pacstall install failed or isn't wanted.
3. NEW dot_config/topgrade/topgrade.toml
Topgrade's built-in pacstall step auto-detects pacstall and
runs 'pacstall -U -Up' (update repo + upgrade packages).
Built-in chezmoi step also auto-detects chezmoi. So our
topgrade config just sets pre_sudo=true for password caching
and ignore_failures for node.
Two bugs hit on rye:
1. neovim tarball URL hardcoded to nvim-linux64.tar.gz (x86_64 only).
On aarch64 boxes (rye is arm64), curl would 404 the tarball, the
unpack would create a binary that won't run, or apt's bundled
neovim might not even be installed.
2. The 'apt's neovim is too old' branch only ran if apt had
successfully installed an old neovim. If apt didn't install
neovim at all (or installed a recent enough version), the
tarball fallback never triggered. On rye, the script reached
the final 'nvim --version' verification line and crashed with
'command not found'.
Fix:
- Detect arch via uname -m, map to correct tarball name
(x86_64 -> nvim-linux64.tar.gz, aarch64 -> nvim-linux-arm64.tar.gz)
- If command -v nvim returns false at all, skip the version check
entirely and go straight to the GitHub tarball
- If apt's neovim IS recent enough (>= 0.9), keep it and skip the
tarball
- Final 'nvim --version' verification preceded by a PATH-ensure
for /usr/local/bin in case the freshly-installed tarball binary
isn't yet on PATH for this script's environment
Verified template renders cleanly on arch.
Script claimed to be arch-only in its comment but had no actual guard.
The body always ran, so on debian it tried pacman-key (which doesn't
exist), failed with 'command not found', and aborted the whole bootstrap
chain (run_once_20 and run_onchange_30 never executed).
Fixes:
1. Wrap entire body in {{ if eq .os_family "arch" }} ... {{ end }} so
the script is a no-op on debian (logs a skip message instead of dying)
2. Prepend sudo to pacman-key, pacman -U, pacman -Syu, pacman -S, and
grep /etc/pacman.conf — same user-vs-root pattern that bit run_once_00
chezmoi runs scripts as the invoking user, not root. run_once_00 was
calling apt-get/pacman directly, which fails on debian with
'Permission denied' on /var/lib/apt/lists/lock and on arch with
similar pacman lock errors. Same pattern was already correct in
run_once_20. Mirror that here.
This is the bug that blocked rye on the second attempt.
chezmoi runs run_once_* scripts as the invoking user (uid != 0).
The earlier check [[ $(id -u) -ne 0 ]] && die ... killed the script
immediately when invoked via 'chezmoi apply' or 'chezmoi init --apply'
from a normal user session.
The scripts use sudo internally for package operations (pacman/apt),
so elevation happens correctly. The id -u check was wrong: it belongs
in a script that's *meant* to be invoked as root directly, not in a
chezmoi-managed script.
debian-stable's /etc/os-release has no ID_LIKE field. Template crashed
with 'map has no entry for key idLike' when chezmoi init ran on rye.
Two fixes:
1. hasKey() guard around .chezmoi.osRelease.idLike so missing key
doesn't error out
2. Flip contains() arg order: sprig's signature is contains(substr, str),
not contains(str, substr). Was checking backwards.
Tested against:
- miche (ID=debian-derivative with ID_LIKE=arch) -> os_family=arch
- empty ID_LIKE fallback (debian-stable) -> falls through to .id=debian
-> os_family=debian
Anonymous read is enabled on the forge, so a freshly-installed box can
clone + init without needing SSH keys pre-configured. SSH stays as the
push URL on the main workstation.
- Arch: paru -S maplemono-nf-cn (AUR package, installed via Chaotic-AUR)
- Debian: download MapleMono-NF.zip from subframe7536/Maple-font v7.9
release, extract to ~/.local/share/fonts, run fc-cache
- Idempotent: skips if fc-list already shows Maple Mono NF
- Pinned to v7.9 (20.6MB); bump MAPLE_FONT_VERSION when upgrading
Also documented in README that the default Maple Mono NF in nvim
init.lua will Just Work on every box thanks to the bootstrap.