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)
| 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: patchelf.rb writes a placeholdered RUNPATH
but leaves the old string dead in the binary and 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..o artefacts (about 29 npm
formulae).abseil replays clean under the current checker yet CI pinned it:
some pins are stale or wrong. 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.Each pinned bottle flipped to cellar :any pours at any prefix with no
length limit and no new machinery.
brew bottle --verbose CI logs for
the abseil class, fix whatever diverges, then batch re-mark provably
clean pins cellar :any with no rebuild (sha256 unchanged)..dynstr corpses.
Flips the glib/gobject-introspection cluster and much of the Linux-only
pinned set on their next rebottle.build/**/obj.target, *.o and *.d from
npm-installed trees and strip or debug-prefix-map compiled .node
addons. Flips the npm cluster.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.abseil-class anomaly: why did CI pin bottles whose contents pass the
current checker? Needs a brew bottle --verbose CI log to reconcile
before trusting the checker's output as blocker metadata.