Library/Homebrew/plans/relocatable-bottles.md
Every Homebrew/core bottle pours at every prefix under 65 characters long.
/usr/local bottling
until EOL and is out of scope.cellar :any nor cellar :any_skip_relocation. The
padded-prefix scheme below handles most of them; the residual classes in
"Completeness" each get their own strategy so that none is left behind
silently. Exceptions must be named, justified in the formula and counted.This is a living document: edit or remove steps as they are implemented so it always describes the remaining work.
End each implementation commit body with this exact line:
This change is part of [`plans/relocatable-bottles.md`](https://github.com/Homebrew/brew/blob/HEAD/Library/Homebrew/plans/relocatable-bottles.md)
Any benchmark quoted in a commit message must be the full hyperfine
output from brew benchmark (using its --exec mode for bespoke
workloads), never hand-summarised numbers.
Pull requests must always fill in the repository's pull request
template (.github/PULL_REQUEST_TEMPLATE.md), never bypass it
(e.g. with gh pr create --fill).
| Tag | :any_skip_relocation | :any | pinned |
|---|---|---|---|
arm64_tahoe (8,369 bottles) | 4,598 (54.9%) | 2,745 (32.8%) | 1,026 (12.3%) |
x86_64_linux (8,401 bottles) | 4,891 (58.2%) | 2,341 (27.9%) | 1,169 (13.9%) |
The 1,121 :all bottles are counted in :any_skip_relocation: they are all
marked that way and apply on every platform. 1,328 formulae carry a pinned
bottle on at least one tag and 892 are pinned on both arm64_tahoe and
x86_64_linux, so causes are mostly inherent rather than
platform-specific. Six formulae still gate pouring on
pour_bottle? only_if: :default_prefix.
Per-bottle rates understate the problem, because an install only avoids
source builds when the formula's entire recursive runtime dependency closure
pours. On arm64_tahoe, although 87.7% of bottles are individually
relocatable, only 56.0% of formulae (4,688 of 8,369) have a fully
relocatable closure; 43.9% contain at least one pinned bottle. A few hub
formulae do most of the poisoning: openssl@3 alone appears in the closure
of 2,152 formulae, followed by gettext (1,325), glib (963),
[email protected] (867), libx11 (855) and fontconfig (831). Fixing only
those six would lift closure coverage to 70.9%, and the top twenty to
76.4%, which is why per-formula fixes alone plateau and the value of this
plan is convex: it concentrates near 100%, where whole dependency trees
become pourable, and every pinned formula left continues to poison its
entire dependent subtree.
Bottles are stored with @@HOMEBREW_PREFIX@@-style placeholders in text
files, libtool files, Mach-O load commands and ELF RPATHs/interpreters
(keg_relocate.rb, extend/os/{mac,linux}/keg_relocate.rb). After
placeholdering, brew bottle scans the keg (dev-cmd/bottle.rb); any
surviving prefix/cellar/library reference or absolute symlink into the prefix
pins the bottle to its build cellar. Surviving references are almost always
raw C strings compiled into binaries, because text replacement deliberately
touches only text files.
Raw C strings are the only length-limited content at pour time:
-headerpad_max_install_names (shims/super/cc), and install-name edits
happen in-process via ruby-macho.Keg#relocate_build_prefix (keg_relocate.rb) implements that NUL-padded
patching and re-signs patched Mach-O files, and
BottleSpecification#compatible_locations? allows pouring a pinned bottle
into an equal-or-shorter prefix, both gated behind the undocumented
HOMEBREW_RELOCATE_BUILD_PREFIX environment variable. History: added
default-on in #12534 (December 2021), reverted twice, then gated behind the
variable in #13217 after #13209 reported hard install failures at a prefix
longer than the bottled one. It is referenced nowhere in homebrew/core,
homebrew-test-bot, homebrew/actions or homebrew/install today.
Prior art: conda-forge builds packages under a long placeholder prefix (up
to 255 characters) and rewrites it at install time, including NUL-padded
replacement inside binaries, and Spack's padded_length configuration pads
install tree paths for the same reason so its build caches relocate. Both
validate the padded-prefix approach at scale and are useful references for
edge cases (e.g. prefixes inside length-prefixed data).
Replaying the exact keg_contain? logic over the contents of 17 pinned and
16 relocatable published bottles:
wget2's localedir, graphviz's
plugin directory, [email protected]'s framework prefix..dynstr bytes: Meson's install-time RPATH rewrite
overwrites the build RPATH without clearing the rest of the old string
(intentional upstream; Nix carries a patch), and patchelf leaves the old
table behind when growing one, so the checker counts the corpse
(harfbuzz, shared-mime-info; the 224-formula glib/gobject
introspection cluster on Linux looks identical). Functionally these are
false positives, since addressed by scanning ELF files by structure
(design decision 11)..o artefacts (about 29 npm
formulae).abseil replays clean under the current checker yet CI pinned it:
since explained and fixed (phantom resolved-linkage matches, Phase 1
item 4). A bottle's cellar lives in the formula, not the tarball, so
provably clean bottles can be re-marked cellar :any without
rebuilding.brew test at the padded
prefix, where Unix socket paths (sun_path is 104 bytes) and shebang
limits punish very long prefixes.:any and :any_skip_relocation bottles
(84 to 88% of the catalogue) are placeholder-based and prefix-independent,
so they pour exactly as today. Only pinned bottles start patching (1 to 2
files typically) plus re-signing at pour. Re-signing preserves
entitlements, requirements, flags and runtime metadata
(codesign_patched_binary passes --preserve-metadata).built_prefix in its tab; absence means the tag's default prefix, so
tabs for today's bottles are unchanged. The literal string, not a
name/version pair, so the constant can change without a mapping.HOMEBREW_RELOCATE_BUILD_PREFIX is added to env_config.rb with
hidden: true until the feature is hardened and diagnosable, and is
ultimately retired in favour of a HOMEBREW_NO_RELOCATE_BUILD_PREFIX
escape hatch.strings-based checker, which counted every prefix byte
sequence in a file as a pin: a wrongly pinned bottle forces source
builds for every non-default-prefix user and poisons its whole
dependent subtree, whereas a wrongly relocatable one surfaces as a
per-formula bug report with a trivial fix and is caught by the Phase 2
validation sweep and the test-bot relocated-pour test. Concretely, ELF
files are scanned by structure rather than as a whole: only the
interpreter the loader uses, the dynamic strings the loader references
and the contents of ordinary sections count; bytes outside every
section and unreferenced entries in loader-owned string tables never
do. Other file types keep the whole-file scan.Each pinned bottle flipped to cellar :any pours at any prefix with no
length limit and no new machinery.
abseil class was pinned
by file_linked_libraries resolving @rpath/@loader_path load
commands against the live keg at bottle time, turning relocatable
linkage into absolute build-prefix paths; the checker now reads raw
load-command names. Text matches never recorded it because googletest
include-path strings hit the ignores filters, which also explains
why only arm64 macOS pinned: /usr/local/include/... never byte-matched
Intel's /usr/local/opt and /usr/local/Cellar search strings and
Linux has no linkage check. Remaining: once the fix is deployed to CI,
batch re-mark provably clean pins cellar :any with no rebuild
(sha256 unchanged) in homebrew/core, starting from the 127 formulae
pinned on all arm64 macOS tags yet :any on x86_64_linux (e.g.
abseil, boost, binutils, aws-sdk-cpp).Whole dependency closures become pourable at prefixes up to the bottled
default length (13 bytes arm64 macOS, 26 Linux), covering the hub formulae
(openssl@3, gettext, glib, [email protected]) that Phase 1 cannot.
Keg#relocate_build_prefix:
multi-occurrence strings, several strings per file, hardlinks, the
sharball skip, the size-mismatch failure path, codesign on macOS and
no-op on Linux, suffix-merged string tables and prefix strings inside
load commands.formula_installer.rb) compares
prefix lengths and states the actionable cause ("prefix 8 characters too
long to patch, building from source" versus "patchable: enable
relocation").HOMEBREW_RELOCATE_BUILD_PREFIX to env_config.rb with
hidden: true so it is typed and testable but not yet public.brew linkage and smoke tests.built_prefix differs from the local prefix and
fits. Patching is unconditional once a bottle is padded-built, because
the default prefix is then just another shorter prefix; the hidden
variable covers only legacy short-built pinned bottles at custom
prefixes until they churn out.built_prefix.openssl@3, gettext, glib, [email protected], libx11
and fontconfig first), then remaining pinned formulae: old pinned
bottles cannot pour into the padded CI prefix, so they force CI
source-build fallbacks until rebottled, whereas old relocatable bottles
pour there immediately (placeholder relocation has no length limit).
Old pinned bottles keep pouring unchanged at default prefixes
throughout.HOMEBREW_NO_RELOCATE_BUILD_PREFIX; docs/Bottles.md,
docs/Installation.md and homebrew/install messaging state the single
global contract.Independent of the phases above and prioritised by poisoned dependent
count, because these help prefixes beyond 64 bytes and remove formulae from
the patching path entirely: prefer executable-relative lookups upstream,
environment fallbacks or moving baked-in paths into generated text files
(which relocate for free). Biggest heads: openssl@3, gettext, glib,
[email protected], libx11, fontconfig, then node, perl, ruby, php,
mysql, postgresql@x, ffmpeg, llvm, gcc, ghc, openjdk.
The padded-prefix scheme alone does not cover every non-:any bottle. Each
residual class below is a goal with its own strategy, tracked to zero or to
a named, formula-annotated exception:
protodesc_cold/.bun sections, e.g.
bun): these keep raw RPATH strings and auto-pin today. Strategy: the
NUL-padded string patcher handles them at pour. A shortened NUL-padded
.dynstr entry is valid (the loader reads to the first NUL), and under
padded builds the replacement always fits. Verified with bun in Phase 2
validation.strings-based scanning cannot see. Under
padded builds these become a correctness risk for default-prefix users
too, since an unseen string never gets patched. Strategy: bottle-time
audit of known container formats with warnings; the Phase 2 pour
validation sweep and the test-bot relocated-pour test are the functional
catch-all; findings become per-formula fixes. NUL padding inside a
length-prefixed field can also corrupt rare formats, which the same
validation catches.pour_bottle? only_if: :default_prefix (apptainer, composer, fish,
ocaml, ocaml@4, screenfetch): these refuse to pour at non-default
prefixes regardless of cellar. Strategy: re-test each under patching and
retire or convert the gates during Phase 4.An audit should enforce the end state: a formula whose fresh bottle is neither relocatable nor patchable must carry an in-formula annotation naming the reason, so the exceptions list stays short, visible and countable.
brew install at any prefix up to 64 bytes never falls back to source
because of relocation on the target platforms.brew benchmark at implementation: 1.67x faster end-to-end on a
5,000-file synthetic keg, relocation cost itself dropping from ~320ms to
~1ms), with gains accruing as bottles are rebottled with metadata.