Skip to content

Resilient Installation Patterns

Context: A failure during a machine build should cost one tool, not the rest of the run.

One bad name in a batch installs nothing at all

brew install pkg1 pkg2 ... pkgN validates every formula up front. It aborts the whole command if even one name is unresolvable — a formula in a tap that was never added, for instance. A missing borders tap once silently took out tmux, neovim and every other system package in the same invocation. It surfaced phases later as tmux: command not found, when tpm ran.

The fix is a batch fast-path with a per-package fallback. Attempt the batch, and on failure retry each package on its own, so the culprits are named — Failed to install: borders — instead of reported as "some packages may have failed". The slow per-package cost is paid only once the batch has already failed.

Nothing about this is a brew problem. apt-get install a b c exiting 1 says just as little about which of the three broke, which is why the isolation belongs to every manager rather than to the one script that first needed it. transact in src/dotfiles/registry.py is where it lives, and registering taps runs before the batch for the same reason an unresolvable formula aborts one.

An is-it-installed check freezes everything it guards

An install can also succeed and then never change again. Yazi's install step was guarded on command -v yazi, and yazi was more than its binary — the same step installed flavors and plugins. Once the binary was on disk the step never ran again, so a plugin added afterwards reached no machine that already had yazi. Nothing reported a problem, because the step was up to date.

Any "is it installed" check that guards more than the thing it names has this bug, and the shape outlives whatever runs it. fzf-tmux is a separate file beside fzf, and the binary being current says nothing about whether it is still there. So a binary at the resolved tag is not enough to call a release converged. The release provider's evidence in src/dotfiles/registry.py asks ghrelease.missing_companions as well, and reports a present binary with an absent companion as missing.

Splitting the kinds apart is the stronger version of the lesson. Yazi's plugins are declared in packages.yml under yazi_plugins and cloned by the plugins resource, so a step that installs one kind of thing cannot freeze another.

Never paper over the difference with || echo "Failed (continuing)". That turns a real error into a line of output nobody reads.

Keep structured failure data off both streams

Which stream an error lands on is the failing tool's choice, not the installer's. Capturing one stream for the record and letting the other through therefore loses causes at random. A failure is a returned value here rather than parsed text, so stdout and stderr stay free to be merged and teed, live and whole. The isolation that makes this work sits in act and measure in src/dotfiles/engine.py — one place, rather than one call site per installer — and dotfiles report latest is where a failed install is read.