Documentation
¶
Overview ¶
Package fixup performs the post-build relocatability fix-ups brewkit applies before a package is bottled: rewriting hardcoded paths in .pc/.cmake files and in installed scripts, removing libtool .la files, consolidating lib64→lib, flattening single-dir include trees, and (via rpath.go) fixing ELF RUNPATHs.
A Windows target needs NONE of this: a PE has no rpath/RUNPATH (DLLs colocate next to the .exe or on PATH), there is no Mach-O or ELF to patch, no lib64 split, and no shebang rewriting — FixUp returns immediately.
Index ¶
- Variables
- func AuditRelocatable(prefix, pkgxDir string) (checked int, problems []error)
- func FixUp(opts Options) error
- func MachoNeeded(path string) ([]string, error)
- func MachoSignatureStale(path string) (bool, error)
- func ReadInterp(path string) (string, error)
- func ReadMachoStrings(path string) ([]string, error)
- func ReadNeeded(path string) ([]string, error)
- func ReadRunpath(path string) (string, error)
- func RewriteMachoStrings(path string, fn func(string) string) error
- func SetInterp(path, value string) error
- func SetRunpath(path, value string) error
- func SonameOf(raw []byte) string
- func SortedUnique(in []string) []string
- type Options
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoInterp is returned for an ELF with no PT_INTERP program header, // which is nearly all of them: a shared library does not name one. ErrNoInterp = errors.New("elf has no PT_INTERP") // ErrInterpNoSpace is returned when the replacement is longer than the // slot. PT_INTERP's string is fixed-width in the file like .dynstr's, and // the segment cannot grow without moving everything after it. ErrInterpNoSpace = errors.New("interpreter path does not fit in place") )
Sentinels returned by SetInterp.
var ( // ErrNoRunpath means the ELF has neither DT_RUNPATH nor DT_RPATH, so there // is no string slot to overwrite in place. ErrNoRunpath = errors.New("fixup: ELF has no DT_RUNPATH/DT_RPATH entry") // ErrNoSpace means the new RUNPATH is longer than the existing string slot. // A pure-Go in-place rewrite cannot grow .dynstr; link the binary with a // longer -Wl,-rpath placeholder so the final value fits. ErrNoSpace = errors.New("fixup: new RUNPATH longer than existing slot") )
Sentinels returned by SetRunpath.
var ErrAbsoluteRef = errors.New("fixup: absolute reference to a path outside the system")
ErrAbsoluteRef means a Mach-O asks dyld for a library by an absolute path that is not part of macOS — so it loads only on a machine that happens to have that exact path, which is the build machine and nobody else.
Every guard beside this one reads `@rpath/…` strings and skips anything spelled differently, so a reference like
/opt/homebrew/opt/gettext/lib/libintl.8.dylib (git-scm.org, 656 files) /opt/homebrew/opt/brotli/lib/libbrotlidec.1.dylib (freetype.org)
was invisible to all of them. Homebrew is not a dependency of anything here; it is what happened to be installed on the runner, found by a configure script that then recorded its path. The consequence is not only a dyld failure on the user's machine: freetype's generated `freetype2.pc` inherited `Requires.private: … libbrotlidec`, so `fontconfig`'s build now fails at `pkg-config` on a package nothing declares.
The rule is stated as an ALLOWLIST on purpose. A denylist of known-bad prefixes inherits the blind spot of whoever wrote it — `/Users/runner` was listed and `/opt/homebrew` was not, which is why 711 references went out. What may legitimately be absolute is small, fixed and owned by Apple: `/usr/lib` and `/System`. Everything else absolute names a machine.
Measured over the installed closures: 5642 such references in 63 projects — `/Users/runner` 4093, `/opt/qt.io` 834, `/opt/homebrew` 711, plus one `/Users/builder/actions-runner/_work/pantry/pantry/builds/…` inherited from an upstream mirror.
var ErrBadSignature = errors.New("fixup: malformed Mach-O code signature")
ErrBadSignature means the embedded signature is not shaped the way the format says. bk refuses to guess at it: a half-understood signature rewritten anyway produces a binary that dies with no message.
var ErrBuilderOnlyRpath = errors.New("fixup: sibling package reachable only through an absolute rpath")
ErrBuilderOnlyRpath means a Mach-O reaches a SIBLING package only through an absolute LC_RPATH — the build machine's own pkgx directory. It runs there and nowhere else.
var ErrDeadRpath = errors.New("fixup: @rpath reference with no LC_RPATH")
ErrDeadRpath means a Mach-O references @rpath/… and carries no LC_RPATH at all, so nothing can ever resolve it.
var ErrDuplicateRpath = errors.New("fixup: duplicate LC_RPATH")
ErrDuplicateRpath means a Mach-O carries the same LC_RPATH twice. ld refuses to link against such a library, so it breaks every dependent rather than the bottle itself — which is why nothing noticed until a dependent's build failed:
ld: duplicate LC_RPATH '@loader_path/../../../..' in
.../facebook.com/folly/v2026.09.14.00/lib/libfolly.0.58.0-dev.dylib
c++: error: linker command failed with exit code 1
It was this factory that wrote the second one: relativising an absolute rpath can produce a string bk had already linked in. That is fixed where it is made, but a defect invisible to every check we had is exactly what a guard is for — the bottle carrying it was built, signed, published and inspected without complaint.
var ErrMachoMagicMismatch = errors.New("fixup: Mach-O magic contradicts its cputype")
ErrMachoMagicMismatch means a Mach-O header's word size contradicts its own cputype: a 32-bit magic (0xfeedface) over a cputype carrying CPU_ARCH_ABI64, or the reverse. No linker produces that. GNU strip does, on a file it was never meant to touch:
cffa edfe 0c00 0001 before: magic 0xfeedfacf, cputype ARM64 cefa edfe 0c00 0001 after: magic 0xfeedface, cputype ARM64
and it exits 0. `pkgx +<deps>` can put gnu.org/binutils ahead of /usr/bin on darwin — gnu.org/gcc pulls it in at runtime — so a recipe's bare `strip` is GNU's, and github.com/rcedgar/muscle 5.3 was built, signed, indexed and published as a binary the kernel kills on sight (exit 137, no output).
It was found by the LC_RPATH guard rather than by anything looking at the header, and only because strip had taken the load commands with it. That is a side effect: the malformation is four bytes wide and worth naming on its own, because debug/macho parses such a file happily AS 32-bit and everything downstream then reads the wrong offsets.
var ErrMissingRef = errors.New("fixup: @rpath reference names a file that is not there")
ErrMissingRef means a Mach-O names a file under $PKGX_DIR that is not there.
The reference records a decision taken when the binary was LINKED — @rpath/gnome.org/libxml2/v2/lib/libxml2.2.dylib — and the resolver takes the same decision again, separately, when a consumer installs. Nothing keeps the two in agreement, and they disagree on both axes:
libxml2 2.13.9 -> 2.15.4 same pkgx major, DIFFERENT soname (libxml2.2 -> libxml2.16) gettext 0.26 -> 1.0.0 different major, SAME soname (libintl.8, compat 13)
So a major-versioned reference can be satisfied and still not resolve, and an ABI-compatible upgrade can look like a break. Measured over 26519 Mach-O in 272 installed closures: 51 packages name a file that is not there.
REPORTED, not refused. qt.io is among the 51 and runs: a dangling reference in a module nothing loads never faults. Refusing would stop a fifth of the catalogue from building over defects that are latent — the backlog has to drain first.
var ErrStaleSignature = errors.New("fixup: code signature no longer describes the file")
ErrStaleSignature means a Mach-O carries a code signature that no longer describes its own bytes — the state an in-place edit leaves behind when nothing restates the hashes.
On Apple silicon this is not a load error. The kernel checks each page against the code directory at first fault and kills the process outright:
$ gm version $ echo $? 137 ← SIGKILL, nothing on either stream
There is no dyld message to grep for, which is why an audit that classified on the output text recorded 34 dead packages as healthy. The crash report is the only thing that says so: termination namespace CODESIGNING, "Invalid Page".
`codesign -v` on the EXECUTABLE is no help either — it is usually not the file that was edited. `graphicsmagick.org` 1.3.48 is Developer ID signed and verifies clean while dying on `gnu.org/libtool`'s `libltdl.7.dylib`.
2123 files across 56 published project@versions reached the catalogue this way before `fixup` learned to re-sign (#96), and every guard here missed them: they all read Mach-O *references* and none asked whether the bytes still hash to what the signature claims. `MachoSignatureStale` could answer it from #101 onwards and nothing ever called it. A guard that is written, tested and unwired guards nothing.
Functions ¶
func AuditRelocatable ¶
AuditRelocatable runs the Mach-O relocatability guards over an already-laid-out prefix WITHOUT modifying a byte, and reports what it found.
`FixUp` runs these same checks while it rewrites a freshly built package, so nothing it produces can reach the registry unrunnable. A MIRRORED bottle is never unpacked — it is republished verbatim — so the guards have no bytes to look at and cannot fire. `git-scm.org 2.55.0` reached the catalogue that way: 164 of its 164 Mach-O files reach a sibling package only through `/Users/runner/.pkgx`, the build machine's own path, so `git` cannot load zlib on any other machine. It surfaced weeks later as some other recipe's build failure — `git-remote-https died of signal 6`.
This reports and does not refuse. Whether a mirror is REQUIRED to be relocatable is a question about what our signature claims, and it is go-pkgx/packages#147's to answer; what a publisher should not have to do is find out from a third package's dyld error.
func MachoNeeded ¶
MachoNeeded is what a Mach-O LOADS: its LC_LOAD_DYLIB, weak and re-export entries, and nothing else.
ReadMachoStrings returns every load-command string, which includes the file's OWN install name (LC_ID_DYLIB) and its LC_RPATH entries. Counting the install name as a dependency makes a library appear to depend on wherever it happens to live — the ELF side has had ReadNeeded for exactly this reason, and the Mach-O side had no equivalent.
func MachoSignatureStale ¶
MachoSignatureStale reports whether path is a signed Mach-O whose code-slot hashes no longer describe its own bytes — the state an in-place edit leaves behind, and the one macOS refuses to run on Apple silicon.
It asks the same question `codesign -v` does, without codesign: the answer is needed on machines that do not have it, and about bottles built for a platform the asking machine is not. An unsigned Mach-O is not stale — there is nothing to disagree with — so it reports false.
func ReadInterp ¶
ReadInterp returns an ELF's PT_INTERP string without its NUL, or "" when the file has no such program header.
func ReadMachoStrings ¶
ReadMachoStrings returns the install name, dylib references and rpaths of a Mach-O (LC_ID_DYLIB, LC_LOAD_DYLIB and friends, LC_RPATH), from every architecture slice of a fat binary.
func ReadNeeded ¶
ReadNeeded returns the DT_NEEDED shared-library names of an ELF.
func ReadRunpath ¶
ReadRunpath returns the DT_RUNPATH (preferred) or DT_RPATH of an ELF, or "" if it has neither.
func RewriteMachoStrings ¶
RewriteMachoStrings rewrites every dylib-name/rpath string in a Mach-O in place by applying fn — every architecture slice of a fat binary. It cannot grow a string (Mach-O load commands are a fixed size), so a longer replacement returns ErrNoSpace; a shorter one is zero-padded. Stripping a staging suffix (…+brewing) always shrinks, so fits.
Any slice it changes is re-signed (see machosign.go): an edited Mach-O whose signature still describes the old bytes is not a binary with a stale signature, it is a binary that cannot run at all.
func SetInterp ¶
SetInterp overwrites an ELF's PT_INTERP string in place, zero-padding the slack.
In place, like SetRunpath, and for the same reason: the string's length is baked into p_filesz and into every offset after it, so growing it would mean rewriting the file. Shrinking is free — the kernel reads p_filesz bytes and requires the last to be NUL, which zero-padding keeps true.
func SetRunpath ¶
SetRunpath overwrites an ELF's existing DT_RUNPATH/DT_RPATH string in place. It is pure-Go (no patchelf): it locates the string offset in .dynstr and overwrites the bytes, zero-padding any slack. It cannot grow .dynstr, so it returns ErrNoSpace when value is longer than the current slot and ErrNoRunpath when there is no rpath entry at all.
func SonameOf ¶
SonameOf returns the ABI name an in-memory shared library calls itself by — LC_ID_DYLIB's basename on darwin, DT_SONAME on ELF — or "" for anything that is not a shared library (an executable, a loadable bundle, an archive, a script).
This is the key a dependent actually binds to, and the one a pkgx constraint cannot express. libxml2 states its own ABI line in configure.ac as LIBXML_MINOR_COMPAT=14, and the soname is CURRENT − AGE = MAJOR + MINOR_COMPAT: 2.13.9 ships libxml2.2.dylib and every 2.14+ ships libxml2.16.dylib. Both answer `^2`. A bottle that records what it provides lets a checker decide, from published metadata alone, whether a declared constraint can honour the references its dependents record — which is the question go-pkgx/packages#207 asks and nothing can currently answer.
The basename, not the whole install name: the path is where this build put it, the basename is what dyld and ld.so match on.
func SortedUnique ¶
SortedUnique is the shape an annotation is written in: one sorted list, no repeats, so the same tree always produces the same string.
Types ¶
type Options ¶
type Options struct {
Prefix string // the final install prefix (…/project/vX.Y.Z)
BuildInstall string // the +brewing staging prefix that paths were baked with
Platform string // target platform: darwin | linux | windows
PkgxDir string // $PKGX_DIR: the root darwin @rpath references resolve against
Skips []string // recipe build.skip entries (fix-machos, fix-patchelf, libtool-cleanup, flatten-includes)
// DepPaths are the install prefixes of the build's dependency closure,
// used to compute $ORIGIN-relative RUNPATHs (linux only).
DepPaths []string
// Log, if set, receives human-readable progress lines.
Log func(string)
}
Options controls a fix-up run.