WIP: per-target dynamic library settings (#66 JS Path C, #67 target-aware GHC Dynamic)#187
Merged
angerman merged 7 commits intoJun 7, 2026
Conversation
#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).
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.
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.
… 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.
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.).
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.
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)
angerman
added a commit
that referenced
this pull request
Jun 7, 2026
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.
angerman
added a commit
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 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.
Builds on top of #184 (multi-target bindist).
Issues
#66 — JS Path C: ship
.dyn_hifor the JS target.The shared stage2 GHC binary in the multi-target bindist is compiled
DYNAMIC=1(required for wasm). cabal-install reads itsGHC Dynamic: YESperghc --infoand auto-enableslibrary-dynamicfor whichever target it's compiling against. Without
.dyn_hifilesin the JS-target sysroot, end-user
cabal buildof a TH-usingpackage (miso, aeson, lens, …) fails reading e.g.
Prelude.dyn_hi.Wasm-side Path C uses
shared: True + executable-dynamic: True. TheJS analogue fails the .so link step (
wasm-ld: error: unknown argument: -h). Instead drop down toghc-options: -dynamic-too—emit
.dyn_hi/.dyn_oalongside.hi/.oduring compile, butskip cabal's library-dynamic .so-link step.
cabal.project.stage3.settings.in: newif arch(javascript)arm applying
ghc-options: -dynamic-tootopackage *.#67 — target-aware `GHC Dynamic` (pending in a follow-up commit)
`compiler/GHC/Driver/Session.hs:3573` currently reports
`("GHC Dynamic", showBool hostIsDynamic)` — where `hostIsDynamic` is
RTS-baked-in at `rts_isDynamic()` (CPP `DYNAMIC` macro). For a
multi-target bindist with one shared stage2 GHC binary that argv[0]-
dispatches three targets, this is wrong: each target's iserv may
expect a different way of host libraries. The user noted: read from
per-target settings file.
Planned change:
after this PR's [!14775] Print fully qualified unit names in name mismatch #66 fix, all targets do ship dyn libs)
Hand-editable text file → end-users can override.
Validation plan
JS Path C arm.
locally) becomes buildable end-to-end.
Does NOT touch any of the wasm path — opens cleanly on top of PR #184.