GHC/Build/Link: relativize absolute rpaths when --enable-relocatable#368
Open
angerman wants to merge 152 commits into
Open
GHC/Build/Link: relativize absolute rpaths when --enable-relocatable#368angerman wants to merge 152 commits into
angerman wants to merge 152 commits into
Conversation
The new parser replicates the grammar of the legacy parser while providing better error reporting and more maintainable code structure. The fallback strategy ensures smooth transition while the legacy parser is phased out. The flag `--project-file-parser` allows you to select which project file parser to use. * `legacy` - the old parser (will be removed in a future release) * `default` - the default parser (uses `fallback` unless compiled with `-f+legacy-comparison`) * `parsec` - the new parser using Parsec * `fallback` - the new parser using Parsec, but falling back to the old parser if it fails * `compare` - the new parser using Parsec, but comparing the results with the old parser When `cabal-install` is compiled, then the `-f+legacy-comparision` flag can be passed which changes the default parser mode to `compare`. Fixes haskell#6101 haskell#7748 haskell#10611
* Error messages are indented by 2 spaces * 0:0 positions are never shown * More attempts are made to remove extra newlines.
Replace cabal project parsing with Parsec
Typo in Makefile .PHONY target
Simply disabling the step causes all non-Windows to fail, so we now skip it only on Windows.
Update Hackage root keys
work around git locking issue on Windows
Mark Terminate package test as flaky wrt haskell#11087
Fix typos in release script comments
- Follow hlint suggestion: Use isJust - Follow hlint suggestion: Use isNothing - Follow hlint suggestion: Redundant == - Follow hlint suggestion: Redundant fmap - Follow hlint suggestion: Use minimumBy - Follow hlint suggestion: Use lefts - Follow hlint suggestion: Use fromRight - Follow hlint suggestion: Use for - Follow hlint suggestion: Use forM_ - Follow hlint suggestion: Use when - Follow hlint suggestion: Use uncurry - Follow hlint suggestion: Use traverse - Follow hlint suggestion: Use ?~ - Follow hlint suggestion: Fuse traverse_/map - Follow hlint suggestion: Fuse traverse_/fmap - Follow hlint suggestion: Use replicateM - Follow hlint suggestion: Missing NOINLINE pragma - Rerun hlint --default - Redo hlint counts after rebase - Use Data.Foldable.minimumBy Co-Authored-By: ˌbodʲɪˈɡrʲim <andrew.lelechenko@gmail.com>
Skip T5634 on Alpine
Fix typo folowing
Apply hlint suggestions with a single count
We had problems with their image last time. Let's see if they're fixed now.
- Follow hlint suggestion: Use isDigit - Follow hlint suggestion: Use isAsciiLower - Follow hlint suggestion: Use isAsciiUpper
Since 9.12.2, it is not sufficient to call displayException, but you must also display the exception context as well. The `topHandler` now takes an argument which explains which exceptions are ones which are supposed to be displayed by the user (`VerboseException CabalException` or `VerboseException CabalInstallException`). Locations are not displayed for these exceptions. On the other hand, an assertion failure will always print a backtrace. Fixes haskell#11090
Satisfy HLint suggestions for isDigit and isAscii
try Ubuntu 24.04 again
This reverts commit 5573dea.
This reverts commit 7c1cd10.
This reverts commit bd93810.
Every supported base has getExecutablePath in System.Environment now.
Restore the file monitor mechanism to track already-built packages, using the same approach previously established for in-place builds. This also replaces `phaseImprovePlan`, which improved the elaborated plan by matching source packages against installed store entries. Key changes: - Restore file monitor updates in `buildAndInstallUnpackedPackage`: source files, inplace dependency build cache files, and registration - Simplify `BuildStatusBuild` by dropping the `Maybe InstalledPackageInfo` field, which is now tracked via the registration file monitor instead - Simplify `checkPackageFileMonitorChanged` accordingly - Remove `phaseImprovePlan` and store entry matching from the plan rebuild phase; `rebuildInstallPlan` now returns a 4-tuple instead of 5
…race When two stages of the same package (e.g. build: and host: in cross- compilation) are scheduled concurrently, both threads can observe the unpacked source directory as non-existent and race to extract the tarball. The second extraction overwrites the first mid-flight, corrupting the result and causing intermittent "No cabal file found" errors. Add an unpackLock (using the existing Lock/criticalSection from JobControl) to serialise the doesDirectoryExist check and tarball extraction in withTarballLocalDirectory. This is the same pattern already used for registerLock and cacheLock.
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.
angerman
added a commit
to stable-haskell/ghc
that referenced
this pull request
Jun 4, 2026
Bumps the stable-haskell/Cabal pin across cabal.project.stage{0,1,2}
to a patched SHA (6a5ce8161) carrying stable-haskell/cabal#368:
a one-function change in Distribution.Simple.GHC.Build.Link
that rewrites absolute library rpaths to a `shortRelativePath`-
computed @loader_path / \$ORIGIN-relative form.
Background: depLibraryPaths returns absolute build-store paths
whenever the dep's libdir isn't under the package's own install
prefix — which is essentially always, in a cabal-store layout
where each package gets its own hash-suffixed subdir. The
unpatched getRPaths only prefixed @loader_path/\$ORIGIN to
already-RELATIVE paths, so those absolute store paths went
straight into LC_RPATH / DT_RUNPATH, with macOS 15 dyld treating
them as fatal (= abort-trap on stable-haskell GHC bindist launch).
The Cabal patch partially mitigates this at link time (sibling
deps in the same store still get baked relative); the Cross:
MULTI darwin install_name_tool step picks up the rest. configure.ac
notes the explicit decision NOT to set `relocatable: True` —
that flag triggers cabal-install's checkRelocatable +
\${pkgroot}-prefixed library-dirs in .conf, neither of which is
compatible with the stable-haskell GHC bindist-assembly Makefile.
angerman
added a commit
to stable-haskell/ghc
that referenced
this pull request
Jun 4, 2026
…AML draft
* multi-target-bindist-design.md — design doc covering the
argv[0]-dispatch model, per-target lib/targets/<triple>/
layout, ghcup Installer DSL 0.1.0 exeSymLinked pattern, and
the bindist install flow.
* rpath-leak-investigation.md — root-cause analysis of the
darwin LC_RPATH leak that motivated the patched Cabal
(stable-haskell/cabal#368) and the post-build install_name_tool
fixup step. Documents the unpatched depLibraryPaths /
getRPaths interaction, the macOS 14 vs 15 dyld behavior
difference, and the four ranked fix options (A: post-strip,
B: relPath patch, C: depLibraryPaths patch, D: full
relocatable+restructure).
* draft-ghcup-multi-target-0.1.0.yaml — the channel YAML draft
that became the published gh-pages
ghcup-multi-target-0.1.0.yaml (kept here as design archive;
the live file lives on the gh-pages branch).
angerman
added a commit
to stable-haskell/ghc
that referenced
this pull request
Jun 4, 2026
Bumps the stable-haskell/Cabal pin across cabal.project.stage{0,1,2}
to a patched SHA (6a5ce8161) carrying stable-haskell/cabal#368:
a one-function change in Distribution.Simple.GHC.Build.Link
that rewrites absolute library rpaths to a `shortRelativePath`-
computed @loader_path / \$ORIGIN-relative form.
Background: depLibraryPaths returns absolute build-store paths
whenever the dep's libdir isn't under the package's own install
prefix — which is essentially always, in a cabal-store layout
where each package gets its own hash-suffixed subdir. The
unpatched getRPaths only prefixed @loader_path/\$ORIGIN to
already-RELATIVE paths, so those absolute store paths went
straight into LC_RPATH / DT_RUNPATH, with macOS 15 dyld treating
them as fatal (= abort-trap on stable-haskell GHC bindist launch).
The Cabal patch partially mitigates this at link time (sibling
deps in the same store still get baked relative); the Cross:
MULTI darwin install_name_tool step picks up the rest. configure.ac
notes the explicit decision NOT to set `relocatable: True` —
that flag triggers cabal-install's checkRelocatable +
\${pkgroot}-prefixed library-dirs in .conf, neither of which is
compatible with the stable-haskell GHC bindist-assembly Makefile.
angerman
added a commit
to stable-haskell/ghc
that referenced
this pull request
Jun 4, 2026
…AML draft
* multi-target-bindist-design.md — design doc covering the
argv[0]-dispatch model, per-target lib/targets/<triple>/
layout, ghcup Installer DSL 0.1.0 exeSymLinked pattern, and
the bindist install flow.
* rpath-leak-investigation.md — root-cause analysis of the
darwin LC_RPATH leak that motivated the patched Cabal
(stable-haskell/cabal#368) and the post-build install_name_tool
fixup step. Documents the unpatched depLibraryPaths /
getRPaths interaction, the macOS 14 vs 15 dyld behavior
difference, and the four ranked fix options (A: post-strip,
B: relPath patch, C: depLibraryPaths patch, D: full
relocatable+restructure).
* draft-ghcup-multi-target-0.1.0.yaml — the channel YAML draft
that became the published gh-pages
ghcup-multi-target-0.1.0.yaml (kept here as design archive;
the live file lives on the gh-pages branch).
angerman
added a commit
to stable-haskell/ghc
that referenced
this pull request
Jun 4, 2026
Bumps the stable-haskell/Cabal pin across cabal.project.stage{0,1,2}
to a patched SHA (6a5ce8161) carrying stable-haskell/cabal#368:
a one-function change in Distribution.Simple.GHC.Build.Link
that rewrites absolute library rpaths to a `shortRelativePath`-
computed @loader_path / \$ORIGIN-relative form.
Background: depLibraryPaths returns absolute build-store paths
whenever the dep's libdir isn't under the package's own install
prefix — which is essentially always, in a cabal-store layout
where each package gets its own hash-suffixed subdir. The
unpatched getRPaths only prefixed @loader_path/\$ORIGIN to
already-RELATIVE paths, so those absolute store paths went
straight into LC_RPATH / DT_RUNPATH, with macOS 15 dyld treating
them as fatal (= abort-trap on stable-haskell GHC bindist launch).
The Cabal patch partially mitigates this at link time (sibling
deps in the same store still get baked relative); the Cross:
MULTI darwin install_name_tool step picks up the rest. configure.ac
notes the explicit decision NOT to set `relocatable: True` —
that flag triggers cabal-install's checkRelocatable +
\${pkgroot}-prefixed library-dirs in .conf, neither of which is
compatible with the stable-haskell GHC bindist-assembly Makefile.
angerman
added a commit
to stable-haskell/ghc
that referenced
this pull request
Jun 4, 2026
…AML draft
* multi-target-bindist-design.md — design doc covering the
argv[0]-dispatch model, per-target lib/targets/<triple>/
layout, ghcup Installer DSL 0.1.0 exeSymLinked pattern, and
the bindist install flow.
* rpath-leak-investigation.md — root-cause analysis of the
darwin LC_RPATH leak that motivated the patched Cabal
(stable-haskell/cabal#368) and the post-build install_name_tool
fixup step. Documents the unpatched depLibraryPaths /
getRPaths interaction, the macOS 14 vs 15 dyld behavior
difference, and the four ranked fix options (A: post-strip,
B: relPath patch, C: depLibraryPaths patch, D: full
relocatable+restructure).
* draft-ghcup-multi-target-0.1.0.yaml — the channel YAML draft
that became the published gh-pages
ghcup-multi-target-0.1.0.yaml (kept here as design archive;
the live file lives on the gh-pages branch).
angerman
added a commit
to stable-haskell/ghc
that referenced
this pull request
Jun 8, 2026
* build: multi-target bindist (native + wasm + JS in one tarball)
Adds the $(DIST_DIR)/ghc-multi-target.tar.gz Makefile rule which
combines the native stage2 host build with the stage3-wasm and
stage3-js cross trees into a single ghcup-installable bindist:
* `bin/` contains all three argv[0]-dispatching frontends
(ghc, wasm32-unknown-wasi-ghc, javascript-unknown-ghcjs-ghc),
each pointing at the same physical executable.
* `lib/<host-platform>/` carries the native package db.
* `lib/targets/<triple>/` carries the per-cross-target package
db (recached on install by the bundled relocate.sh).
* mk/multi-target-{configure,relocate,bindist-Makefile} —
autoconf-shaped install scripts so ghcup's installer-DSL drives
the unpack via the same `configure` + `make install` flow it
uses for the wasm-only bindist.
Tar dereferences symlinks (`tar czhf`), so the hardlinked argv[0]
frontends become independent copies in the tarball — argv[0]
dispatch still works after extract.
* stage3: scope shared+executable-dynamic to wasm32 via if arch()
Refines the R7 path-i template: instead of unconditionally
emitting `shared: True` / `executable-dynamic: True` for every
stage3 target, gate them on `if arch(wasm32)` so cabal applies
them ONLY when --with-compiler points at the wasm cross-compiler.
The JS target (arch=javascript) and native build-side packages
(happy-lib, alex, deriveConstants, Setup.hs) skip the conditional
and stay static, fixing two regressions multi-target exposed:
* JS target: emcc/wasm-ld can't produce .so output, and
DYNAMIC=1 with shared:True hit
`wasm-ld: error: unknown argument: -h` when the setting
flowed into JS link lines.
* Native build-side: with shared:True applied, happy-lib /
alex emitted -dynamic-too, which then failed when stage2's
dynamic1 dist lacked .dyn_hi for native-only deps.
Hardcodes the per-package field instead of @ALL_PACKAGES@
substitution — the wasm target always needs shared:True
regardless of how stage2 was built, and the conditional handles
JS/native exclusion cleanly without per-build template
substitution.
* ci(nix-ci): add Cross: MULTI job + darwin bindist verification
Cross: MULTI builds native stage2 + stage3-wasm + stage3-js into a
combined multi-target tarball. Reuses Cross: WASM setup (dynamic1
stage2 download, devx shell, wasi-sdk + Node 22 install, patchelf
for Linux ELF rpath rewrite) and adds emscripten install for the JS
target. Single make invocation drives all three. On multi-* tag
push, uploads to the matching GitHub Release alongside Cross: WASM.
The darwin Mach-O cleanup (LC_RPATH strip + nix-store LC_LOAD_DYLIB
rewrite) is done CONSTRUCTIVELY in the Makefile's stage2.dist phase
(CLEAN_DARWIN_DIST macro, see "build(stage2.dist): clean darwin
Mach-O at construction time" on the base branch). This step here
just VERIFIES the bindist is clean — if the construction-site fix
regresses, CI fails loud rather than shipping a broken bindist that
abort-traps on end-user macOS 15 hosts.
* build: pin patched Cabal (relocatable rpath relativization)
Bumps the stable-haskell/Cabal pin across cabal.project.stage{0,1,2}
to a patched SHA (6a5ce8161) carrying stable-haskell/cabal#368:
a one-function change in Distribution.Simple.GHC.Build.Link
that rewrites absolute library rpaths to a `shortRelativePath`-
computed @loader_path / \$ORIGIN-relative form.
Background: depLibraryPaths returns absolute build-store paths
whenever the dep's libdir isn't under the package's own install
prefix — which is essentially always, in a cabal-store layout
where each package gets its own hash-suffixed subdir. The
unpatched getRPaths only prefixed @loader_path/\$ORIGIN to
already-RELATIVE paths, so those absolute store paths went
straight into LC_RPATH / DT_RUNPATH, with macOS 15 dyld treating
them as fatal (= abort-trap on stable-haskell GHC bindist launch).
The Cabal patch partially mitigates this at link time (sibling
deps in the same store still get baked relative); the Cross:
MULTI darwin install_name_tool step picks up the rest. configure.ac
notes the explicit decision NOT to set `relocatable: True` —
that flag triggers cabal-install's checkRelocatable +
\${pkgroot}-prefixed library-dirs in .conf, neither of which is
compatible with the stable-haskell GHC bindist-assembly Makefile.
* ci(channel-e2e): multi-target gate, darwin PATH fix, fail-loud cabal, cron canary
Four refinements landed iteratively while bringing multi-9.14.0.stable.1
through the release cycle:
* Multi-target install gate (channel-e2e-multi.yml) — strict
\`set -euo pipefail\` install of the multi-target GHC + argv[0]
dispatch verification (Target platform per frontend). Any
post-install failure (broken symlink, missing binary) now
fails the job loud rather than silently skipping.
* Darwin PATH fix — on macos-15, the wasi-sdk install step
prepends WASI_BIN to PATH, which puts wasi-sdk's wasm-only
\`clang\` ahead of /usr/bin/clang. GHC's native code generator
then invoked the wrong clang and hit
\`clang -cc1as: error: unknown target triple 'arm64-apple-macosx15.0.0'\`.
Re-prepend /usr/bin after the wasi-sdk install so Apple's
clang wins for unqualified \`clang\` lookups while the
wasm32-unknown-wasi-clang symlinks still resolve.
* Add wasm channel for cabal — the multi-target channel YAML
intentionally ships only GHC frontends; cabal lives in
ghcup-wasm.yaml. The multi workflow now adds both channels.
* Fail-loud cabal install — drop the stale soft-fallback that
used to silently set cabal_installed=false on "Unable to find
a download for Tool" errors. cabal-3.17.0.0.stable.0 is
universally available now; any install failure is a real bug.
Removes the now-meaningless gate from downstream steps.
* Weekly cron canary — added to both workflows so passive drift
(release asset re-uploaded with different bytes, channel YAML
mis-deployed, NodeSource setup_22.x breaking, ghcup-runner
image quirks) gets caught between manual release events.
Fires Mondays 06:00 UTC on the workflows' home default branch.
* lode: multi-target bindist design + rpath leak root-cause + channel YAML draft
* multi-target-bindist-design.md — design doc covering the
argv[0]-dispatch model, per-target lib/targets/<triple>/
layout, ghcup Installer DSL 0.1.0 exeSymLinked pattern, and
the bindist install flow.
* rpath-leak-investigation.md — root-cause analysis of the
darwin LC_RPATH leak that motivated the patched Cabal
(stable-haskell/cabal#368) and the post-build install_name_tool
fixup step. Documents the unpatched depLibraryPaths /
getRPaths interaction, the macOS 14 vs 15 dyld behavior
difference, and the four ranked fix options (A: post-strip,
B: relPath patch, C: depLibraryPaths patch, D: full
relocatable+restructure).
* draft-ghcup-multi-target-0.1.0.yaml — the channel YAML draft
that became the published gh-pages
ghcup-multi-target-0.1.0.yaml (kept here as design archive;
the live file lives on the gh-pages branch).
* per-target dynamic library settings (#66 JS Path C, #67 target-aware GHC Dynamic) (#187)
* stage3(JS): Path C — emit .dyn_hi via -dynamic-too ghc-option
#66 — symmetric to the wasm-side Path C in cabal.project.stage3.
settings.in.
The shared stage2 GHC binary in the multi-target bindist was
compiled DYNAMIC=1 (required for wasm). cabal-install reads its
`GHC Dynamic: YES` per `ghc --info` and auto-enables library-dynamic
for whichever target you compile against. Without `.dyn_hi` files
in the JS-target sysroot the end-user `cabal build` of a TH-using
package (miso, aeson, lens) fails reading e.g. `Prelude.dyn_hi`.
For wasm we ship `.dyn_hi` + `.so` via `shared: True +
executable-dynamic: True` (the existing arch(wasm32) arm). For JS
the same incantation fails the .so link step:
wasm-ld: error: unknown argument: -h
because emcc/wasm-ld can't produce a real .so for the JS backend
(and we don't need one — there's no dlopen in the JS runtime).
Path C for JS: drop down to `ghc-options: -dynamic-too`. That asks
GHC to emit `.dyn_o + .dyn_hi` alongside `.o + .hi` during compile,
without invoking cabal's library-dynamic .so-link step. The
`.dyn_hi` files are what cabal-install actually needs to find when
later building TH-using packages against this target's sysroot.
The .so byproducts are skipped — irrelevant for JS anyway.
This is a per-package fix; the constraint `rts +dynamic` stays.
Header comment updated to reflect the per-target dial. Companion
follow-up #67 will make `GHC Dynamic` itself target-aware (read
from per-target settings file), at which point the JS bindist
could drop the .dyn_hi shipping if we want to slim it.
Builds + ships verified end-to-end via the multi-target Cross: MULTI
CI job + a follow-on multi-9.14.0.stable.3 candidate tag (TBD —
that's the next commit).
* ghc: target-aware GHC Dynamic via per-target settings key (#67)
Adds a per-target dial — `"target ships dynamic libraries"` —
read from `lib/targets/<triple>/lib/settings`. `ghc --info`'s
`GHC Dynamic` is now `hostIsDynamic && sTargetShipsDynLibs`, so on a
multi-target bindist different targets in one binary can correctly
disagree even though the shared stage2 GHC's compile-time
`DYNAMIC` macro (`rts_isDynamic()`) is fixed.
Why: cabal-install reads `GHC Dynamic` to decide whether to enable
`library-dynamic` by default. Pre-this-change, the multi-target
bindist's one stage2 GHC binary always reported YES (because it
was built `DYNAMIC=1` for wasm), causing cabal to demand .dyn_hi
files even for targets whose lib tree doesn't ship them — concretely
the symptom that motivated this work was `Prelude.dyn_hi: does not
exist` when an end-user `cabal build`-ed a TH-using miso app
against the JS target. The companion commit (#66, ship .dyn_hi for
the JS target too) makes the immediate symptom go away, but the
proper architectural fix is to make `GHC Dynamic` per-target so
*any* future target can opt out cleanly without breaking cabal.
Implementation:
* compiler/GHC/Platform.hs
add `platformMisc_targetShipsDynLibs :: Bool` to PlatformMisc.
* compiler/GHC/Settings/IO.hs
read the new settings key. Default `True` (via Either-fallback)
for backward compatibility with older bindist settings files
that predate the key — matches the historical behaviour of
always reporting YES when the GHC binary is dyn-built.
* compiler/GHC/Settings.hs
expose `sTargetShipsDynLibs` accessor.
* compiler/GHC/Driver/Session.hs (compilerInfo, L3573)
`("GHC Dynamic", showBool (hostIsDynamic && sTargetShipsDynLibs (settings dflags)))`.
hostIsDynamic stays for the GHC binary's own RTS introspection
(e.g. the internal-interpreter linker decisions in
Linker/Deps.hs and Downsweep.hs — both of which already gate
on `internalInterpreter`, so external-interpreter cross flows
are unaffected).
* Makefile (stage3-$(1) rule)
sed-inject the key into ghc-toolchain's generated settings
file. Per-target override via `STAGE3_<triple>_TARGET_SHIPS_DYN_LIBS`
Make variable; defaults to YES for every target we currently
ship (wasm Path C ships .dyn_hi+.so; JS Path C — sibling commit —
ships .dyn_hi via -dynamic-too; native inherits from host).
The per-target settings file is hand-editable so end-users can flip
the dial without rebuilding — e.g. testing a "JS without dyn libs"
scenario by setting the key to NO on an installed bindist.
* fix(Makefile): $$$$ in template recipe to preserve sed end-anchor
Spotted on PR #187 attempt 1: my sed expression
s/\]$$/,("target ships dynamic libraries","YES")]/
inside the `define stage3` template lost its end-of-line anchor.
After template expansion `$$` collapsed to `$`, and Make then read
the lone `$/` in the recipe as a variable lookup (which is empty),
dropping the anchor entirely. The shell saw:
s/\],("target ships dynamic libraries","YES")]/
which sed rejected as `unterminated 's' command`.
Fix: write `$$$$` (four dollars) — collapses through both layers
(define-template expansion AND recipe-execution expansion) into a
literal `$` at shell time, which is sed's end-of-line anchor.
Comment in the recipe records the gotcha so it doesn't get
re-introduced.
* revert(stage3): JS Path C via -dynamic-too leaks to BUILD-side native compiles
Retro from PR #187 first push to feat/multi-target-per-target-dyn:
The Cross: WASM linux jobs both went green ✅ (so the Makefile
sed-injection of the new settings key worked), but Cross: JS
aarch64-darwin failed at alex's first .hs module:
src/DFS.hs:24:8: error: [GHC-47808]
Failed to load dynamic interface file for Prelude:
Exception when reading interface file
.../base-4.22.0.0/Prelude.dyn_hi: does not exist
alex is a NATIVE BUILD-side tool, compiled with the native stage2
GHC (built DYNAMIC=0, no .dyn_hi). The `ghc-options: -dynamic-too`
in the `if arch(javascript) package *` arm leaked to that compile.
`shared: True` (the wasm-side analogue) is properly per-target-arch
in cabal's dual-compiler split — only host packages see it. But
`ghc-options` propagates to BUILD compiles regardless of the arch
conditional. Confirmed: alex emitted both .o and .dyn_o per
module (the `-dynamic-too` signature), and the path was
`aarch64-apple-darwin` (native, not javascript-unknown-ghcjs).
Reverting the arm and documenting the gotcha inline so a future
attempt doesn't trip over the same cabal-side asymmetry.
The companion #67 commit (target-aware GHC Dynamic) becomes the
unblock for the JS demo instead — set the JS-target value to NO
via the new Makefile var STAGE3_javascript-unknown-ghcjs_TARGET_SHIPS_DYN_LIBS.
That tells cabal-install to stop enabling library-dynamic by default
on the JS target, so end-user `cabal build` of TH-using packages
(miso, aeson, lens, …) doesn't demand non-existent .dyn_hi files.
Path C for JS (actually ship .dyn_hi alongside) still wants doing
eventually, but it needs deeper cabal work — a `host-ghc-options`
field or a Makefile-side .dyn_hi copy after the fact. Tracking
remains in task #66; this PR pivots to the #67 unblock.
* ghc: GHC Dynamic = sTargetIsDynamic && sTargetShipsDynLibs
Per review feedback on PR #187: the target's settings file should
COMPLETELY control GHC Dynamic. hostIsDynamic (rts_isDynamic()'s
CPP DYNAMIC macro) is a property of the GHC binary, not of the
target it's currently compiling for; consulting it leaks a
non-per-target signal into a per-target answer.
Drop hostIsDynamic from the GHC Dynamic computation. Split the
single `sTargetShipsDynLibs` into two orthogonal dials:
sTargetIsDynamic ← "target is dynamic" settings key
(the GHC for this target is capable of
producing dynamic output — -dynamic /
-dynamic-too honoured)
sTargetShipsDynLibs ← "target ships dynamic libraries" key
(the lib tree actually has .dyn_hi / .so)
GHC Dynamic = (sTargetIsDynamic && sTargetShipsDynLibs)
The axes are independent on purpose — a target can be dynamic-
capable but not currently ship dyn artifacts (a slimmed bindist),
or have shipped artifacts but a vanilla iserv that doesn't load
them. Both default True for backward compatibility with bindists
that predate the keys.
Per-target Makefile knobs:
STAGE3_<triple>_TARGET_IS_DYNAMIC = YES|NO (default YES)
STAGE3_<triple>_TARGET_SHIPS_DYN_LIBS = YES|NO (default YES)
JS target overridden to NO/NO — the JS iserv runs vanilla (no
dlopen in the JS runtime) and the lib tree ships no .dyn_hi
(Path C doesn't apply to JS yet, see #66). Both reported as NO,
so end-user cabal-install stops auto-enabling library-dynamic on
JS and TH-using packages compile without demanding .dyn_hi files.
Settings file end-users can hand-edit both keys to flip the dial
for an installed bindist (testing slimmed scenarios, etc.).
* ghc: GHC Profiled + Support dynamic-too also target-settings driven
Round out the per-target dial work from the previous commit:
Support dynamic-too:
was `not isWindows`
now `not isWindows && sTargetIsDynamic`
GHC Profiled:
was `hostIsProfiled` (RTS-baked-in)
now `sTargetIsProfiled && sTargetShipsProfLibs`
`Support dynamic-too` keeps the Windows guard as defence-in-depth
for pre-this-patch bindists on Windows that lack the key (default
sTargetIsDynamic=True would otherwise regress them to YES).
`GHC Profiled` mirrors `GHC Dynamic`'s two-dial design exactly,
with two new per-target settings keys:
"target is profiled" (sTargetIsProfiled)
"target ships profiling libraries" (sTargetShipsProfLibs)
(Note: `Debug on` stays `debugIsOn`. That's a CPP constant set at
GHC binary compile-time describing whether the COMPILER itself was
built with -DDEBUG. It's legitimately a property of the binary, not
of the target — unlike Dynamic/Profiled which had a multi-target
asymmetry.)
Makefile side:
* The existing per-target sed-injection for the cross targets'
lib/targets/<triple>/lib/settings now writes all four keys
in one go.
* NEW: native lib/settings injection (line ~617, in the stage1
rule that calls ghc-toolchain-bin --output-settings for the
host platform). Native gets dyn dials = YES iff DYNAMIC=1, prof
dials always NO (stage2 isn't built -prof). Without this the
native target's settings file would be missing the keys and
fall back to the IO.hs default of True for prof — which would
have wrongly reported GHC Profiled=YES on our non-prof native.
JS target overrides extended:
STAGE3_javascript-unknown-ghcjs_TARGET_IS_PROFILED = NO
STAGE3_javascript-unknown-ghcjs_TARGET_SHIPS_PROF_LIBS = NO
(matches the JS reality: vanilla iserv, no .p_hi shipped).
Defaults in the Makefile injection: dyn dials YES, prof dials NO.
Reflects the typical post-this-patch bindist where dyn libs ship
but prof libs don't (since stage2 isn't built -prof). Per-target
overrides via Make vars if any target needs to disagree.
* ci: drop standalone Cross: WASM + Cross: JS jobs (cross-multi is sole)
The multi-target bindist subsumes both targets — wasm32-unknown-wasi
and javascript-unknown-ghcjs ship together in one tarball via the
ghcup-multi-target-0.1.0.yaml channel. The standalone wasm + JS
bindist channels are not maintained anymore; there's no reason to
double-build them in CI.
Removed:
* job `cross-js` (146 lines) — was darwin-only (vestige predating
the host-matrix expansion).
* job `cross-wasm` (442 lines) — matrixed across all 3 hosts with
its own wasm32-wasi-* tag-trigger release upload.
Kept:
* job `cross-multi` (matrixed across all 3 hosts) — builds the
multi-target bindist + uploads to the matching multi-* GitHub
Release on tag push. This is the single Cross job now.
Trigger header updated:
* tags: only `multi-*` (was `wasm32-wasi-*` + `multi-*`)
* Comment rewritten to describe the new single-channel reality.
Internal cross-references (`same as Cross: WASM`, `same as Cross: JS`,
etc.) inside cross-multi stripped — those were narrative decorations
pointing at jobs that no longer exist.
Net diff: -576 lines.
Final DAG:
build (matrix:6) ──┬─> test (matrix:6, needs:build)
└─> cross-multi (matrix:3, needs:build)
* address #184 review comments on stage3 settings + Cabal pins
Four review comments from PR #184 after the squash-merge of #187:
1. cabal.project.stage0:4 ("What tag/branch is this? Why this commit?")
The Cabal SHA 6a5ce816 is the head of stable-haskell/Cabal branch
`feat/rpath-relativize-absolute` (= stable-haskell/Cabal PR #368)
patching Link.hs to relativize absolute rpaths unconditionally,
fixing the darwin LC_RPATH leak from task #51 without flipping
cabal's `relocatable: True` (which also breaks our bindist's
post-stage2 path rewriting). Add explanatory headers to stage0
and back-reference from stage1 + stage2 (same pin) + stage3
(deliberately different pin, since cross-build doesn't hit the
macOS rpath issue and we don't want to validate the rpath patch
on the wasm path).
2. stage3.settings.in:63 ("Do we still need this?")
The 17-line NOTE about the failed JS Path C `ghc-options:
-dynamic-too` attempt is commit-message-grade history, not
code-comment material. The reasoning lives in #187's commit
message and the lode plan doc. Removed.
3. stage3.settings.in:12 ("Why did we drop this?")
#187 replaced the original clean 4-line JS section description
("FALSE → settings do NOT apply") with a 17-line narrative
claiming `arch=javascript → conditional TRUE (ghc-options
branch)` — but the file has no `if arch(javascript)` block, so
the comment misdescribed the code. Restored the accurate
FALSE-based description and added a one-line pointer to the
per-target settings dials from #67 (where JS .dyn_hi shipping
actually gets driven from).
4. stage3.settings.in:66 ("Why hardcode this? configure.ac sets it.")
The `rts +dynamic` constraint is wasm-only (RTS needs the
dynamic flag because wasm32 builds shared libs); it has nothing
to do with the JS or native build-side paths. Moved inside
the `if arch(wasm32)` block alongside `shared: True` and
`executable-dynamic: True` so the wasm-only intent is visible
in the code, not just the comment. Verified cabal-install
accepts `constraints:` inside an `if arch(...)` block.
* unify stage3 Cabal pin with stage0/1/2 (6a5ce816)
Address review feedback on the two-Cabal split. The diff between the
old stage3 SHA (44817477) and the unified SHA (6a5ce816) is exactly
two commits, +46/-1 line, all in `Distribution.Simple.GHC.Build.Link`
— the host-linker rpath handling. wasm-ld doesn't honour rpaths and
neither does the JS backend, so the patch is a true no-op on stage3's
cross-build outputs. Carrying two Cabals just adds maintenance
overhead (every upstream Cabal change has to be re-applied or
re-validated twice) for no real isolation.
Unifies the pin and rewrites the cabal.project.stage0/stage3 header
comments to reflect this.
* stage3.settings.in: restore @ALL_PACKAGES@ / @Constraints@ template
Carrying a `.in` file + autoconf substitution machinery but
hard-coding the package fields and constraints in the .in defeats
the point of having both. Re-use the same APPEND_PKG_FIELD /
APPEND_CONSTRAINT data that stage2.settings.in already consumes.
The reason stage3 went hardcoded before was indentation: stage2 puts
@ALL_PACKAGES@ at column 0 / @Constraints@ under `constraints:`, both
of which line up with the 2-space indent the substitution variables
ship with. Stage3 wraps in `if arch(wasm32)`, so the same content
needs to be 4-space-indented instead.
Add 4-space-indented variants in configure.ac (@ALL_PACKAGES_STAGE3@
and @CONSTRAINTS_STAGE3@, re-indented from the existing ALL_PACKAGES
/ CONSTRAINTS), and use them in stage3.settings.in under the
`if arch(wasm32)` block. Now `--enable-dynamic` (DYNAMIC=1) flips
stage2 and stage3 in lockstep, with one source of truth for the
package-fields / constraint list.
NOTE: this couples stage3-wasm builds to DYNAMIC=1 — the wasm
target genuinely needs `shared: True` to produce .dyn_hi/.so for
end-user TH, so `make stage3-wasm... DYNAMIC=1` is now the
required invocation. CI already passes it; document in commit
message so anyone running it locally knows.
* stage3.settings.in: don't name @var@ tokens literally in the comment
autoconf substitutes @ALL_PACKAGES_STAGE3@ / @CONSTRAINTS_STAGE3@
wherever they appear in the input — including inside Haskell-style
`-- ...` comments — and the multi-line replacement body broke across
the comment boundary, producing invalid cabal syntax at
stage3.settings:33:30.
CI caught this on the first run of the @ALL_PACKAGES_STAGE3@
substitution against the wasm cross-build:
cabal.project.stage3.settings:33:30: error:
unexpected '/'
expecting space or end of input
33 | executable-dynamic: True / rts +dynamic variants — so passing
The fix is the same one used elsewhere in this repo (commit 3f4aa3b
on stage3.settings.in for the same hazard with the older @ALL_PACKAGES@
token): refer to the substitution mechanism without writing the literal
autoconf marker.
* ci: fold miso-counter TH-heavy test into e2e-MULTI, retire e2e-WASM
The legacy `Channel e2e (WASM)` workflow tested the single-target
`ghcup-wasm.yaml` channel by building both the hello and miso-counter
templates against the shipped wasm cross-compiler. The newer
`Channel e2e (MULTI)` workflow already covers hello-worlds for all
three frontends (native / wasm / JS) via the multi-target bindist,
but lacked the TH-at-scale miso-counter coverage — and that's the
fragile, end-user-relevant signal: hello-worlds don't exercise the
dyld / JSFFI / cabal-dual-compiler chain at any meaningful depth.
Fold the miso-counter step (with its 60s diagnostic monitor) into
e2e-MULTI, run it via the wasm frontend of the multi-target bindist,
and bump the matrix timeout 45 → 60 min to fit it. Delete
channel-e2e-wasm.yml: the multi-target bindist supersedes the legacy
single-target wasm channel and we don't want to maintain two near-
identical CI surfaces.
JS-target miso coverage is a follow-up — the existing
stable-haskell-wasm-miso-counter template's Makefile is wasm-specific
and a parallel JS template (or a target-agnostic cabal.project) doesn't
exist yet. The per-target dial from #67 makes JS-target miso buildable
in principle (it no longer demands .dyn_hi from a JS sysroot that
doesn't ship them), but the templating work is independent of this PR.
* ci(e2e-multi): add miso-counter TH-heavy step via the wasm frontend
Follow-up to the e2e-WASM retirement in the previous commit
(d17a78c) — that commit deleted the workflow but didn't manage to
land its companion edits to channel-e2e-multi.yml in the same commit
(amended local, but the prior SHA had already been pushed and we don't
auto-force-push).
This commit adds the miso-counter step we discussed (50+ TH-heavy
deps, 60s diagnostic monitor, .wasm artifact verification), bumps the
matrix timeout 45 → 60 min to fit it, and refreshes the workflow
header / comments so they no longer cross-reference the now-deleted
e2e-WASM workflow.
* ci(e2e-multi): cabal update before the miso-counter build
The first run of the new miso-counter step (run 27091838526, all 3
matrix entries) failed at the solver stage with
[__1] unknown package: host:jsaddle-wasm (dependency of host:myapp)
The cabal package index was never populated in this workflow — the
hello steps above don't touch Hackage so they don't reveal the gap.
The retired e2e-WASM workflow had `cabal update` in its sanity step;
when its miso-counter step ran, the index was already populated.
Add an explicit `cabal update` immediately before the miso fetch.
This was referenced Jun 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
When
--enable-relocatable(orrelocatable: Truein a project file) is in effect, replace absolute library rpaths withshortRelativePath-computed@loader_path(Darwin) /\$ORIGIN(other ELF) form, instead of letting them through unchanged.Why
Distribution.Simple.LocalBuildInfo.depLibraryPathsreturns absolute library paths whenever the dep's libdir is not under the package's own install prefix — i.e. essentially always in a cabal-store layout where each package gets its own hash-suffixed subdir. The previousgetRPathsonly prefixed@loader_path/\$ORIGINto already-relative paths, so those absolute store paths were baked directly into LC_RPATH (Mach-O) and DT_RUNPATH (ELF).macOS up to and including Sonoma (macOS 14) dyld silently falls through to subsequent rpath entries when an absolute rpath cannot be resolved. macos-15 (Sequoia) dyld treats the same condition as fatal and SIGABRTs the binary on launch. This bit the stable-haskell GHC bindist hard: a build-runner-absolute
/Volumes/WorkSpace/_work/ghc/ghc/_build/stage2/store/host/aarch64-apple-darwin/librpath causedghc --numeric-versionto abort-trap on every macos-15 host.Linux ELFs have the identical defect; downstream (the stable-haskell GHC build system) was masking it with
patchelf --force-rpath --set-rpath '\$ORIGIN'post-build. Once this fix propagates, the Linux patchelf rewrite and the post-buildinstall_name_tool -delete_rpathworkaround we added on Darwin both become no-ops and can eventually be retired.Behavior matrix
relocatableFalse(default)@loader_path/p@loader_path/p(unchanged)False(default)pp(unchanged — preserves existing semantics for non-relocated installs)True@loader_path/p@loader_path/p(unchanged)Truep(leaked)@loader_path/<shortRelativePath relDir p>Non-relocatable builds are unchanged.
How
relDircomputed ingetRPathsthe same waydepLibraryPathsdoes (executable →bindir installDirs, otherwise →libdir installDirs).relPathextended with a third case for absolute paths underrelocatable lbi.bindir/libdirfromDistribution.Simple.InstallDirs; everything else (absoluteComponentInstallDirs,localPkgDescr,NoCopyDest,shortRelativePath) is already in scope.Where the dyld-fatal behavior comes from
This is a documented Sequoia change: dyld now enforces stricter handling of unresolvable
LC_RPATHentries that look like absolute filesystem paths. Falling through has been the historical behavior on every other Mach-O dyld I've tested, so the existing macOS 14 path of "emit absolute, hope dyld falls through" no longer holds.Companion work
010b365582cpost-strips the leaked rpaths withinstall_name_tool -delete_rpathfor shipped bindists, until this Cabal fix lands and propagates.49cebdd64dbpins the patched Cabal SHA + flipsrelocatable: Trueincabal.project.stage2.settingsso the stage2 host build picks up the new behavior.Risk
Per the behavior matrix: non-relocatable builds are byte-identical to before. Relocatable builds gain working absolute-path handling. Worst case for a relocatable build with an unusual layout (e.g. dep at
/usr/local/lib, exe in user home): the rpath becomes a../../../../../usr/local/lib-style relative form that resolves correctly to the same physical path when the binary stays at its original location, and gracefully fails through (rather than fatally aborting) when relocated to a host that doesn't have/usr/local/libat that level. That's semantically what relocatable mode is supposed to mean.