WIP: multi-target GHC bindist (native + wasm + JS in one)#184
Merged
Conversation
angerman
added a commit
that referenced
this pull request
Jun 1, 2026
…e2e multi-target steps
Three related changes for the multi-target work:
1. nix-ci.yml cross-multi job: add `env: EMSDK_VERSION: "3.1.74"`.
Cross: JS declares this at the JOB level (workflow-level env doesn't
exist in this file). Cross: MULTI inherited nothing → ${{ env.EMSDK_VERSION }}
expanded to empty → `git clone --branch '' ...` exited 129 with
git's usage-help output (PR #184 run 26731649704 logs lines 1894-2020).
2. nix-ci.yml on.push.tags: add `multi-*` alongside `wasm32-wasi-*`.
Future tag pushes for multi-target candidates fire the workflow.
3. channel-e2e.yml: add multi-target test steps after the existing
stable.12 wasm tests. continue-on-error: true per step so failures
don't break the wasm signal. Tests:
* add channel + install multi-9.14.0.stable.0 + ghcup set
* native hello via ghc
* wasm hello template via wasm32-unknown-wasi-ghc
(WASM_VERSION=multi-9.14.0.stable.0 override)
* JS hello via javascript-unknown-ghcjs-ghc (emscripten install
included)
The multi-target channel YAML isn't published yet, so the multi-
target steps will fail at `ghcup install` until Phase 11 promotes —
intentionally, the continue-on-error guards against that.
angerman
added a commit
that referenced
this pull request
Jun 1, 2026
…ch(wasm32) Multi-target Cross: MULTI on PR #184 (runs 26731492182 → 26735573736) hit `wasm-ld: error: unknown argument: -h` when building host:rts-fs for the javascript-unknown-ghcjs target. Root cause: DYNAMIC=1 expanded cabal.project.stage3.settings to a package-wide `shared: True` + `executable-dynamic: True`, which then flows through to the JS-side rts-fs build → emcc → wasm-ld → unknown -h. JS doesn't support shared libraries the same way wasm does (emcc/wasm-ld refuse the `-Bsymbolic -h` pattern ghc emits for shared targets). Fix: scope the settings to wasm32 only via `if arch(wasm32)`. Cabal evaluates this conditional against `--with-compiler`'s target arch per-invocation: * wasm cross compiler: arch=wasm32 → TRUE → shared applies * JS cross compiler: arch=javascript → FALSE → shared does NOT apply * native build-side (happy-lib, alex, …): arch=native → FALSE Hardcoded (no autoconf substitution); DYNAMIC=1 has no effect on stage3 anymore. cabal-install at SHA 44817477 (stable-haskell/cabal) evaluates arch() per-cabal-call so this scoping is per-target. PR #181 (wasm-only) continues to work — for wasm targets the conditional fires TRUE same as before. dynamic1 stage2 supplies native .dyn_hi for build-side packages (happy-lib etc.) so they link cleanly against vanilla native base.
angerman
added a commit
that referenced
this pull request
Jun 2, 2026
…only)
Second class of build-host leak in our darwin bindist: stage2 host
libraries (libHSghc-internal, libHSterminfo, ...) link against the
nix-store install-names devx/clang saw at link time:
libHSghc-internal-9.1400.0-ghc9.14.dylib references
/nix/store/...-libiconv-109.100.2/lib/libiconv.2.dylib
libHSterminfo-0.4.1.7-ghc9.14.dylib references
/nix/store/...-ncurses-6.5/lib/libncursesw.6.dylib
Off the build runner those paths don't exist; dyld can't load the
.dylib and the binary abort-traps. e2e/aarch64-darwin in PR #184
run 26805774141 caught this:
dyld[91216]: Library not loaded: /nix/store/...-libiconv-...
Referenced from: .ghcup/.../libHSghc-internal-...dylib
Abort trap: 6
Adopts the devx fixup-nix-deps pattern (input-output-hk/devx
static.nix SHA 5f05c1e1af6) — for each Mach-O in the bindist,
walk `otool -L` and `install_name_tool -change` each nix-store
install-name to its /usr/lib equivalent:
libiconv[.2] → /usr/lib/libiconv[.2].dylib
libffi.* → /usr/lib/libffi.dylib
libc++.* → /usr/lib/libc++.dylib
libz → /usr/lib/libz.dylib
libresolv.* → /usr/lib/libresolv.dylib
Plus an addition over devx's pattern for our haskeline dependency:
libncurses[w].* → /usr/lib/libncurses.5.4.dylib
(macOS ships only 5.4 in the dyld shared cache;
it is ABI-compatible for terminfo lookups)
Merged into the existing rpath-strip step rather than adding a
second darwin-only step. Both classes get one re-sign per file
(skipped if neither rewrite touched the binary).
Verified locally on /tmp/multi-extract/darwin: pattern correctly
rewrites both libiconv (ghc-internal) and libncursesw (terminfo).
angerman
added a commit
that referenced
this pull request
Jun 3, 2026
PR #184 channel-e2e MULTI darwin run 79288405270 caught a real bug in the e2e setup. The 'multi - native hello' step failed at link time on macos-15: clang -cc1as: error: unknown target triple 'arm64-apple-macosx15.0.0' 'clang' failed in phase 'Assembler'. (Exit code: 1) Root cause: the wasi-sdk install step appends WASI_BIN to GITHUB_PATH (via 'echo "$WASI_BIN" >> "$GITHUB_PATH"'). GITHUB_PATH prepends entries to PATH for subsequent steps, so WASI_BIN ends up ahead of /usr/bin. WASI_BIN contains a `clang` symlink to a wasm-only LLVM build that doesn't know any Apple host triple — when GHC's native code generator on macos-15 invokes unqualified `clang`, it resolves to wasi-sdk's, which rejects 'arm64-apple-macosx15.0.0' at the assembler stage. Linux is unaffected: GHC there invokes `cc` (gcc), not `clang`, so wasi-sdk's clang never shadows the native toolchain. The WASM-only workflow on darwin is unaffected because its tests don't exercise native-host code generation (only wasm32-unknown-wasi-ghc). Fix: on Darwin, re-prepend /usr/bin to GITHUB_PATH after the wasi-sdk install step. Apple's clang wins the bare 'clang' resolution again, while the wasm32-unknown-wasi-clang symlinks in WASI_BIN (unique names) still resolve correctly for the wasm compile path.
3a7cd9c to
fa20de3
Compare
e7722a6 to
3651cf0
Compare
c8d6fb3 to
1e74fe9
Compare
3651cf0 to
1e74fe9
Compare
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.
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.
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.
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.
… 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.
…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).
…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)
angerman
commented
Jun 7, 2026
| type: git | ||
| location: https://github.com/stable-haskell/Cabal.git | ||
| tag: 44817477ff6d22de4bfa4307e061df58f319d3b6 | ||
| tag: 6a5ce8161ca76356a9ea43f2e9e09483e6f5849d |
Author
There was a problem hiding this comment.
What tag/branch is this? Why do we use this commit?
Member
There was a problem hiding this comment.
44817477ff6d22de4bfa4307e061df58f319d3b6 used to be wip/angerman/compile-less.
It looks like in build: pin patched Cabal that emits relocatable rpaths you then changed to 723df03ec80a1e1b9b2fefde71f1ace8ba216287 and finally to 6a5ce8161ca76356a9ea43f2e9e09483e6f5849d in build: rev Cabal pin to unconditional rpath relativization (6a5ce816)
Comment on lines
+47
to
+63
| -- NOTE: an earlier draft of #66 (PR #187) attempted Path C for the | ||
| -- JS target via: | ||
| -- if arch(javascript) | ||
| -- package * | ||
| -- ghc-options: -dynamic-too | ||
| -- but cabal-install applies `ghc-options` differently from `shared` | ||
| -- in the dual-compiler split: `shared` is properly per-target-arch | ||
| -- (only host packages see it; native build-side packages like alex, | ||
| -- happy-lib do NOT), but `ghc-options` leaks to build-side compiles | ||
| -- regardless of the arch conditional. The native stage2 is built | ||
| -- DYNAMIC=0 (no .dyn_hi for the host arch), so alex's first .hs | ||
| -- module failed with | ||
| -- Prelude.dyn_hi: does not exist (No such file or directory) | ||
| -- when compiled by the build compiler with -dynamic-too. | ||
| -- Path C for JS therefore needs deeper cabal work (or a Makefile- | ||
| -- side .dyn_hi copy after the fact); the GHC Dynamic settings dial | ||
| -- in the sibling commit (#67) is the proper long-term answer. |
Comment on lines
-11
to
-12
| package * | ||
| @ALL_PACKAGES@@STAGE3_EXTRA_PKG@ |
|
|
||
| constraints: | ||
| @CONSTRAINTS@ | ||
| rts +dynamic |
Author
There was a problem hiding this comment.
Why hardcode this? We do set it in configure.ac?
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.
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.
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.
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.
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.
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.
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.
angerman
added a commit
to stable-haskell/notebooks
that referenced
this pull request
Jun 7, 2026
Use stable-haskell/ghc#184's multi-target bindist (native + wasm + JS in one ghcup install) instead of the separate wasm32-wasi cross-compiler + upstream native GHC: - §2 installs from ghcup-multi-target-0.1.0.yaml: ghcup install/set ghc multi-9.14.0.stable.1. One install provides ghc, wasm32-unknown-wasi-ghc, and javascript-unknown-ghcjs-ghc, symlinked into ~/.ghcup/bin (exeSymLinked bin/**). - drop the separate 'ensure native ghc' step — multi includes native. - WASM_VERSION points at the multi GHC dir so the template Makefile finds post-link.mjs; PATH simplifies to ~/.ghcup/bin. - §2.4 wasi-sdk retained: still not bundled (channel viPreInstall says so). - recreate the notebook generator as a committed wasm-hello.nb/notebook.py (+ 'make notebook') so regeneration is reproducible.
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.
Stacked on PR #181 (the .dyn_hi shipping work for stable.12).
Builds a single GHC bindist that exposes three compilation targets through
the same
bin/ghcbinary via argv[0] dispatch:bin/ghc— nativebin/wasm32-unknown-wasi-ghc— wasm crossbin/javascript-unknown-ghcjs-ghc— JS crossEnd-user install (planned):
Channel YAML uses the Installer DSL 0.1.0 schema's
exeSymLinkedwithtargetPattern: "bin/**"to symlink all per-target binaries into~/.ghcup/bin/automatically.What's in this PR
Makefile: new$(DIST_DIR)/ghc-multi-target.tar.gzrule that dependson stage3-wasm + stage3-js + native stage2; uses
tar -czhftodereference per-target symlinks into standalone binaries
mk/multi-target-{configure.sh,bindist-Makefile,relocate.sh}:autoconf-shaped install path that handles all 3 per-target package
caches at install time
.github/workflows/nix-ci.yml: newCross: MULTIjob (matrixed acrossall 3 host platforms) that installs wasi-sdk + emscripten + Node 22,
builds the multi-target tarball, patchelfs the host ELFs with
$ORIGIN-relative rpath, and uploads to GitHub Release on tagsmatching
multi-*lode/multi-target-bindist-design.md: design doc with rationale,layout, and open questions
Status
WIP — gating before promoting to LatestPrerelease on the channel YAML.
Rollback safety: gh-pages tag
demo-freeze-2026-06-05pins thestable.12 channel state.
Not for merge yet.