Harden build prefix relocation of poured bottles - #23665
Conversation
- `Keg#relocate_build_prefix` NUL-pads raw prefix strings in pinned bottles at pour time but had no tests beyond one happy path; the hardening cases in the plan each get a spec, and three found bugs. - Patching wrote the file atomically, giving it a new inode, so any hardlinked names kept the unpatched content. Files are now grouped by inode, patched once and re-linked afterwards, as text relocation already does. - A prefix longer than the bottled one failed midway with a size mismatch after earlier files had already been rewritten; it is now refused up front. - Linkers suffix-merge strings, so a dynamic string table entry can be referenced from its interior (`libfoo.so` inside `/old/prefix/libfoo.so`); trailing NUL padding shifted such references onto garbage. Strings with interior loader references are now padded with extra path separators after the prefix, which path resolution ignores, keeping every reference's offset valid. Other strings keep NUL padding. - Also covered: several occurrences per string and several strings per file, the sharball skip, re-signing of patched files and prefix strings inside Mach-O load commands. This change is part of [`plans/relocatable-bottles.md`](https://github.com/Homebrew/brew/blob/HEAD/Library/Homebrew/plans/relocatable-bottles.md)
- `HOMEBREW_RELOCATE_BUILD_PREFIX` was read straight from `ENV` in two places; it is now a hidden `env_config` boolean so it is typed and testable while staying undocumented until the feature is hardened. - A bottle poured with its build prefix patched in place now records `relocated_build_prefix` and `relocated_files` in its tab, so a relocated keg can be told apart from a pristine one when diagnosing bug reports. `Keg#relocate_build_prefix` returns the patched files to make that possible. - The "building from source" warning for a bottle built for another prefix now states how many characters longer or shorter the local prefix is than the bottle's, which decides whether in-place patching could ever apply, without mentioning the hidden variable. This change is part of [`plans/relocatable-bottles.md`](https://github.com/Homebrew/brew/blob/HEAD/Library/Homebrew/plans/relocatable-bottles.md)
|
Thanks for your pull request. This has been closed because it appears to be missing the pull request template, perhaps because this was written by an AI not a human. We require humans to read and fill in these templates. Please edit this pull request to fill in the current pull request template. This workflow will reopen this pull request automatically once the template is complete. Do not open a new pull request for this. |
There was a problem hiding this comment.
Pull request overview
This PR hardens Homebrew’s “build prefix relocation” path for poured bottles, improving correctness and observability when patching bottles built for a different (typically longer) prefix. It extends tab metadata to record relocation details, adds a hidden env-config knob, and strengthens relocation logic/tests (including ELF string-table edge cases and Mach-O load-command patching).
Changes:
- Teach
Keg#relocate_build_prefixto return patched file paths, handle multi-occurrence substitutions, and preserve suffix-merged ELF string table references. - Record build-prefix relocation state in the installed formula’s tab (build prefix + list of patched files) and gate relocation behind
Homebrew::EnvConfig.relocate_build_prefix?. - Add/adjust tests and documentation around relocation behavior and diagnostics.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| Library/Homebrew/test/tab_spec.rb | Extends Tab JSON serialization coverage for relocation fields and ensures bottle hash excludes them. |
| Library/Homebrew/test/os/mac/keg_spec.rb | Adds macOS coverage that relocation patches Mach-O load-command strings (raw/unresolved). |
| Library/Homebrew/test/keg_relocate/binary_relocation_spec.rb | Adds coverage for multi-occurrence replacement, hardlinks, sharball skipping, prefix-length refusal, codesigning, and ELF suffix-merge behavior. |
| Library/Homebrew/test/formula_installer_spec.rb | Verifies pour warnings report prefix-length deltas (longer/shorter). |
| Library/Homebrew/test/bottle_specification_spec.rb | Ensures longer bottle cellar is accepted when build-prefix relocation is enabled. |
| Library/Homebrew/tab/tab.rb | Adds relocated_build_prefix and relocated_files to Tab, includes them in to_json, and keeps them out of to_bottle_hash. |
| Library/Homebrew/sorbet/rbi/dsl/homebrew/env_config.rbi | Adds Sorbet RBI for relocate_build_prefix?. |
| Library/Homebrew/plans/relocatable-bottles.md | Updates plan documentation for relocation constraints and suffix-merge padding strategy. |
| Library/Homebrew/keg_relocate.rb | Refactors/strengthens build-prefix relocation, including inode grouping and ELF dynamic string reference handling. |
| Library/Homebrew/formula_installer.rb | Improves incompatible-bottle warning messaging; records relocation results to the Tab when relocation occurs. |
| Library/Homebrew/env_config.rb | Adds hidden HOMEBREW_RELOCATE_BUILD_PREFIX env var wiring. |
| Library/Homebrew/bottle_specification.rb | Uses Homebrew::EnvConfig.relocate_build_prefix? to allow longer-prefix bottles when relocation is enabled. |
Files not reviewed (1)
- Library/Homebrew/sorbet/rbi/dsl/homebrew/env_config.rbi: File type not supported
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
| inode_groups = if files | ||
| # Metadata-driven pour: only the files recorded at bottle time carry raw | ||
| # prefix strings, so skip the whole-keg scan. | ||
| hardlinks = Set.new | ||
| files.each do |relative_path| | ||
| file = path/relative_path | ||
| next if !file.file? || file.symlink? | ||
| # Hardlinks share an inode, so patch each inode only once. | ||
| next unless hardlinks.add? file.stat.ino | ||
|
|
||
| candidates << file | ||
| end | ||
| files.map { |relative_path| path/relative_path } | ||
| .select { |file| file.file? && !file.symlink? } | ||
| .group_by { |file| file.stat.ino } | ||
| .values |
cask/cask_loader#23664 from Homebrew/pr-pull-require-cask/cask_loaderfetchphase run beforeinstall#23657 from Homebrew/fetch_methodcask/cask_loaderfetchphase run beforeinstall