Skip to content

GHC/Build/Link: relativize absolute rpaths + self-$ORIGIN (stacked on #378)#385

Draft
angerman wants to merge 5 commits into
wip/andrea/stage-qualified-optionsfrom
feat/rpath-relativize-and-self-origin
Draft

GHC/Build/Link: relativize absolute rpaths + self-$ORIGIN (stacked on #378)#385
angerman wants to merge 5 commits into
wip/andrea/stage-qualified-optionsfrom
feat/rpath-relativize-and-self-origin

Conversation

@angerman

Copy link
Copy Markdown

Draft / stacked on #378 (wip/andrea/stage-qualified-options).

Forward-ports the absolute-rpath relativization onto the post-split
stage-qualified line and adds a self-$ORIGIN rpath entry. Stacking here (not
on master) so the result is the unified branch the stable-haskell GHC
build needs — stage-qualified / host-only (#378) plus the rpath fixes — in
one place; it re-bases onto master naturally once #378 lands.

Commits

  1. (Andrea) relativize absolute rpaths when --enable-relocatable
  2. (Andrea) drop the relocatable gate, always relativize abs rpaths
  3. (new) include $ORIGIN/@loader_path in rpaths

(1)+(2) are #368, currently stranded against the pre-split feature/rebase-CI;
this is the same change on the post-split line. (3) adds the artifact's own dir
to the runpath: getRPaths never did, so a same-dir dependency (a Backpack
signature impl libHSp next to the instantiated libHSindef) relativizes to
$ORIGIN/.. and isn't found. glibc hides this via the runtime LD_LIBRARY_PATH
GHC sets before dlopen; musl reads it only at startup, so it's fatal there
(testsuite backpack/cabal/T14304 on Alpine/musl). The new entry is relative,
so it doesn't reintroduce the absolute path (1)+(2) remove (no macOS regression).

Status

Cherry-picks onto stage-qualified-options with no conflicts (the Verbosity
split doesn't touch the rpath code). Not built locally — please let CI
confirm it typechecks against the post-split LocalBuildInfo/InstallDirs API.

Tracked in #384. Supersedes the master-targeting draft #383 (closed).

andreabedini and others added 5 commits June 11, 2026 16:19
Add `package <stage>:*` project-file stanzas (e.g. `package build:*`, `package
host:*`) so per-package configuration can target a single build stage. The same
package can be built in both the build and host stages, so `package *`,
top-level fields and `package <name>` cannot distinguish them; a stage qualifier
can.

- ProjectConfig gains projectConfigStagePackages :: MapMappend Stage
  PackageConfig (+ lens, parsec parser and field grammar). The legacy parser
  does not support the new syntax.
- Per-package option lookup and the shared/profiling-lib downward-closed
  property in ProjectPlanning are made stage-aware, so e.g. `package build:*
  shared: False` keeps the build stage static even when the host stage is built
  dynamic.
- Add a parser test (project-config-stage-packages).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These are latent warnings that -Wincomplete-patterns / -Wunused-imports
promote to errors only under validate's -Werror; they predate and are
unrelated to the stage-qualified package configuration work below.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
depLibraryPaths returns absolute library paths whenever the dep's
libdir is not under the package's own install prefix (i.e. almost
always, in a cabal-store layout where each package gets its own
hash-suffixed subdir). The previous getRPaths only prefixed
`@loader_path` / `$ORIGIN` to already-relative paths, so those
absolute store paths were baked directly into LC_RPATH (Mach-O)
and DT_RUNPATH (ELF).

On macOS up to and including Sonoma (macOS 14) dyld silently falls
through to subsequent rpath entries when an absolute rpath does
not exist. Sequoia (macOS 15) dyld treats the same condition as
fatal and aborts the binary on launch. This affected the
stable-haskell GHC bindist: a `/Volumes/WorkSpace/_work/ghc/ghc/
_build/stage2/store/host/aarch64-apple-darwin/lib` rpath baked in
by the build runner caused `ghc --numeric-version` to SIGABRT
when the bindist was installed on any macos-15 host.

When the user has opted into relocatable mode
(`--enable-relocatable`, or `relocatable: True` in a cabal
project), this commit replaces such absolute rpaths with a
`shortRelativePath`-computed relative form against the artifact's
install directory. The relative form is well-formed (no `/Volumes`
prefix to abort dyld on), and harmless when the bindist layout no
longer matches the build store (dyld treats it as a normal missing
rpath and falls through to subsequent entries).

Non-relocatable builds (the default) are unchanged: absolute paths
still pass through to preserve the existing semantics for
non-relocated installs.

The PackageDescription, InstallDirs, and shortRelativePath utility
this needs are all already in scope or come from already-imported
modules; only `bindir` and `libdir` field accessors are pulled in
freshly from `Distribution.Simple.InstallDirs`.

Companion to commit 010b365582c in stable-haskell/ghc, which strips
the same leaked rpaths post-build with `install_name_tool` while
this Cabal-side fix propagates through bindist rebuilds.
Initial version of this fix gated the new relativization on
`relocatable lbi` to keep non-relocatable builds byte-identical. The
gate doesn't help in practice:

  * the stable-haskell GHC bindist build requires the new behavior
    (else the darwin host binaries abort-trap on macOS 15);

  * setting `relocatable: True` to flip the gate also triggers
    `checkRelocatable` (which refuses cabal-store layouts whose deps
    live in sibling prefixes) and changes how `library-dirs` are
    written into .conf files — neither change is desirable for the
    rpath fix, and the latter actively breaks the bindist
    post-stage2 .conf rewriting in our Makefile pipeline.

The new always-on behavior is a strict improvement for every
relocatable scenario (cabal-store relocation, bindist relocation,
nix-style closure moves) and only a theoretical regression for the
"copy a single executable to an unrelated host and expect the same
absolute path to still resolve" case, which has never been part of
cabal's documented contract.

Companion to stable-haskell/ghc commit that drops the matching
`relocatable: True` flag from configure.ac.
…deps)

The rpath relativization (added for the darwin LC_RPATH-leak fix) never adds the
artifact's own directory to the runpath. depLibraryPaths can return the parent
directory for a dependency installed in the same directory as the artifact, so
that entry relativizes to "$ORIGIN/.." and the dependency is left unfound.

glibc papers over this because GHC sets LD_LIBRARY_PATH at runtime before
dlopen and glibc re-reads it per dlopen; musl reads LD_LIBRARY_PATH only at
process startup and ignores the runtime change, so the missing self-rpath is
fatal there. Concretely, a Backpack signature implementation (libHSp)
installed next to the instantiated unit (libHSindef) fails to load under musl
(GHC testsuite backpack/cabal/T14304 on Alpine), though it passes on every
glibc platform.

Always prepend the loader-relative self directory ($ORIGIN, or @loader_path on
macOS) to the rpaths so a same-directory sibling is found. This is a relative
entry, so it does not reintroduce the absolute build-host path the
relativization removed (no macOS regression).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants