Homebrew's formulae are Ruby files. Zapbrew never evaluates them. Everything it knows about a package comes from the signed JSON catalogs that Homebrew also publishes, and everything it installs is a prebuilt bottle. That single decision shapes the whole design, including what Zapbrew cannot do.
zapbrew-api loads formula.jws.json and cask.jws.json from
HOMEBREW_API_DOMAIN, default https://formulae.brew.sh/api.
These files are JWS envelopes. The payload is verified against Homebrew's built-in RSA public key using PSS with SHA-512 before anything is deserialized. A payload that does not verify is rejected rather than used.
Fetching is conditional. When a usable cache entry exists, the request carries
If-Modified-Since, and a 304 Not Modified response reuses the cached bytes.
HOMEBREW_API_AUTO_UPDATE_SECS bounds how often this happens;
HOMEBREW_NO_AUTO_UPDATE disables it.
A command loads only the catalogs it reads. dispatch.rs classifies each command
with needs_formula and needs_cask, and a command that reads neither gets an
empty catalog instead of a download. Path queries such as --prefix answer
before any of this runs.
The host produces a bottle tag: operating system, architecture, and on macOS the
product version from sw_vers -productVersion. A formula with no bottle for that
tag cannot be installed, because building from source is out of scope.
Dependency resolution walks the catalog graph. Build, test, recommended, and optional edges are filtered according to the requested flags, so a runtime install does not pull build-only dependencies.
install::run resolves the request in stages. A normal install prepares prefix
paths and creates tap and formula lock files before it checks conflicts:
- Refuse Ruby-only modes such as
--build-from-source. - Resolve each requested name to a catalog formula, following aliases.
- Expand dependencies and deduplicate the candidate list.
- Refuse disabled formulae and report deprecated ones.
- Collect affected names, including anything in
conflicts_with. - For a normal install, run prefix setup, take per-tap and per-formula locks, and scan what is installed. Prefix setup includes Linux runtime symlinks where applicable, and the lock files remain after the locks are released.
- Drop candidates that are already satisfied unless
--force. - Check conflicts.
--dry-run takes a non-mutating path at step 6: it scans installed state
without prefix setup or locks. It then prints what would be installed and makes
no changes to the Cellar or prefix. It does not skip the catalog, so a refresh
may still write to the API cache.
Downloads then run concurrently, bounded by
HOMEBREW_DOWNLOAD_CONCURRENCY, and every artifact is checked against the
catalog's checksum before use.
Each formula is poured inside a transaction. The transaction journals the keg-install mutations it makes under the Cellar and prefix, so a failure before commit can be reversed. Downloads and cache writes happen earlier and are not part of it. In order:
| Step | Journal entry that makes it reversible |
|---|---|
| Create the rack directory | created_dirs |
| Create a staging directory | stage_root |
| Unpack the bottle into staging | staging is removable |
| Relocate paths in text, ELF, and Mach-O files | staging is removable |
Write INSTALL_RECEIPT.json |
inside staging |
| Unlink the previously linked keg, if replacing | old_unlinked |
| Rename the old keg aside, if replacing | backup |
| Rename staging into its final keg path | promoted |
| Link the keg into the prefix | new_link_attempted |
| Copy the prefix skeleton | skeleton |
| Run post-install steps | filesystem steps record a steps inverse; run and maintenance steps are executed as processes and record no inverse |
Only after all of that does the journal set committed. Before that point, a
failure triggers rollback, which walks the journal in reverse: undo the
journaled filesystem post-install steps, remove copied skeleton entries, unlink
the new keg, remove the promoted keg, restore the backup, relink the old keg,
remove staging, and remove created directories.
Rollback is honest about failure. Anything it could not undo is collected as leftovers and reported in the error, rather than being silently ignored.
Executed post-install steps are outside this guarantee. A run or
maintenance step is handed to run_command as a process, and no inverse is
recorded for it. If a later step fails, rollback cannot undo what that command
did to the system, and it does not appear in the leftovers either — the journal
has no entry for it. Treat a formula whose post-install plan executes commands as
not fully reversible.
After commit, the backup and step scratch directories are removed. If that cleanup fails, the paths are reported too.
The transaction's destructive steps are confined: paths are checked to fall inside the Cellar or prefix, and a target that is a symlink where a real directory was expected is an error rather than something to follow.
Libraries return typed thiserror errors. anyhow appears only in the CLI entry
point, so no library flattens an error into a string before the CLI decides how
to render it.
For what each crate owns, see Repository layout.