From c054e2cc895ac8820c60ab0965394cc8c7f8ff5a Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Fri, 5 Jun 2026 17:35:59 +0900 Subject: [PATCH 1/7] =?UTF-8?q?stage3(JS):=20Path=20C=20=E2=80=94=20emit?= =?UTF-8?q?=20.dyn=5Fhi=20via=20-dynamic-too=20ghc-option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #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). --- cabal.project.stage3.settings.in | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/cabal.project.stage3.settings.in b/cabal.project.stage3.settings.in index 0be702118ae4..785061e122f5 100644 --- a/cabal.project.stage3.settings.in +++ b/cabal.project.stage3.settings.in @@ -1,8 +1,8 @@ -- cabal.project.stage3.settings - generated by configure from .in template -- Do not edit this file directly; edit cabal.project.stage3.settings.in instead. -- --- Multi-target stage3: applies dynamic library settings ONLY to wasm32 --- via cabal's `if arch(wasm32)` conditional, which cabal evaluates against +-- Multi-target stage3: applies dynamic library settings per-target +-- via cabal's `if arch(...)` conditional, which cabal evaluates against -- the --with-compiler's target arch per-invocation. -- -- * stage3-wasm32-unknown-wasi (--with-compiler=wasm32-...-ghc): @@ -11,10 +11,21 @@ -- (miso, jsaddle, aeson, …) -- -- * stage3-javascript-unknown-ghcjs (--with-compiler=javascript-...-ghc): --- arch=javascript → conditional FALSE → settings do NOT apply --- → no shared:True flowing into emcc/wasm-ld (which can't produce --- .so for the JS backend; the alternative breaks with --- `wasm-ld: error: unknown argument: -h`) +-- arch=javascript → conditional TRUE (ghc-options branch) +-- → ghc-options: -dynamic-too applied per-package +-- → produces .dyn_hi files without invoking cabal's library-dynamic +-- .so link step (which fails for JS with +-- `wasm-ld: error: unknown argument: -h`). +-- Path C symmetric: the shared stage2 GHC binary reports +-- GHC Dynamic=YES (it is) which makes cabal-install enable +-- library-dynamic by default for the JS target too. Without +-- .dyn_hi files cabal then fails reading e.g. Prelude.dyn_hi +-- when compiling miso. We don't want shared:True here (which +-- would also invoke wasm-ld for an .so we don't need), so +-- ghc-options is the surgical alternative: ask GHC to emit +-- .dyn_o + .dyn_hi alongside .o + .hi during compile, and let +-- cabal skip the library-dynamic link step. .so byproducts +-- are inert for JS (no dlopen). -- -- * Native build-side packages (compiled with --with-build-compiler=ghc, -- i.e. happy-lib, alex, deriveConstants, Setup.hs scripts): @@ -33,5 +44,14 @@ if arch(wasm32) shared: True executable-dynamic: True +-- Path C for the JS target: emit .dyn_hi alongside .hi so cabal- +-- install's library-dynamic auto-detection (driven by +-- GHC Dynamic=YES on the shared stage2 GHC binary) is satisfied +-- when end-users build TH-heavy packages (miso, aeson, lens, …). +-- See header comment for the full reasoning. +if arch(javascript) + package * + ghc-options: -dynamic-too + constraints: rts +dynamic From 5f2454fbd1592d35f077a8d798efb04cd5abec55 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Fri, 5 Jun 2026 17:41:24 +0900 Subject: [PATCH 2/7] ghc: target-aware GHC Dynamic via per-target settings key (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a per-target dial — `"target ships dynamic libraries"` — read from `lib/targets//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__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. --- Makefile | 13 +++++++++++++ compiler/GHC/Driver/Session.hs | 14 ++++++++++++-- compiler/GHC/Platform.hs | 11 +++++++++++ compiler/GHC/Settings.hs | 9 +++++++++ compiler/GHC/Settings/IO.hs | 15 +++++++++++++++ 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e24f99eb626a..f6c40b35f76d 100644 --- a/Makefile +++ b/Makefile @@ -1007,6 +1007,19 @@ ifeq ($(DYNAMIC),1) $(SED) -i -e 's/"RTS ways","/"RTS ways","dyn /' $$(TARGET_DIR)/lib/settings endif + @# Inject the per-target "target ships dynamic libraries" key. + @# Drives `ghc --info`'s `GHC Dynamic` — cabal-install reads that + @# to decide whether to enable library-dynamic by default. On a + @# multi-target bindist the shared stage2 GHC binary's RTS-baked- + @# in dynamic-ness isn't a good per-target proxy. Default YES for + @# every target we currently build (wasm Path C ships .dyn_hi/.so, + @# JS Path C ships .dyn_hi via -dynamic-too in cabal.project.stage3. + @# settings.in, native inherits from host). Override in a target- + @# specific variable below if a target ever ships static-only. + @# The settings file is the literal list-of-pairs ghc-toolchain + @# emitted; insert before the closing `]`. + $(SED) -i -e 's/\]$$/,("target ships dynamic libraries","$(if $(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),$(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),YES)")]/' $$(TARGET_DIR)/lib/settings + $$(DIST_DIR)/bin/$(1)-ghc --info @rm -rf $$(TARGET_DIR)/lib/package.conf.d diff --git a/compiler/GHC/Driver/Session.hs b/compiler/GHC/Driver/Session.hs index 02afd8c91ce4..3fe5bab4ffbd 100644 --- a/compiler/GHC/Driver/Session.hs +++ b/compiler/GHC/Driver/Session.hs @@ -132,6 +132,7 @@ module GHC.Driver.Session ( sGhcWithInterpreter, sLibFFI, sTargetRTSLinkerOnlySupportsSharedLibs, + sTargetShipsDynLibs, GhcNameVersion(..), FileSettings(..), PlatformMisc(..), @@ -3569,8 +3570,17 @@ compilerInfo dflags ("Uses package keys", "YES"), -- Whether or not we support the @-this-unit-id@ flag ("Uses unit IDs", "YES"), - -- Whether or not GHC was compiled using -dynamic - ("GHC Dynamic", showBool hostIsDynamic), + -- Whether or not GHC was compiled using -dynamic AND this + -- target's installed library tree actually ships .dyn_hi / .so + -- files. cabal-install reads this to decide whether to enable + -- @library-dynamic@ by default; on a multi-target bindist the + -- shared stage2 GHC binary's RTS-baked-in @hostIsDynamic@ is + -- not a good per-target proxy. The @sTargetShipsDynLibs@ dial + -- comes from the per-target settings file key + -- @"target ships dynamic libraries"@ (defaults to True for + -- backward compatibility with older bindists), so different + -- targets in one bindist can correctly disagree. + ("GHC Dynamic", showBool (hostIsDynamic && sTargetShipsDynLibs (settings dflags))), -- Whether or not GHC was compiled using -prof ("GHC Profiled", showBool hostIsProfiled), ("Debug on", showBool debugIsOn), diff --git a/compiler/GHC/Platform.hs b/compiler/GHC/Platform.hs index 5375c243c6ef..bd38098f7955 100644 --- a/compiler/GHC/Platform.hs +++ b/compiler/GHC/Platform.hs @@ -291,6 +291,17 @@ data PlatformMisc = PlatformMisc , platformMisc_libFFI :: Bool , platformMisc_llvmTarget :: String , platformMisc_targetRTSLinkerOnlySupportsSharedLibs :: Bool + -- | Does the target's installed library tree ship @.dyn_hi@ / + -- @.so@ files? Set per-target by the bindist build (a hand- + -- editable @lib/targets/\/lib/settings@ key — + -- @"target ships dynamic libraries"@). cabal-install reads + -- @ghc --info@'s @GHC Dynamic@ to decide whether to enable + -- @library-dynamic@ by default; on a multi-target bindist the + -- one stage2 GHC binary's RTS-baked-in dynamic-ness isn't a + -- good per-target proxy, since different targets may genuinely + -- not ship @.dyn_hi@ (e.g. a slimmed JS bindist whose iserv + -- only loads vanilla code). + , platformMisc_targetShipsDynLibs :: Bool } platformSOName :: Platform -> FilePath -> FilePath diff --git a/compiler/GHC/Settings.hs b/compiler/GHC/Settings.hs index b8ec884a94e4..57701de080d0 100644 --- a/compiler/GHC/Settings.hs +++ b/compiler/GHC/Settings.hs @@ -67,6 +67,7 @@ module GHC.Settings , sGhcWithInterpreter , sLibFFI , sTargetRTSLinkerOnlySupportsSharedLibs + , sTargetShipsDynLibs ) where import GHC.Prelude @@ -314,3 +315,11 @@ sLibFFI = platformMisc_libFFI . sPlatformMisc sTargetRTSLinkerOnlySupportsSharedLibs :: Settings -> Bool sTargetRTSLinkerOnlySupportsSharedLibs = platformMisc_targetRTSLinkerOnlySupportsSharedLibs . sPlatformMisc + +-- | Does this target's installed library tree ship .dyn_hi / .so files? +-- Read from the per-target settings file key +-- @"target ships dynamic libraries"@. Drives @ghc --info@'s @GHC Dynamic@ +-- value, which cabal-install reads to decide whether to enable +-- @library-dynamic@ by default. See PlatformMisc note for the rationale. +sTargetShipsDynLibs :: Settings -> Bool +sTargetShipsDynLibs = platformMisc_targetShipsDynLibs . sPlatformMisc diff --git a/compiler/GHC/Settings/IO.hs b/compiler/GHC/Settings/IO.hs index 63964ff02ff8..6836485b1add 100644 --- a/compiler/GHC/Settings/IO.hs +++ b/compiler/GHC/Settings/IO.hs @@ -185,6 +185,20 @@ initSettings top_dir = do ghcWithInterpreter <- getBooleanSetting "Use interpreter" useLibFFI <- getBooleanSetting "Use LibFFI" + -- Whether this target's installed library tree actually ships + -- .dyn_hi / .so files. cabal-install reads `GHC Dynamic` to + -- decide whether to enable library-dynamic by default; on a + -- multi-target bindist this needs to be per-target because the + -- shared stage2 GHC binary's RTS-baked-in dynamic-ness is fixed + -- but different targets may genuinely not ship dyn artifacts. + -- + -- Default to True for backward compatibility with older bindist + -- settings files that predate this key (matches the historical + -- behaviour of always reporting GHC Dynamic when the binary is + -- dyn-built). + targetShipsDynLibs <- either (const $ pure True) pure $ + getRawBooleanSetting settingsFile mySettings "target ships dynamic libraries" + baseUnitId <- getSetting_raw "base unit-id" return $ Settings @@ -267,6 +281,7 @@ initSettings top_dir = do , platformMisc_libFFI = useLibFFI , platformMisc_llvmTarget = llvmTarget , platformMisc_targetRTSLinkerOnlySupportsSharedLibs = targetRTSLinkerOnlySupportsSharedLibs + , platformMisc_targetShipsDynLibs = targetShipsDynLibs } , sRawSettings = settingsList From 0ceb8cbda3d7be2bdb4deacd5230cada97e118b2 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Fri, 5 Jun 2026 18:45:57 +0900 Subject: [PATCH 3/7] fix(Makefile): $$$$ in template recipe to preserve sed end-anchor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f6c40b35f76d..14c336572b0f 100644 --- a/Makefile +++ b/Makefile @@ -1018,7 +1018,13 @@ endif @# specific variable below if a target ever ships static-only. @# The settings file is the literal list-of-pairs ghc-toolchain @# emitted; insert before the closing `]`. - $(SED) -i -e 's/\]$$/,("target ships dynamic libraries","$(if $(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),$(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),YES)")]/' $$(TARGET_DIR)/lib/settings + @# Note: `$$$$` (four dollars) collapses through two layers of + @# Make expansion (define-template + recipe-time) to a literal `$` + @# at shell time, which is what sed needs as the end-of-line anchor. + @# `$$` would collapse to `$` after template expansion, then Make + @# would interpret the lone `$/` in the recipe as a variable lookup + @# and drop the anchor entirely (verified: PR #187 first attempt). + $(SED) -i -e 's/\]$$$$/,("target ships dynamic libraries","$(if $(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),$(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),YES)")]/' $$(TARGET_DIR)/lib/settings $$(DIST_DIR)/bin/$(1)-ghc --info From b4e03087dff63cbdc72712c82654b2054a8ba133 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Fri, 5 Jun 2026 20:30:21 +0900 Subject: [PATCH 4/7] revert(stage3): JS Path C via -dynamic-too leaks to BUILD-side native compiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 10 ++++++++++ cabal.project.stage3.settings.in | 25 +++++++++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 14c336572b0f..1a50d9d1d232 100644 --- a/Makefile +++ b/Makefile @@ -919,6 +919,16 @@ STAGE3_javascript-unknown-ghcjs_NM = emnm STAGE3_javascript-unknown-ghcjs_RANLIB = emranlib STAGE3_javascript-unknown-ghcjs_STRIP = emstrip STAGE3_javascript-unknown-ghcjs_GHC_TOOLCHAIN_ARGS = $(GHC_TOOLCHAIN_ARGS) --disable-tables-next-to-code +# The JS-target lib tree does NOT ship .dyn_hi (the wasm-style Path C +# via `shared: True` fails for JS with `wasm-ld: error: unknown +# argument: -h`, and a `ghc-options: -dynamic-too` workaround leaks +# to BUILD-side native compiles via cabal's dual-compiler split — +# see PR #187 retro). Tell end-user cabal-install via the +# `target ships dynamic libraries` settings key that it should NOT +# enable `library-dynamic` by default on the JS target — otherwise +# TH-using packages (miso, aeson, lens, …) fail looking for missing +# .dyn_hi. Drives compiler/GHC/Driver/Session.hs:3573's GHC Dynamic. +STAGE3_javascript-unknown-ghcjs_TARGET_SHIPS_DYN_LIBS = NO STAGE3_wasm32-unknown-wasi_CC = wasm32-unknown-wasi-clang STAGE3_wasm32-unknown-wasi_CC_OPTS = -fno-strict-aliasing -Wno-error=int-conversion -Oz -msimd128 -mnontrapping-fptoint -msign-ext -mbulk-memory -mmutable-globals -mmultivalue -mreference-types diff --git a/cabal.project.stage3.settings.in b/cabal.project.stage3.settings.in index 785061e122f5..03bc97751120 100644 --- a/cabal.project.stage3.settings.in +++ b/cabal.project.stage3.settings.in @@ -44,14 +44,23 @@ if arch(wasm32) shared: True executable-dynamic: True --- Path C for the JS target: emit .dyn_hi alongside .hi so cabal- --- install's library-dynamic auto-detection (driven by --- GHC Dynamic=YES on the shared stage2 GHC binary) is satisfied --- when end-users build TH-heavy packages (miso, aeson, lens, …). --- See header comment for the full reasoning. -if arch(javascript) - package * - ghc-options: -dynamic-too +-- 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. constraints: rts +dynamic From 59bbe7999e1c2a7f030cff45868a1cfcf248ec79 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Sat, 6 Jun 2026 10:15:10 +0900 Subject: [PATCH 5/7] ghc: GHC Dynamic = sTargetIsDynamic && sTargetShipsDynLibs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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__TARGET_IS_DYNAMIC = YES|NO (default YES) STAGE3__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.). --- Makefile | 48 +++++++++++++++++++++------------- compiler/GHC/Driver/Session.hs | 25 ++++++++++-------- compiler/GHC/Platform.hs | 24 ++++++++++------- compiler/GHC/Settings.hs | 14 +++++++--- compiler/GHC/Settings/IO.hs | 27 +++++++++++-------- 5 files changed, 85 insertions(+), 53 deletions(-) diff --git a/Makefile b/Makefile index 1a50d9d1d232..b0f1ef50adb3 100644 --- a/Makefile +++ b/Makefile @@ -923,12 +923,15 @@ STAGE3_javascript-unknown-ghcjs_GHC_TOOLCHAIN_ARGS = $(GHC_TOOLCHAIN_ARGS) --dis # via `shared: True` fails for JS with `wasm-ld: error: unknown # argument: -h`, and a `ghc-options: -dynamic-too` workaround leaks # to BUILD-side native compiles via cabal's dual-compiler split — -# see PR #187 retro). Tell end-user cabal-install via the -# `target ships dynamic libraries` settings key that it should NOT -# enable `library-dynamic` by default on the JS target — otherwise -# TH-using packages (miso, aeson, lens, …) fail looking for missing -# .dyn_hi. Drives compiler/GHC/Driver/Session.hs:3573's GHC Dynamic. -STAGE3_javascript-unknown-ghcjs_TARGET_SHIPS_DYN_LIBS = NO +# see PR #187 retro). And the JS-target iserv runs vanilla — there's +# no dlopen in the JS host runtime, so the target can't meaningfully +# do dynamic linking either. Both per-target dials NO → `ghc --info` +# reports `GHC Dynamic: NO` for the JS target → end-user cabal-install +# stops auto-enabling library-dynamic → TH-using packages (miso, +# aeson, lens, …) build without demanding missing .dyn_hi. Drives +# compiler/GHC/Driver/Session.hs's GHC Dynamic computation. +STAGE3_javascript-unknown-ghcjs_TARGET_IS_DYNAMIC = NO +STAGE3_javascript-unknown-ghcjs_TARGET_SHIPS_DYN_LIBS = NO STAGE3_wasm32-unknown-wasi_CC = wasm32-unknown-wasi-clang STAGE3_wasm32-unknown-wasi_CC_OPTS = -fno-strict-aliasing -Wno-error=int-conversion -Oz -msimd128 -mnontrapping-fptoint -msign-ext -mbulk-memory -mmutable-globals -mmultivalue -mreference-types @@ -1017,24 +1020,33 @@ ifeq ($(DYNAMIC),1) $(SED) -i -e 's/"RTS ways","/"RTS ways","dyn /' $$(TARGET_DIR)/lib/settings endif - @# Inject the per-target "target ships dynamic libraries" key. - @# Drives `ghc --info`'s `GHC Dynamic` — cabal-install reads that - @# to decide whether to enable library-dynamic by default. On a - @# multi-target bindist the shared stage2 GHC binary's RTS-baked- - @# in dynamic-ness isn't a good per-target proxy. Default YES for - @# every target we currently build (wasm Path C ships .dyn_hi/.so, - @# JS Path C ships .dyn_hi via -dynamic-too in cabal.project.stage3. - @# settings.in, native inherits from host). Override in a target- - @# specific variable below if a target ever ships static-only. - @# The settings file is the literal list-of-pairs ghc-toolchain - @# emitted; insert before the closing `]`. + @# Inject the two per-target dials that drive `ghc --info`'s + @# `GHC Dynamic` value (cabal-install reads this to decide whether + @# to enable library-dynamic by default): + @# + @# target is dynamic — is the GHC for this target + @# capable of producing dynamic + @# output (-dynamic / -dynamic-too)? + @# target ships dynamic libraries — does the lib tree actually + @# ship .dyn_hi / .so artifacts? + @# + @# Together: `GHC Dynamic` = first && second. Per-target settings + @# file completely controls this — the shared stage2 GHC binary's + @# RTS-baked-in dynamic-ness is no longer consulted. Two dials + @# instead of one so a target can be dynamic-capable but not + @# currently ship dyn artifacts (or vice versa) — keeps the axes + @# orthogonal for slimming experiments. + @# + @# Defaults YES; override via STAGE3__TARGET_IS_DYNAMIC + @# or STAGE3__TARGET_SHIPS_DYN_LIBS Make variables. + @# @# Note: `$$$$` (four dollars) collapses through two layers of @# Make expansion (define-template + recipe-time) to a literal `$` @# at shell time, which is what sed needs as the end-of-line anchor. @# `$$` would collapse to `$` after template expansion, then Make @# would interpret the lone `$/` in the recipe as a variable lookup @# and drop the anchor entirely (verified: PR #187 first attempt). - $(SED) -i -e 's/\]$$$$/,("target ships dynamic libraries","$(if $(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),$(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),YES)")]/' $$(TARGET_DIR)/lib/settings + $(SED) -i -e 's/\]$$$$/,("target is dynamic","$(if $(STAGE3_$(1)_TARGET_IS_DYNAMIC),$(STAGE3_$(1)_TARGET_IS_DYNAMIC),YES)"),("target ships dynamic libraries","$(if $(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),$(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),YES)")]/' $$(TARGET_DIR)/lib/settings $$(DIST_DIR)/bin/$(1)-ghc --info diff --git a/compiler/GHC/Driver/Session.hs b/compiler/GHC/Driver/Session.hs index 3fe5bab4ffbd..ec32f79b6e5b 100644 --- a/compiler/GHC/Driver/Session.hs +++ b/compiler/GHC/Driver/Session.hs @@ -132,6 +132,7 @@ module GHC.Driver.Session ( sGhcWithInterpreter, sLibFFI, sTargetRTSLinkerOnlySupportsSharedLibs, + sTargetIsDynamic, sTargetShipsDynLibs, GhcNameVersion(..), FileSettings(..), @@ -3570,17 +3571,19 @@ compilerInfo dflags ("Uses package keys", "YES"), -- Whether or not we support the @-this-unit-id@ flag ("Uses unit IDs", "YES"), - -- Whether or not GHC was compiled using -dynamic AND this - -- target's installed library tree actually ships .dyn_hi / .so - -- files. cabal-install reads this to decide whether to enable - -- @library-dynamic@ by default; on a multi-target bindist the - -- shared stage2 GHC binary's RTS-baked-in @hostIsDynamic@ is - -- not a good per-target proxy. The @sTargetShipsDynLibs@ dial - -- comes from the per-target settings file key - -- @"target ships dynamic libraries"@ (defaults to True for - -- backward compatibility with older bindists), so different - -- targets in one bindist can correctly disagree. - ("GHC Dynamic", showBool (hostIsDynamic && sTargetShipsDynLibs (settings dflags))), + -- Reported as YES iff *both* per-target settings dials say so: + -- `target is dynamic` — the GHC for this target + -- can produce dynamic output + -- `target ships dynamic libraries` — the lib tree actually has + -- .dyn_hi / .so artifacts + -- cabal-install reads this to decide whether to enable + -- @library-dynamic@ by default. The target's per-target settings + -- file completely controls this value — no host-RTS dependency, + -- so on a multi-target bindist with one shared stage2 GHC binary + -- different targets can correctly disagree. Both keys default to + -- True if absent (matches pre-this-change behaviour). + ("GHC Dynamic", showBool (sTargetIsDynamic (settings dflags) + && sTargetShipsDynLibs (settings dflags))), -- Whether or not GHC was compiled using -prof ("GHC Profiled", showBool hostIsProfiled), ("Debug on", showBool debugIsOn), diff --git a/compiler/GHC/Platform.hs b/compiler/GHC/Platform.hs index bd38098f7955..42f131c1bc96 100644 --- a/compiler/GHC/Platform.hs +++ b/compiler/GHC/Platform.hs @@ -291,16 +291,22 @@ data PlatformMisc = PlatformMisc , platformMisc_libFFI :: Bool , platformMisc_llvmTarget :: String , platformMisc_targetRTSLinkerOnlySupportsSharedLibs :: Bool + -- | Is the GHC for this target capable of producing dynamic + -- output (i.e. can it honour @-dynamic@ / @-dynamic-too@)? + -- Per-target settings key @"target is dynamic"@ in + -- @lib/targets/\/lib/settings@. On a multi-target bindist + -- the shared stage2 GHC binary's RTS-baked-in dynamic-ness is + -- not a per-target proxy — different targets in one binary may + -- need to disagree (e.g. a JS target whose iserv runs vanilla + -- and whose lib tree has no dyn artifacts). Combined with + -- 'platformMisc_targetShipsDynLibs' to drive @ghc --info@'s + -- @GHC Dynamic@ value, which cabal-install reads. + , platformMisc_targetIsDynamic :: Bool -- | Does the target's installed library tree ship @.dyn_hi@ / - -- @.so@ files? Set per-target by the bindist build (a hand- - -- editable @lib/targets/\/lib/settings@ key — - -- @"target ships dynamic libraries"@). cabal-install reads - -- @ghc --info@'s @GHC Dynamic@ to decide whether to enable - -- @library-dynamic@ by default; on a multi-target bindist the - -- one stage2 GHC binary's RTS-baked-in dynamic-ness isn't a - -- good per-target proxy, since different targets may genuinely - -- not ship @.dyn_hi@ (e.g. a slimmed JS bindist whose iserv - -- only loads vanilla code). + -- @.so@ files? Per-target settings key + -- @"target ships dynamic libraries"@. Set independently of + -- 'platformMisc_targetIsDynamic' so a target can be dynamic- + -- capable but not currently ship dyn artifacts (or vice versa). , platformMisc_targetShipsDynLibs :: Bool } diff --git a/compiler/GHC/Settings.hs b/compiler/GHC/Settings.hs index 57701de080d0..2548c09e1e2f 100644 --- a/compiler/GHC/Settings.hs +++ b/compiler/GHC/Settings.hs @@ -67,6 +67,7 @@ module GHC.Settings , sGhcWithInterpreter , sLibFFI , sTargetRTSLinkerOnlySupportsSharedLibs + , sTargetIsDynamic , sTargetShipsDynLibs ) where @@ -316,10 +317,15 @@ sLibFFI = platformMisc_libFFI . sPlatformMisc sTargetRTSLinkerOnlySupportsSharedLibs :: Settings -> Bool sTargetRTSLinkerOnlySupportsSharedLibs = platformMisc_targetRTSLinkerOnlySupportsSharedLibs . sPlatformMisc +-- | Is the GHC for this target capable of producing dynamic output? +-- Read from the per-target settings file key @"target is dynamic"@. +-- Combined with 'sTargetShipsDynLibs' to drive @ghc --info@'s +-- @GHC Dynamic@ value — the target's settings file completely +-- controls that, no host-RTS dependency. +sTargetIsDynamic :: Settings -> Bool +sTargetIsDynamic = platformMisc_targetIsDynamic . sPlatformMisc + -- | Does this target's installed library tree ship .dyn_hi / .so files? --- Read from the per-target settings file key --- @"target ships dynamic libraries"@. Drives @ghc --info@'s @GHC Dynamic@ --- value, which cabal-install reads to decide whether to enable --- @library-dynamic@ by default. See PlatformMisc note for the rationale. +-- Per-target settings key @"target ships dynamic libraries"@. sTargetShipsDynLibs :: Settings -> Bool sTargetShipsDynLibs = platformMisc_targetShipsDynLibs . sPlatformMisc diff --git a/compiler/GHC/Settings/IO.hs b/compiler/GHC/Settings/IO.hs index 6836485b1add..16eab618c706 100644 --- a/compiler/GHC/Settings/IO.hs +++ b/compiler/GHC/Settings/IO.hs @@ -185,17 +185,21 @@ initSettings top_dir = do ghcWithInterpreter <- getBooleanSetting "Use interpreter" useLibFFI <- getBooleanSetting "Use LibFFI" - -- Whether this target's installed library tree actually ships - -- .dyn_hi / .so files. cabal-install reads `GHC Dynamic` to - -- decide whether to enable library-dynamic by default; on a - -- multi-target bindist this needs to be per-target because the - -- shared stage2 GHC binary's RTS-baked-in dynamic-ness is fixed - -- but different targets may genuinely not ship dyn artifacts. - -- - -- Default to True for backward compatibility with older bindist - -- settings files that predate this key (matches the historical - -- behaviour of always reporting GHC Dynamic when the binary is - -- dyn-built). + -- Per-target dial #1: is the GHC for THIS target capable of + -- producing dynamic output (i.e. honouring -dynamic / + -- -dynamic-too)? On a multi-target bindist with one shared stage2 + -- GHC binary, this can't be derived from the binary's compile- + -- time `hostIsDynamic`. Default True for backward compatibility + -- with older bindist settings files that predate the key. + targetIsDynamic <- either (const $ pure True) pure $ + getRawBooleanSetting settingsFile mySettings "target is dynamic" + + -- Per-target dial #2: does this target's installed lib tree + -- actually ship .dyn_hi / .so files? Independent of + -- `target is dynamic` so a dynamic-capable target can still + -- truthfully say it doesn't ship artifacts (e.g. a slimmed + -- bindist). cabal-install combines both via GHC Dynamic to + -- decide whether to enable library-dynamic by default. targetShipsDynLibs <- either (const $ pure True) pure $ getRawBooleanSetting settingsFile mySettings "target ships dynamic libraries" @@ -281,6 +285,7 @@ initSettings top_dir = do , platformMisc_libFFI = useLibFFI , platformMisc_llvmTarget = llvmTarget , platformMisc_targetRTSLinkerOnlySupportsSharedLibs = targetRTSLinkerOnlySupportsSharedLibs + , platformMisc_targetIsDynamic = targetIsDynamic , platformMisc_targetShipsDynLibs = targetShipsDynLibs } From 1621ef43e2d9bade42d17608d0ef52c4d6f37824 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Sat, 6 Jun 2026 10:38:30 +0900 Subject: [PATCH 6/7] ghc: GHC Profiled + Support dynamic-too also target-settings driven MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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. --- Makefile | 68 ++++++++++++++++++++-------------- compiler/GHC/Driver/Session.hs | 24 ++++++++++-- compiler/GHC/Platform.hs | 10 +++++ compiler/GHC/Settings.hs | 12 ++++++ compiler/GHC/Settings/IO.hs | 12 ++++++ 5 files changed, 94 insertions(+), 32 deletions(-) diff --git a/Makefile b/Makefile index b0f1ef50adb3..e069f75b46ac 100644 --- a/Makefile +++ b/Makefile @@ -617,6 +617,12 @@ $(STAGE1_STAMP): $(CONFIGURE_SCRIPTS) $(CONFIGURED_FILES) cabal.project.stage1 c ifeq ($(DYNAMIC),1) $(SED) -i -e 's/"RTS ways","/"RTS ways","dyn debug_dyn thr_dyn thr_debug_dyn /' $(STORE_DIR)/host/$(HOST_PLATFORM)/lib/settings endif + @# Inject the four per-target dials into the native settings file + @# too — same keys as the stage3 cross targets (see the lengthy + @# comment above $(TARGET_DIR)/lib/settings injection). The native + @# target's dynamic state tracks our DYNAMIC=0/1 build flag; prof + @# is always NO because stage2 isn't built -prof. + $(SED) -i -e 's/\]$$/,("target is dynamic","$(if $(filter 1,$(DYNAMIC)),YES,NO)"),("target ships dynamic libraries","$(if $(filter 1,$(DYNAMIC)),YES,NO)"),("target is profiled","NO"),("target ships profiling libraries","NO")]/' $(STORE_DIR)/host/$(HOST_PLATFORM)/lib/settings $(call LOG,Creating packagedb in $(STORE_DIR)/host/$(HOST_PLATFORM)/lib/package.conf.d) @rm -rf $(STORE_DIR)/host/$(HOST_PLATFORM)/lib/package.conf.d @@ -919,19 +925,17 @@ STAGE3_javascript-unknown-ghcjs_NM = emnm STAGE3_javascript-unknown-ghcjs_RANLIB = emranlib STAGE3_javascript-unknown-ghcjs_STRIP = emstrip STAGE3_javascript-unknown-ghcjs_GHC_TOOLCHAIN_ARGS = $(GHC_TOOLCHAIN_ARGS) --disable-tables-next-to-code -# The JS-target lib tree does NOT ship .dyn_hi (the wasm-style Path C -# via `shared: True` fails for JS with `wasm-ld: error: unknown -# argument: -h`, and a `ghc-options: -dynamic-too` workaround leaks -# to BUILD-side native compiles via cabal's dual-compiler split — -# see PR #187 retro). And the JS-target iserv runs vanilla — there's -# no dlopen in the JS host runtime, so the target can't meaningfully -# do dynamic linking either. Both per-target dials NO → `ghc --info` -# reports `GHC Dynamic: NO` for the JS target → end-user cabal-install -# stops auto-enabling library-dynamic → TH-using packages (miso, -# aeson, lens, …) build without demanding missing .dyn_hi. Drives -# compiler/GHC/Driver/Session.hs's GHC Dynamic computation. +# JS target overrides: NO across the board. +# * dyn: iserv runs vanilla (no dlopen in the JS runtime), lib tree +# ships no .dyn_hi (Path C doesn't apply to JS — see PR #187). +# * prof: stage2 isn't built -prof, no .p_hi shipped. +# All four dials NO → `ghc --info` for the JS target reports +# `GHC Dynamic: NO`, `GHC Profiled: NO`, `Support dynamic-too: NO`. +# End-user cabal-install correctly skips library-{dynamic,profiling}. STAGE3_javascript-unknown-ghcjs_TARGET_IS_DYNAMIC = NO STAGE3_javascript-unknown-ghcjs_TARGET_SHIPS_DYN_LIBS = NO +STAGE3_javascript-unknown-ghcjs_TARGET_IS_PROFILED = NO +STAGE3_javascript-unknown-ghcjs_TARGET_SHIPS_PROF_LIBS = NO STAGE3_wasm32-unknown-wasi_CC = wasm32-unknown-wasi-clang STAGE3_wasm32-unknown-wasi_CC_OPTS = -fno-strict-aliasing -Wno-error=int-conversion -Oz -msimd128 -mnontrapping-fptoint -msign-ext -mbulk-memory -mmutable-globals -mmultivalue -mreference-types @@ -1020,25 +1024,33 @@ ifeq ($(DYNAMIC),1) $(SED) -i -e 's/"RTS ways","/"RTS ways","dyn /' $$(TARGET_DIR)/lib/settings endif - @# Inject the two per-target dials that drive `ghc --info`'s - @# `GHC Dynamic` value (cabal-install reads this to decide whether - @# to enable library-dynamic by default): + @# Inject the per-target dials that drive `ghc --info`'s + @# `GHC Dynamic` and `GHC Profiled` values (cabal-install reads + @# these to decide whether to enable library-dynamic / + @# library-profiling by default): @# - @# target is dynamic — is the GHC for this target - @# capable of producing dynamic - @# output (-dynamic / -dynamic-too)? - @# target ships dynamic libraries — does the lib tree actually - @# ship .dyn_hi / .so artifacts? + @# target is dynamic — GHC capable of -dynamic / + @# -dynamic-too output + @# target ships dynamic libraries — lib tree has .dyn_hi / .so + @# target is profiled — GHC capable of -prof output + @# target ships profiling libraries — lib tree has .p_hi / .p_a @# - @# Together: `GHC Dynamic` = first && second. Per-target settings - @# file completely controls this — the shared stage2 GHC binary's - @# RTS-baked-in dynamic-ness is no longer consulted. Two dials - @# instead of one so a target can be dynamic-capable but not - @# currently ship dyn artifacts (or vice versa) — keeps the axes - @# orthogonal for slimming experiments. + @# Reported pairs: + @# GHC Dynamic = (target is dynamic) && (target ships dynamic libraries) + @# GHC Profiled = (target is profiled) && (target ships profiling libraries) @# - @# Defaults YES; override via STAGE3__TARGET_IS_DYNAMIC - @# or STAGE3__TARGET_SHIPS_DYN_LIBS Make variables. + @# Per-target settings file completely controls these — the + @# shared stage2 GHC binary's RTS-baked-in dynamic/prof-ness is + @# no longer consulted. Two dials per way (is / ships) so a + @# target can be capable but not currently ship artifacts (or + @# vice versa) — keeps the axes orthogonal for slimming + @# experiments and matches what cabal really wants to know. + @# + @# Defaults for our bindists: dynamic dials YES (most targets + @# ship dyn libs), prof dials NO (stage2 isn't built -prof so + @# no target currently ships prof libs). + @# Override via STAGE3__TARGET_{IS_DYNAMIC,SHIPS_DYN_LIBS, + @# IS_PROFILED,SHIPS_PROF_LIBS}. @# @# Note: `$$$$` (four dollars) collapses through two layers of @# Make expansion (define-template + recipe-time) to a literal `$` @@ -1046,7 +1058,7 @@ endif @# `$$` would collapse to `$` after template expansion, then Make @# would interpret the lone `$/` in the recipe as a variable lookup @# and drop the anchor entirely (verified: PR #187 first attempt). - $(SED) -i -e 's/\]$$$$/,("target is dynamic","$(if $(STAGE3_$(1)_TARGET_IS_DYNAMIC),$(STAGE3_$(1)_TARGET_IS_DYNAMIC),YES)"),("target ships dynamic libraries","$(if $(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),$(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),YES)")]/' $$(TARGET_DIR)/lib/settings + $(SED) -i -e 's/\]$$$$/,("target is dynamic","$(if $(STAGE3_$(1)_TARGET_IS_DYNAMIC),$(STAGE3_$(1)_TARGET_IS_DYNAMIC),YES)"),("target ships dynamic libraries","$(if $(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),$(STAGE3_$(1)_TARGET_SHIPS_DYN_LIBS),YES)"),("target is profiled","$(if $(STAGE3_$(1)_TARGET_IS_PROFILED),$(STAGE3_$(1)_TARGET_IS_PROFILED),NO)"),("target ships profiling libraries","$(if $(STAGE3_$(1)_TARGET_SHIPS_PROF_LIBS),$(STAGE3_$(1)_TARGET_SHIPS_PROF_LIBS),NO)")]/' $$(TARGET_DIR)/lib/settings $$(DIST_DIR)/bin/$(1)-ghc --info diff --git a/compiler/GHC/Driver/Session.hs b/compiler/GHC/Driver/Session.hs index ec32f79b6e5b..74e96790de0e 100644 --- a/compiler/GHC/Driver/Session.hs +++ b/compiler/GHC/Driver/Session.hs @@ -134,6 +134,8 @@ module GHC.Driver.Session ( sTargetRTSLinkerOnlySupportsSharedLibs, sTargetIsDynamic, sTargetShipsDynLibs, + sTargetIsProfiled, + sTargetShipsProfLibs, GhcNameVersion(..), FileSettings(..), PlatformMisc(..), @@ -3550,8 +3552,16 @@ compilerInfo dflags ("Have native code generator", showBool $ platformNcgSupported platform), ("target has RTS linker", showBool $ platformHasRTSLinker platform), ("Target default backend", show $ platformDefaultBackend platform), - -- Whether or not we support @-dynamic-too@ - ("Support dynamic-too", showBool $ not isWindows), + -- Whether or not we support @-dynamic-too@ for this target. + -- Historically `not isWindows` (Windows tooling couldn't do + -- it). Now also gated on the per-target `sTargetIsDynamic` + -- dial — if the target isn't dynamic-capable, -dynamic-too + -- is meaningless. Keep the Windows guard as defence in depth + -- for pre-this-patch bindists on Windows that lack the key + -- and so default sTargetIsDynamic=True (the AND would + -- otherwise regress them). + ("Support dynamic-too", showBool $ not isWindows + && sTargetIsDynamic (settings dflags)), -- Whether or not we support the @-j@ flag with @--make@. ("Support parallel --make", "YES"), -- Whether or not we support "Foo from foo-0.1-XXX:Foo" syntax in @@ -3584,8 +3594,14 @@ compilerInfo dflags -- True if absent (matches pre-this-change behaviour). ("GHC Dynamic", showBool (sTargetIsDynamic (settings dflags) && sTargetShipsDynLibs (settings dflags))), - -- Whether or not GHC was compiled using -prof - ("GHC Profiled", showBool hostIsProfiled), + -- Profiling-way analogue of `GHC Dynamic`. Per-target dials + -- via `target is profiled` + `target ships profiling libraries` + -- settings keys. Drops the historical `hostIsProfiled` RTS- + -- baked-in for the same reason the dyn pair did: on a multi- + -- target bindist the shared stage2 GHC binary's prof-ness is + -- fixed but the lib trees can disagree per target. + ("GHC Profiled", showBool (sTargetIsProfiled (settings dflags) + && sTargetShipsProfLibs (settings dflags))), ("Debug on", showBool debugIsOn), ("LibDir", topDir dflags), -- This is always an absolute path, unlike "Relative Global Package DB" which is diff --git a/compiler/GHC/Platform.hs b/compiler/GHC/Platform.hs index 42f131c1bc96..972f3fb23fc1 100644 --- a/compiler/GHC/Platform.hs +++ b/compiler/GHC/Platform.hs @@ -308,6 +308,16 @@ data PlatformMisc = PlatformMisc -- 'platformMisc_targetIsDynamic' so a target can be dynamic- -- capable but not currently ship dyn artifacts (or vice versa). , platformMisc_targetShipsDynLibs :: Bool + -- | Profiling-way analogue of 'platformMisc_targetIsDynamic'. + -- Per-target settings key @"target is profiled"@. Drives + -- @ghc --info@'s @GHC Profiled@ — cabal-install reads that to + -- decide whether to enable @library-profiling@. + , platformMisc_targetIsProfiled :: Bool + -- | Profiling-way analogue of 'platformMisc_targetShipsDynLibs'. + -- Per-target settings key @"target ships profiling libraries"@. + -- Combined with 'platformMisc_targetIsProfiled' for the + -- @GHC Profiled@ report. + , platformMisc_targetShipsProfLibs :: Bool } platformSOName :: Platform -> FilePath -> FilePath diff --git a/compiler/GHC/Settings.hs b/compiler/GHC/Settings.hs index 2548c09e1e2f..db5653794314 100644 --- a/compiler/GHC/Settings.hs +++ b/compiler/GHC/Settings.hs @@ -69,6 +69,8 @@ module GHC.Settings , sTargetRTSLinkerOnlySupportsSharedLibs , sTargetIsDynamic , sTargetShipsDynLibs + , sTargetIsProfiled + , sTargetShipsProfLibs ) where import GHC.Prelude @@ -329,3 +331,13 @@ sTargetIsDynamic = platformMisc_targetIsDynamic . sPlatformMisc -- Per-target settings key @"target ships dynamic libraries"@. sTargetShipsDynLibs :: Settings -> Bool sTargetShipsDynLibs = platformMisc_targetShipsDynLibs . sPlatformMisc + +-- | Profiling-way analogue of 'sTargetIsDynamic'. Per-target settings +-- key @"target is profiled"@. +sTargetIsProfiled :: Settings -> Bool +sTargetIsProfiled = platformMisc_targetIsProfiled . sPlatformMisc + +-- | Profiling-way analogue of 'sTargetShipsDynLibs'. Per-target settings +-- key @"target ships profiling libraries"@. +sTargetShipsProfLibs :: Settings -> Bool +sTargetShipsProfLibs = platformMisc_targetShipsProfLibs . sPlatformMisc diff --git a/compiler/GHC/Settings/IO.hs b/compiler/GHC/Settings/IO.hs index 16eab618c706..2ede21587c6c 100644 --- a/compiler/GHC/Settings/IO.hs +++ b/compiler/GHC/Settings/IO.hs @@ -203,6 +203,16 @@ initSettings top_dir = do targetShipsDynLibs <- either (const $ pure True) pure $ getRawBooleanSetting settingsFile mySettings "target ships dynamic libraries" + -- Profiling-way analogues of the two dyn dials above. Drive + -- `GHC Profiled` the same way (sTargetIsProfiled && sTargetShipsProfLibs). + -- Defaults to True for backward compatibility with bindists that + -- predate the keys — matches the historical hostIsProfiled + -- behaviour when GHC was prof-built. + targetIsProfiled <- either (const $ pure True) pure $ + getRawBooleanSetting settingsFile mySettings "target is profiled" + targetShipsProfLibs <- either (const $ pure True) pure $ + getRawBooleanSetting settingsFile mySettings "target ships profiling libraries" + baseUnitId <- getSetting_raw "base unit-id" return $ Settings @@ -287,6 +297,8 @@ initSettings top_dir = do , platformMisc_targetRTSLinkerOnlySupportsSharedLibs = targetRTSLinkerOnlySupportsSharedLibs , platformMisc_targetIsDynamic = targetIsDynamic , platformMisc_targetShipsDynLibs = targetShipsDynLibs + , platformMisc_targetIsProfiled = targetIsProfiled + , platformMisc_targetShipsProfLibs = targetShipsProfLibs } , sRawSettings = settingsList From 3cf0e1be7ba512eff5df13c8b35c3631c7cae438 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Sat, 6 Jun 2026 21:57:00 +0900 Subject: [PATCH 7/7] ci: drop standalone Cross: WASM + Cross: JS jobs (cross-multi is sole) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/nix-ci.yml | 636 ++--------------------------------- 1 file changed, 29 insertions(+), 607 deletions(-) diff --git a/.github/workflows/nix-ci.yml b/.github/workflows/nix-ci.yml index 63d22798e29c..0917894dd061 100644 --- a/.github/workflows/nix-ci.yml +++ b/.github/workflows/nix-ci.yml @@ -7,13 +7,13 @@ on: - synchronize push: branches: [stable-ghc-9.14, stable-master] - # Tag pushes matching wasm32-wasi-* trigger the Cross: WASM jobs to - # additionally upload their bindist tarballs to the matching GitHub - # Release, populating the stable-haskell ghcup channel. - # multi-* trigger the Cross: MULTI job for the same reason (separate - # tag namespace, separate channel YAML). + # Tag pushes matching multi-* trigger the Cross: MULTI job to + # additionally upload its multi-target bindist tarball to the + # matching GitHub Release, populating the stable-haskell + # ghcup-multi-target-0.1.0.yaml channel. The single multi-target + # bindist replaces the previously-separate wasm32-wasi-* and + # JS standalone channels — both targets ship via multi-* now. tags: - - 'wasm32-wasi-*' - 'multi-*' workflow_dispatch: @@ -560,597 +560,21 @@ jobs: fi # --------------------------------------------------------------------------- - # Cross: JS backend (aarch64-darwin, uses static dist) - # --------------------------------------------------------------------------- - cross-js: - name: "Cross: JS / aarch64-darwin" - # Cross-builds only need the dist artifact from build. They run in parallel - # with tests — each VM is independent (separate nix daemon, separate disk). - needs: [build] - if: ${{ !cancelled() && contains(fromJSON('["success", "failure"]'), needs.build.result) }} - runs-on: [self-hosted, nix] - continue-on-error: true - - env: - EMSDK_VERSION: "3.1.74" - - steps: - - name: Clean workspace - run: | - echo "$HOME/.nix-profile/bin" >> "$GITHUB_PATH" - echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH" - echo "/usr/local/bin" >> "$GITHUB_PATH" - - echo "CABAL_DIR=$GITHUB_WORKSPACE/_build/cabal-dir" >> "$GITHUB_ENV" - - echo "=== Disk usage before cleanup ===" - df -h / || true - df -h "$GITHUB_WORKSPACE" || true - rm -rf "$GITHUB_WORKSPACE"/* "$GITHUB_WORKSPACE"/.??* || true - rm -rf ~/.cabal/store ~/.cabal-devx/store ~/.cabal-devx/packages || true - sudo rm -f /usr/local/bin/devx 2>/dev/null || true - export PATH="/nix/var/nix/profiles/default/bin:$PATH" - nix-collect-garbage -d 2>/dev/null || true - nix-store --gc 2>/dev/null || true - echo "=== Disk usage after cleanup ===" - df -h / || true - - - uses: actions/checkout@v4 - with: - submodules: "recursive" - fetch-depth: 1 - - # Minimize source tree BEFORE devx import to give devx more headroom. - - name: Minimize source tree - run: | - GIT_COMMIT_ID=$(git rev-parse HEAD) - echo "GIT_COMMIT_ID=$GIT_COMMIT_ID" >> "$GITHUB_ENV" - echo "Captured GIT_COMMIT_ID=$GIT_COMMIT_ID" - rm -rf .git - find . -name .git -type d -exec rm -rf {} + 2>/dev/null || true - rm -rf testsuite docs || true - echo "=== Disk after minimize ===" - df -h / || true - - - name: Ensure devx prerequisites - run: | - export PATH="$HOME/.nix-profile/bin:/nix/var/nix/profiles/default/bin:$PATH" - command -v zstd || nix-env -iA nixpkgs.zstd - sudo mkdir -p /usr/local/bin - - - uses: input-output-hk/actions/devx@latest - with: - platform: aarch64-darwin - compiler-nix-name: 'ghc98' - minimal: true - ghc: true - - - name: Download dist - uses: actions/download-artifact@v4 - with: - name: aarch64-darwin-dynamic0-dist - path: _build/dist - - - name: Set up stage2 from dist - run: | - chmod +x _build/dist/bin/* - mkdir -p _build/stage2/bin _build/stage2/lib - for exe in _build/dist/bin/*; do - ln -sf "$(pwd)/$exe" "_build/stage2/bin/$(basename $exe)" - done - if [[ -f _build/dist/lib/settings ]]; then - cp -rfp _build/dist/lib/settings _build/stage2/lib/ - fi - - - name: Update hackage - shell: devx {0} - run: | - set -eo pipefail - export CABAL_DIR="$GITHUB_WORKSPACE/_build/cabal-dir" - mkdir -p "$CABAL_DIR" - _build/dist/bin/cabal update - - - name: Free disk for cross build - run: | - rm -rf "${CABAL_DIR:-~/.cabal-devx}/packages" "${CABAL_DIR:-~/.cabal-devx}/logs" "${CABAL_DIR:-~/.cabal-devx}/store" || true - rm -rf ~/.cabal-devx/packages ~/.cabal-devx/logs ~/.cabal-devx/store || true - rm -rf ~/.cabal/store || true - echo "=== Disk before emscripten install ===" - df -h / || true - df -h /Volumes/WorkSpace || true - - - name: Install emscripten - shell: devx {0} - run: | - set -eux - export TMPDIR="$GITHUB_WORKSPACE/_build/tmp" - mkdir -p "$TMPDIR" - git clone --depth 1 --branch ${{ env.EMSDK_VERSION }} https://github.com/emscripten-core/emsdk.git - cd emsdk - ./emsdk install ${{ env.EMSDK_VERSION }} - ./emsdk activate ${{ env.EMSDK_VERSION }} - - - name: Build JS cross libraries - shell: devx {0} - run: | - set -eux - export CABAL_DIR="$GITHUB_WORKSPACE/_build/cabal-dir" - export TMPDIR="$GITHUB_WORKSPACE/_build/tmp" - mkdir -p "$TMPDIR" - source emsdk/emsdk_env.sh - make DIST_BUILD=1 \ - CABAL=$PWD/_build/dist/bin/cabal \ - GHC_TOOLCHAIN_BIN=$PWD/_build/dist/bin/ghc-toolchain-bin \ - DERIVE_CONSTANTS_BIN=$PWD/_build/dist/bin/deriveConstants \ - GENAPPLY_BIN=$PWD/_build/dist/bin/genapply \ - HAPPY_TEMPLATE_DIR=$PWD/_build/dist/share/happy-lib/data \ - stage3-javascript-unknown-ghcjs - - - name: Smoke test - shell: devx {0} - run: | - set -eo pipefail - source emsdk/emsdk_env.sh - echo 'main = putStrLn "Hello from JS backend"' > /tmp/hello.hs - # JS backend creates a .jsexe directory; the entry point is all.js - _build/dist/bin/javascript-unknown-ghcjs-ghc /tmp/hello.hs -o /tmp/hello - node /tmp/hello.jsexe/all.js - - - name: Upload JS cross artifacts - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: aarch64-darwin-cross-js - retention-days: 1 - path: _build/dist/lib/targets/javascript-unknown-ghcjs - - # --------------------------------------------------------------------------- - # Cross: WASM backend — matrixed across all build hosts so each one - # produces a host-specific bindist tarball that can be shipped through - # the stable-haskell ghcup channel. - # --------------------------------------------------------------------------- - cross-wasm: - name: "Cross: WASM / ${{ matrix.plat }}" - # Cross-builds only need the dist artifact from build. They run in parallel - # with tests — each VM/runner is independent (separate nix daemon, etc.). - needs: [build] - if: ${{ !cancelled() && contains(fromJSON('["success", "failure"]'), needs.build.result) }} - runs-on: ${{ fromJSON(matrix.runner) }} - continue-on-error: true - - strategy: - fail-fast: false - matrix: - include: - - { plat: aarch64-darwin, devx-plat: aarch64-darwin, runner: '["self-hosted", "nix"]' } - - { plat: x86_64-linux, devx-plat: x86_64-linux, runner: '"ubuntu-latest"' } - - { plat: aarch64-linux, devx-plat: aarch64-linux, runner: '"ubuntu-24.04-arm"' } - - steps: - - name: Clean workspace - run: | - echo "$HOME/.nix-profile/bin" >> "$GITHUB_PATH" - echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH" - echo "/usr/local/bin" >> "$GITHUB_PATH" - - echo "CABAL_DIR=$GITHUB_WORKSPACE/_build/cabal-dir" >> "$GITHUB_ENV" - - echo "=== Disk usage before cleanup ===" - df -h / || true - df -h "$GITHUB_WORKSPACE" || true - rm -rf "$GITHUB_WORKSPACE"/* "$GITHUB_WORKSPACE"/.??* || true - rm -rf ~/.cabal/store ~/.cabal-devx/store ~/.cabal-devx/packages || true - rm -rf ~/.ghc-wasm || true - sudo rm -f /usr/local/bin/devx 2>/dev/null || true - export PATH="/nix/var/nix/profiles/default/bin:$PATH" - nix-collect-garbage -d 2>/dev/null || true - nix-store --gc 2>/dev/null || true - echo "=== Disk usage after cleanup ===" - df -h / || true - - - uses: actions/checkout@v4 - with: - submodules: "recursive" - fetch-depth: 1 - - # Minimize source tree BEFORE devx import — same strategy as cross-js. - - name: Minimize source tree - run: | - GIT_COMMIT_ID=$(git rev-parse HEAD) - echo "GIT_COMMIT_ID=$GIT_COMMIT_ID" >> "$GITHUB_ENV" - echo "Captured GIT_COMMIT_ID=$GIT_COMMIT_ID" - rm -rf .git - find . -name .git -type d -exec rm -rf {} + 2>/dev/null || true - rm -rf testsuite docs || true - echo "=== Disk after minimize ===" - df -h / || true - - - name: Ensure devx prerequisites - run: | - export PATH="$HOME/.nix-profile/bin:/nix/var/nix/profiles/default/bin:$PATH" - command -v zstd || nix-env -iA nixpkgs.zstd - sudo mkdir -p /usr/local/bin - - - uses: input-output-hk/actions/devx@latest - with: - platform: ${{ matrix.devx-plat }} - compiler-nix-name: 'ghc98' - minimal: true - ghc: true - - - name: Download dist - uses: actions/download-artifact@v4 - with: - # dynamic1 stage2 (with .dyn_hi for native base/filepath/process/etc.) - # is needed for two reasons: - # 1. stage3 build-side: when happy-lib / alex etc. compile with - # shared:True (via DYNAMIC=1 settings), cabal asks this build - # compiler for native Prelude.dyn_hi. - # 2. wasm bindist runtime: bin/wasm32-unknown-wasi-ghc is a - # Makefile symlink to bin/ghc which `tar -czhf` dereferences; - # the dereferenced binary is dyn-linked against libHS*.so - # files from THIS stage2. The wasm tar rule (Makefile) also - # copies lib/$(HOST_PLATFORM)/ from THIS stage2 alongside, - # so the binary's @rpath resolves at end-user install time. - name: ${{ matrix.plat }}-dynamic1-dist - path: _build/dist - - - name: Set up stage2 from dist - run: | - chmod +x _build/dist/bin/* - mkdir -p _build/stage2/bin _build/stage2/lib - for exe in _build/dist/bin/*; do - ln -sf "$(pwd)/$exe" "_build/stage2/bin/$(basename $exe)" - done - if [[ -f _build/dist/lib/settings ]]; then - cp -rfp _build/dist/lib/settings _build/stage2/lib/ - fi - - - name: Update hackage - shell: devx {0} - run: | - set -eo pipefail - export CABAL_DIR="$GITHUB_WORKSPACE/_build/cabal-dir" - mkdir -p "$CABAL_DIR" - _build/dist/bin/cabal update - - - name: Free disk for cross build - run: | - rm -rf "${CABAL_DIR:-~/.cabal-devx}/packages" "${CABAL_DIR:-~/.cabal-devx}/logs" "${CABAL_DIR:-~/.cabal-devx}/store" || true - rm -rf ~/.cabal-devx/packages ~/.cabal-devx/logs ~/.cabal-devx/store || true - rm -rf ~/.cabal/store || true - echo "=== Disk before wasi-sdk install ===" - df -h / || true - df -h /Volumes/WorkSpace || true - - - name: Install wasi-sdk - shell: devx {0} - run: | - set -eux - export TMPDIR="$GITHUB_WORKSPACE/_build/tmp" - mkdir -p "$TMPDIR" - # ghc-wasm-meta's bootstrap.sh needs jq + unzip; the smoke test below - # also needs node + wasmtime + wasm-objdump. The devx shell scrubs the - # system PATH so we install or expose them per host kind. - # - # Pick the installer based on uname -s — devx scrubs /usr/bin on both - # macOS and Linux, so `command -v apt-get` would falsely return no on - # both. Also CRUCIAL: on darwin we must NOT add /usr/bin to PATH, or - # BSD `find` (no `-mindepth`) takes precedence over GNU find used in - # Makefile:476 / 485 and the dist-copy step breaks. - case "$(uname -s)" in - Linux) - export PATH="$HOME/.nix-profile/bin:/usr/local/bin:/usr/bin:/bin:$PATH" - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends \ - jq unzip zstd wabt - # post-link.mjs uses import.meta.filename (added Node 20.11+), - # so we need Node >= 20.11. Ubuntu noble's apt nodejs is - # 18.19.1, and runner-preinstalled Node 20.x at - # /opt/hostedtoolcache isn't visible inside devx for the smoke - # test. Install Node 22 from NodeSource unconditionally so we - # land /usr/bin/node — devx scrubs random tool caches but - # /usr/bin survives via the smoke step's PATH append. - curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - - sudo apt-get install -y --no-install-recommends nodejs - if ! command -v wasmtime >/dev/null 2>&1; then - curl -sSL https://wasmtime.dev/install.sh | bash - export PATH="$HOME/.wasmtime/bin:$PATH" - echo "$HOME/.wasmtime/bin" >> "$GITHUB_PATH" - fi - ;; - Darwin) - export PATH="$HOME/.nix-profile/bin:/nix/var/nix/profiles/default/bin:$PATH" - for tool in jq unzip zstd node wasmtime; do - if ! command -v "$tool" >/dev/null 2>&1; then - pkg="$tool" - [ "$tool" = "node" ] && pkg="nodejs" - echo "Installing $pkg via nix-env..." - /nix/var/nix/profiles/default/bin/nix-env -iA "nixpkgs.$pkg" - fi - done - if ! command -v wasm-objdump >/dev/null 2>&1; then - echo "Installing wabt (wasm-objdump) via nix-env..." - /nix/var/nix/profiles/default/bin/nix-env -iA nixpkgs.wabt - fi - ;; - esac - curl -fsSL https://gitlab.haskell.org/ghc/ghc-wasm-meta/-/raw/master/bootstrap.sh | \ - FLAVOUR=9.12 PREFIX=$HOME/.ghc-wasm sh - - - name: Build WASM cross libraries - shell: devx {0} - run: | - set -eux - export CABAL_DIR="$GITHUB_WORKSPACE/_build/cabal-dir" - export TMPDIR="$GITHUB_WORKSPACE/_build/tmp" - mkdir -p "$TMPDIR" - # Create wasm32-unknown-wasi-* symlinks for LLVM tools in wasi-sdk so they - # don't clash with devx's llvm-ar/llvm-nm/llvm-ranlib (which have - # different capabilities). Also symlink clang/clang++ from the - # wasm32-wasi- name bootstrap actually creates to the canonical autoconf - # triple wasm32-unknown-wasi- that ghc-toolchain-bin searches for — - # without these our build fails at "wasm32-unknown-wasi-clang not found - # in search path". - WASI_BIN="$HOME/.ghc-wasm/wasi-sdk/bin" - for tool in ar nm ranlib strip; do - ln -sf "$WASI_BIN/llvm-$tool" "$WASI_BIN/wasm32-unknown-wasi-$tool" - done - for tool in clang clang++; do - ln -sf "$WASI_BIN/wasm32-wasi-$tool" "$WASI_BIN/wasm32-unknown-wasi-$tool" - done - # wasi-sdk at END of PATH so native tools (cc, clang) resolve to the - # host compiler. WASM tools (wasm32-unknown-wasi-clang, etc.) have unique - # prefixed names that don't clash. - export PATH=$PATH:$WASI_BIN - # DYNAMIC=1 makes ./configure pass --enable-dynamic, which expands - # cabal.project.stage3.settings to: - # package * { shared:True; executable-dynamic:True } - # constraints: rts +dynamic - # …so the stage3 wasm build produces both .hi + .dyn_hi files AND - # ships .so libraries. Without it the bindist is static-only, and - # end-user TH-heavy apps (miso, jsaddle, aeson, …) hit - # error: [GHC-47808] Failed to load dynamic interface file for X - # because cabal sets `shared: True` for wasm32 user packages but - # the system base/ghc-internal/etc. ship no .dyn_hi to link against. - make DYNAMIC=1 DIST_BUILD=1 \ - CABAL=$PWD/_build/dist/bin/cabal \ - GHC_TOOLCHAIN_BIN=$PWD/_build/dist/bin/ghc-toolchain-bin \ - DERIVE_CONSTANTS_BIN=$PWD/_build/dist/bin/deriveConstants \ - GENAPPLY_BIN=$PWD/_build/dist/bin/genapply \ - HAPPY_TEMPLATE_DIR=$PWD/_build/dist/share/happy-lib/data \ - stage3-wasm32-unknown-wasi - - - name: Smoke test - if: success() - shell: devx {0} - run: | - set -eo pipefail - # devx scrubs the user-installed tool locations. Add them back after - # devx's $PATH so devx wins where there's overlap, but the per-OS - # installer outputs are still findable: - # Linux: apt installs node / wasm-objdump to /usr/bin; - # curl wasmtime installer drops into ~/.wasmtime/bin - # Darwin: nix-env -iA puts everything (wasmtime, wabt, node, …) - # into ~/.nix-profile/bin - # /usr/bin is not added on Darwin because it would shadow nix - # coreutils with BSD versions (no -mindepth on find, etc.). - case "$(uname -s)" in - Linux) export PATH="$PATH:$HOME/.wasmtime/bin:$HOME/.ghc-wasm/wasi-sdk/bin:/usr/bin" ;; - Darwin) export PATH="$PATH:$HOME/.nix-profile/bin:$HOME/.ghc-wasm/wasi-sdk/bin" ;; - esac - echo 'main = putStrLn "Hello from WASM backend"' > /tmp/hello.hs - _build/dist/bin/wasm32-unknown-wasi-ghc /tmp/hello.hs -o /tmp/hello.wasm - - # Verify the .wasm binary was produced - ls -lh /tmp/hello.wasm - file /tmp/hello.wasm - - # Diagnostic: inspect WASM imports to determine execution strategy. - # If ghc_wasm_jsffi imports are present, we need post-link.mjs + Node.js. - # If not, wasmtime can run it directly as a pure WASI module. - echo "=== WASM import sections ===" - wasm-objdump -x /tmp/hello.wasm | head -60 || true - echo "=== Checking for ghc_wasm_jsffi imports ===" - - if wasm-objdump -x /tmp/hello.wasm 2>/dev/null | grep -q 'ghc_wasm_jsffi'; then - echo "JSFFI imports detected → using post-link.mjs + Node.js" - - # GHC WASM executables with ghc_wasm_jsffi custom sections require - # JavaScript FFI glue at instantiation time. The post-link.mjs tool - # (installed in libdir, maintained upstream at utils/jsffi/post-link.mjs) - # parses these sections and generates a JavaScript ESM module providing - # the ghc_wasm_jsffi imports. - # - # Workflow (from docs/users_guide/wasm.rst §"JSFFI"): - # 1. Compile: wasm32-unknown-wasi-ghc hello.hs -o hello.wasm - # 2. Post-link: $(ghc --print-libdir)/post-link.mjs -i hello.wasm -o hello.mjs - # 3. Run: node runner.mjs - WASM_LIBDIR=_build/dist/lib/targets/wasm32-unknown-wasi/lib - - # Generate JavaScript FFI glue from WASM custom sections - node "$WASM_LIBDIR/post-link.mjs" -i /tmp/hello.wasm -o /tmp/hello.mjs - - # Instantiate WASM module with JSFFI + WASI imports, then run main. - # This mirrors the knot-tying pattern from docs/users_guide/wasm.rst: - # __exports starts empty, WASM is instantiated with JSFFI imports - # (which capture __exports by reference), then instance exports are - # assigned back into __exports before calling wasi.start(). - node --input-type=module -e ' - import { readFile } from "node:fs/promises"; - import { WASI } from "node:wasi"; - const mod = await WebAssembly.compile(await readFile("/tmp/hello.wasm")); - const jsffi = (await import("/tmp/hello.mjs")).default; - const wasi = new WASI({ version: "preview1", args: ["hello.wasm"] }); - let __exports = {}; - const instance = await WebAssembly.instantiate(mod, { - ghc_wasm_jsffi: jsffi(__exports), - wasi_snapshot_preview1: wasi.wasiImport, - }); - Object.assign(__exports, instance.exports); - wasi.start(instance); - ' - else - echo "No JSFFI imports → running with wasmtime (pure WASI module)" - - # Per docs/users_guide/wasm.rst: "the same toolchain still generates - # self-contained wasm32-unknown-wasi modules by default" — these can be run - # directly with any WASI-compatible runtime. - wasmtime run /tmp/hello.wasm - fi - - # Package the full ghcup-shippable bindist tarball — same recipe the - # Makefile uses locally (tar czhf -h dereferences the wasm-prefixed - # symlinks into real files, includes bin/ + lib/ + configure + Makefile - # + relocate.sh). Renamed with the host triple to disambiguate. - # - # NOTE: the tarball Makefile target depends on `stage3-$(plat)` which is - # .PHONY, so it re-runs ghc-toolchain-bin. That tool needs - # wasm32-unknown-wasi-clang on PATH — the symlinks were created by the - # previous step but PATH doesn't persist across CI steps, so we re-add - # the wasi-sdk bin dir here. - - name: Package bindist tarball - shell: devx {0} - if: ${{ !cancelled() }} - run: | - set -eux - export CABAL_DIR="$GITHUB_WORKSPACE/_build/cabal-dir" - export TMPDIR="$GITHUB_WORKSPACE/_build/tmp" - # Keep devx's $PATH first so `make` resolves to devx's GNU Make (4.4+) - # not Ubuntu's /usr/bin/make (4.3 — no $(let), silently returns empty - # which corrupts LIB_NAME_GLOB and breaks the package.conf copy phase). - export PATH="$PATH:$HOME/.ghc-wasm/wasi-sdk/bin" - rm -f _build/dist/ghc-wasm32-unknown-wasi.tar.gz - # The same five DIST_BUILD vars the previous "Build WASM cross - # libraries" step uses — stage3 phony deps invoke configure which - # otherwise can't find deriveConstants/genapply/happy templates. - # DYNAMIC=1 mirrors the Build step so configure re-runs with the - # same --enable-dynamic flag (stage3-$plat is PHONY → re-runs). - make DYNAMIC=1 DIST_BUILD=1 \ - CABAL=$PWD/_build/dist/bin/cabal \ - GHC_TOOLCHAIN_BIN=$PWD/_build/dist/bin/ghc-toolchain-bin \ - DERIVE_CONSTANTS_BIN=$PWD/_build/dist/bin/deriveConstants \ - GENAPPLY_BIN=$PWD/_build/dist/bin/genapply \ - HAPPY_TEMPLATE_DIR=$PWD/_build/dist/share/happy-lib/data \ - _build/dist/ghc-wasm32-unknown-wasi.tar.gz - # rename with the host triple so we ship one tarball per host - cp _build/dist/ghc-wasm32-unknown-wasi.tar.gz \ - _build/dist/ghc-wasm32-unknown-wasi-${{ matrix.plat }}.tar.gz - ls -lh _build/dist/ghc-wasm32-unknown-wasi-${{ matrix.plat }}.tar.gz - echo "SHA256:" - shasum -a 256 _build/dist/ghc-wasm32-unknown-wasi-${{ matrix.plat }}.tar.gz - - # The wasm cross-compiler is built inside `devx` (a nix-shell), so all - # ELF binaries' interpreter (`PT_INTERP`) is pinned to a `/nix/store/.../ - # ld-linux-*.so.*` path. On any non-nix Linux system that path doesn't - # exist, and the kernel fails to load the binary with ENOENT — manifesting - # as `cannot execute: required file not found`. The same broad class of - # problem `fixup-nix-deps` solves on Darwin (Mach-O dyld refs). - # - # Patchelf the interpreter back to the canonical system ld-linux path and - # drop the RPATH so dynamic libs resolve through ldconfig (libgmp10, - # libffi8, libc, libm — all stock on any modern Linux distro). - # - # Skipped on Darwin: Mach-O doesn't pin an interpreter path the way ELF - # does, so darwin bindists are portable as-is. - - name: Normalize ELF interpreters for portability (Linux only) - if: ${{ !cancelled() && contains(matrix.plat, 'linux') }} - shell: devx {0} - run: | - set -eux - # patchelf is in apt on ubuntu — keep PATH outside devx for sudo. - export PATH="$PATH:/usr/bin:/usr/sbin" - if ! command -v patchelf >/dev/null 2>&1; then - sudo apt-get install -y --no-install-recommends patchelf - fi - case "${{ matrix.plat }}" in - x86_64-linux) INTERP=/lib64/ld-linux-x86-64.so.2; HOST_DIR=x86_64-unknown-linux ;; - aarch64-linux) INTERP=/lib/ld-linux-aarch64.so.1; HOST_DIR=aarch64-unknown-linux ;; - *) echo "::error::unexpected matrix.plat=${{ matrix.plat }}"; exit 1 ;; - esac - - TGZ=_build/dist/ghc-wasm32-unknown-wasi-${{ matrix.plat }}.tar.gz - STAGE="$(mktemp -d)" - tar -C "$STAGE" -xzf "$TGZ" - - echo "═══ Patchelfing binaries to interpreter $INTERP ═══" - # Scan everything executable; patchelf only on dynamically-linked ELFs - # whose interpreter still points into /nix/store. Also set an - # @ORIGIN-relative rpath so the dyn-linked binary can locate the - # libHS*.so files we ship in lib/$HOST_DIR/ (mirrors the - # @executable_path/../lib/$(HOST_PLATFORM) rpath stage2 sets on - # Darwin via install_name_tool). - patched=0 - while IFS= read -r bin; do - if file -L "$bin" 2>/dev/null \ - | grep -q "ELF.*dynamically linked.*interpreter /nix/store"; then - echo " patching: ${bin#$STAGE/}" - patchelf --set-interpreter "$INTERP" "$bin" - patchelf --remove-rpath "$bin" - patchelf --force-rpath --set-rpath "\$ORIGIN/../lib/$HOST_DIR" "$bin" - patched=$((patched + 1)) - fi - done < <(find "$STAGE" -type f -executable) - echo "═══ Patched $patched binaries ═══" - test "$patched" -gt 0 || { echo "::error::no ELF binaries patched — something is wrong"; exit 1; } - - # Also fix rpath on the shipped host .so files (lib/$HOST_DIR/*.so): - # they reference each other (e.g. libHSghc.so needs libHSrts.so) - # and use $ORIGIN-relative rpath at stage2 build time. patchelf - # may have left a nix-store rpath in place; replace with the - # local-$ORIGIN form so cross-library lookups work from the - # bindist install location. - echo "═══ Setting rpath on lib/$HOST_DIR/*.so ═══" - so_patched=0 - for so in "$STAGE"/lib/"$HOST_DIR"/*.so; do - [ -f "$so" ] || continue - patchelf --force-rpath --set-rpath "\$ORIGIN" "$so" 2>/dev/null || true - so_patched=$((so_patched + 1)) - done - echo "═══ Set rpath on $so_patched shared libs ═══" - - # Repack — tar czhf was used to dereference symlinks originally; the - # staging dir is already real files (tar xzf preserves that), so plain - # tar czf is fine here. Keep the same filename for downstream uploads. - rm "$TGZ" - tar -C "$STAGE" -czf "$TGZ" . - ls -lh "$TGZ" - echo "SHA256 after patchelf:" - shasum -a 256 "$TGZ" - - - name: Upload WASM cross artifacts - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: ${{ matrix.plat }}-cross-wasm - retention-days: 30 - path: _build/dist/ghc-wasm32-unknown-wasi-${{ matrix.plat }}.tar.gz - - # On a tag push matching wasm32-wasi-*, also upload the bindist - # tarball directly to that GitHub Release so the ghcup channel can - # point at it. Other triggers (PR pushes, branch pushes) only produce - # workflow artifacts which is fine for review purposes. - - name: Upload bindist to release (on tag push) - if: ${{ startsWith(github.ref, 'refs/tags/wasm32-wasi-') }} - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_name }} - files: _build/dist/ghc-wasm32-unknown-wasi-${{ matrix.plat }}.tar.gz - fail_on_unmatched_files: true - - # --------------------------------------------------------------------------- - # Cross: MULTI — multi-target bindist combining native + wasm + JS. + # Cross: MULTI — the only Cross job. Builds the multi-target bindist + # combining native + wasm32-unknown-wasi + javascript-unknown-ghcjs in + # one tarball (argv[0] dispatched at runtime). Replaces the previously- + # separate Cross: WASM + Cross: JS jobs — those produced standalone + # bindists that we no longer ship; the multi-target bindist is the + # single end-user-facing artifact and covers both targets. # - # Reuses the Cross: WASM machinery (dynamic1 stage2 download, devx shell, - # wasi-sdk + Node install, patchelf for ELF interpreter + $ORIGIN rpath) - # and adds emscripten install for the JS target. The combined Makefile - # rule `_build/dist/ghc-multi-target.tar.gz` depends on both - # stage3-wasm32-unknown-wasi AND stage3-javascript-unknown-ghcjs, so - # this job builds BOTH cross trees + the multi-target tarball in one - # CI cycle. + # Per-host: dynamic1 stage2 download, devx shell, wasi-sdk + Node + # install, emscripten install, then `make stage3-wasm32-unknown-wasi + # stage3-javascript-unknown-ghcjs` + the combined `ghc-multi-target. + # tar.gz` Makefile rule. patchelf adjusts ELF interpreter + $ORIGIN + # rpath on the linux runners. # - # `continue-on-error: true` matches Cross: WASM — failures are - # informative but don't block the main pipeline. + # `continue-on-error: true` — failures are informative but don't + # block the main pipeline. # --------------------------------------------------------------------------- cross-multi: name: "Cross: MULTI / ${{ matrix.plat }}" @@ -1160,10 +584,8 @@ jobs: continue-on-error: true env: - # Pinned same as Cross: JS — emsdk's git tag, used in the - # `Install emscripten` step's `git clone --branch ${{ env.EMSDK_VERSION }}`. - # Workflow-level env doesn't exist on this workflow; each job - # needing EMSDK_VERSION declares it itself. + # emsdk's git tag — used by the `Install emscripten` step's + # `git clone --branch ${{ env.EMSDK_VERSION }}`. EMSDK_VERSION: "3.1.74" strategy: @@ -1225,7 +647,7 @@ jobs: - name: Download dist uses: actions/download-artifact@v4 with: - # dynamic1 stage2 (same rationale as Cross: WASM): + # dynamic1 stage2: # 1. happy-lib / alex etc. need native Prelude.dyn_hi at build-side # 2. the dyn-linked native binary becomes bin/ghc + bin/-ghc # in the multi-target bindist and needs lib/$(HOST_PLATFORM) @@ -1259,7 +681,7 @@ jobs: df -h / || true # Install wasi-sdk (wasm-target C toolchain) + Node 22 + wasmtime, same - # logic as Cross: WASM since this job builds the wasm half of the + # logic since this job builds the wasm half of the # multi-target bindist. - name: Install wasi-sdk + Node 22 shell: devx {0} @@ -1290,7 +712,7 @@ jobs: FLAVOUR=9.12 PREFIX=$HOME/.ghc-wasm sh # Install emscripten (JS-target C toolchain). EMSDK_VERSION is set in - # the workflow env (same as Cross: JS). + # the workflow env. - name: Install emscripten shell: devx {0} run: | @@ -1311,7 +733,7 @@ jobs: # Source emscripten env (provides emcc on PATH) source emsdk/emsdk_env.sh # Symlink wasi-sdk's wasm32-wasi-* tools as wasm32-unknown-wasi-* - # (same as Cross: WASM — autoconf canonical triple bridging). + #. WASI_BIN="$HOME/.ghc-wasm/wasi-sdk/bin" for tool in ar nm ranlib strip; do ln -sf "$WASI_BIN/llvm-$tool" "$WASI_BIN/wasm32-unknown-wasi-$tool" @@ -1320,7 +742,7 @@ jobs: ln -sf "$WASI_BIN/wasm32-wasi-$tool" "$WASI_BIN/wasm32-unknown-wasi-$tool" done export PATH=$PATH:$WASI_BIN - # DYNAMIC=1 for the same reason as Cross: WASM — both wasm and JS + # DYNAMIC=1 for both wasm and JS # targets need .dyn_hi for end-user TH builds (miso, aeson, ...). make DYNAMIC=1 DIST_BUILD=1 \ CABAL=$PWD/_build/dist/bin/cabal \ @@ -1329,7 +751,7 @@ jobs: GENAPPLY_BIN=$PWD/_build/dist/bin/genapply \ HAPPY_TEMPLATE_DIR=$PWD/_build/dist/share/happy-lib/data \ _build/dist/ghc-multi-target.tar.gz - # Rename to add platform suffix (matches Cross: WASM scheme). + # Rename to add platform suffix. cp _build/dist/ghc-multi-target.tar.gz \ _build/dist/ghc-multi-target-${{ matrix.plat }}.tar.gz ls -lh _build/dist/ghc-multi-target-${{ matrix.plat }}.tar.gz @@ -1373,7 +795,7 @@ jobs: echo "═══ Patched $patched binaries ═══" test "$patched" -gt 0 || { echo "::error::no ELF binaries patched"; exit 1; } - # Fix rpath on the shipped host .so files (same as Cross: WASM). + # Fix rpath on the shipped host .so files. echo "═══ Setting rpath on lib/$HOST_DIR/*.so ═══" so_patched=0 for so in "$STAGE"/lib/"$HOST_DIR"/*.so; do @@ -1448,7 +870,7 @@ jobs: path: _build/dist/ghc-multi-target-${{ matrix.plat }}.tar.gz # On a tag push matching multi-*, upload to that GitHub Release. - # (Separate tag namespace from wasm32-wasi-* so the two channels + # (Separate tag namespace from the (no-longer-shipped) wasm32-wasi-* so the channels # stay independent.) - name: Upload bindist to release (on tag push) if: ${{ startsWith(github.ref, 'refs/tags/multi-') }}