Skip to content

Add stable-haskell GHC (ghc914-sh): GHC 9.14 built with cabalProject instead of hadrian; darwin→linux cross via hyper-linux - #2528

Open
hamishmack wants to merge 74 commits into
masterfrom
hkm/stable-haskell
Open

Add stable-haskell GHC (ghc914-sh): GHC 9.14 built with cabalProject instead of hadrian; darwin→linux cross via hyper-linux#2528
hamishmack wants to merge 74 commits into
masterfrom
hkm/stable-haskell

Conversation

@hamishmack

@hamishmack hamishmack commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR adds haskell-nix.compiler.sghc914, a GHC 9.14 compiler built from the
stable-haskell/ghc stable-ghc-9.14
branch using haskell.nix's own cabalProject machinery — no hadrian. It
replicates the two-stage bootstrap from that branch's Makefile /
cabal.project.* files:

  • stage1 — GHC executables built with the boot compiler (ghc9103)
  • stage2 — all boot libraries and executables built with stage1
  • cross — target boot libraries built with a cross-targeting stage1;
    the final cross compiler is assembled from native stage1 binaries +
    target settings (via ghc-toolchain) + the target package DB

Every cabal component is its own derivation, so:

  • incremental rebuilds after a compiler change are per-component,
  • downstream closures can reference subsets of the boot libraries,
  • the full compiler closure is 2.4 GiB vs 3.9 GiB for the hadrian-built
    ghc9141 on aarch64-darwin (~38% smaller).

Verified

Target Status
native aarch64-darwin ✅ builds; compiles & runs programs (hello via hackage-package)
wasi32 cross ✅ builds; hello.wasm runs under wasmtime
ghcjs, musl32/64, static, aarch64-multiplatform(-musl), aarch64/armv7a-android, mingwW64, ucrt64, x86_64-linux native ✅ full plans instantiate (hydraJobs...hello.drvPath); builds not yet run

sghc914 is added to ci.nix (unstable, runTests = true).

How it works

  • Source pins (lazy-inputs/): sghc914 (the GHC tree, no submodules —
    upstream dropped them) plus sghc914-cabal / sghc914-hpc-bin /
    sghc914-hsc2hs for the source-repository-packages the project files
    reference, wired into the stage projects via inputMap (no rev fetching at
    eval time, works with the moving stable-haskell/master Cabal branch).
  • overlays/stable-haskell.nix (new, ~1900 lines) holds everything:
    configuredSrc (autoreconf/configure + project-file merging), the three
    cabalProject' instances, compiler assembly, and target-specific fixes
    (wasm libffi, ghcjs via emscripten, Windows dual-universe handling — see
    below).
  • Plan-time installed-package dump: the boot libraries that upstream pins
    as direct hackage tarball URLs are no longer in the GHC tree, so the
    synthesised ghc-pkg dump would miss them. A dumpSrc view (configured
    tree + those packages' .cabal files) is exposed via the final compilers'
    raw-src passthru so user-project plans see the true installed set.

New general-purpose features

  • replace-hackage-tarball-urls project option (default off): rewrites
    https://hackage.haskell.org/package/NAME-VER/NAME-VER.tar.gz entries in a
    project's packages: stanza to local store paths fetched via hackage.nix
    (haskell-nix.hackageTarball), so plan-to-nix needs no network and the
    pinned versions need not exist at the project's index-state.
  • haskell-nix.hackageTarball { name; version; }: fetch + unpack an exact
    hackage release using the sha256 hackage.nix already knows.
  • Dev shells for GHC hacking (haskell-nix.sghc914-shells): from-boot
    (boot GHC as GHC0, make stage1 from a clean tree) and from-stage1
    (haskell.nix's cached stage1 as STAGE1_PATH, rebuild only stage2 with
    cabal). Consumed by a flake.nix in the GHC checkout.

Mainline fixes included (each is a standalone commit, cherry-pickable into separate PRs on request)

  • modules/install-plan/redirect.nix: treat TargetDisabledByUser like
    TargetNotBuildable — previously a package whose only plan target was
    disabled (e.g. unix on Windows) crashed evaluation with
    "expected a set but found a string".
  • lib/system-nixpkgs-map.nix: GHC 9.14 rts / ghc-internal extra-libraries —
    c/rt/dl on Linux; wsock32/dbghelp/psapi/ntdll/ucrt/mingw32
    on Windows.
  • overlays/haskell.nix: guard hadrianProject / iserv-proxy-exes
    existence in roots (sghc914 has neither).

Windows notes (the interesting part)

On Windows targets cabal resolves the Win32 → hsc2hs → process → Win32
build-tool cycle by solving a second, older universe inside the
Win32:hsc2hs:exe scope (old Win32 predates build-tool-depends). This is
correct solver behaviour — pinning any.hsc2hs makes the plan unsatisfiable —
so the overlay tolerates it: boot libraries are registered into the compiler
DB by versioned key, and the exe-scope duplicates stay private to the tool
build. Cross-TH (templateHaskell / iserv-proxy-exes) required registering
the compiler attr in cross-host package sets and cutting a recursion in
crossBootProject (it builds the libraries the compiler is assembled from,
so it can never use the TH wrapper itself).

Known issues / follow-ups

  • The built compiler reports version 9.14 while the overlay pins 9.14.0
    (ghcVersionFull) — reconcile if version-keyed paths misbehave.
  • x86_64-linux native build expected to hit the known rts -mavx2
    per-file-CMM-flags issue; Windows / ghcjs / android builds not yet run
    (plans verified only); wine/qemu iserv TH untested.
  • No materialization for sghc914 yet.

How to test

# native compiler + smoke test
nix build .#hydraJobs.aarch64-darwin.unstable.sghc914.native.hello

# wasm cross
nix build .#hydraJobs.aarch64-darwin.unstable.sghc914.wasi32.hello
wasmtime result/bin/hello.wasm

# dev shell for hacking on the GHC tree (in a stable-haskell/ghc checkout)
nix develop  # from-stage1: prebuilt stage1, rebuild only stage2

Update — renamed ghc914-sh, darwin→linux cross, v2 slice builder (2026-07)

Since the original description this PR has grown; latest master is merged in.

Renamed sghc914ghc914-sh

The compiler attr, ci.nix, the dev shells, and the lazy-inputs/ pins now use
ghc914-sh throughout. Compilers built with cabalProject set
passthru.isStableHaskell = true.

darwin → linux cross-compilation via hyper-linux (hl)

aarch64-darwin build hosts can now cross-compile to
aarch64-unknown-linux-musl (fully static) and run the resulting Linux ELFs —
including the Template Haskell iserv interpreter — directly on Apple Silicon via
Hypervisor.framework. qemu user-mode doesn't exist on macOS, so hl fills the
"emulator" slot in the TH/test path:

  • overlays/linux-cross.nix gains a pipeCommand argument (defaults to
    qemu-<suffix>; when passed, qemu is never forced) — the wrapper/wrapGhc
    logic is otherwise emulator-agnostic.
  • overlays/armv6l-linux.nix selects ${pkgsBuildBuild.hyper-linux}/bin/hl as
    pipeCommand on darwin build hosts.
  • lib/default.nix isNativeMusl now requires buildPlatform.isLinux (darwin's
    linuxArch is also arm64, so darwin→aarch64-musl was misclassified as
    native musl).

Verified end-to-end (compile + run + Template Haskell under hl) for both
ghc914-sh and the hadrian ghc9141hello and test/th-dlls-minimal run as
static aarch64 Linux ELFs and drive the full iserv GHCi message loop. The hadrian
path needed one extra fix (compiler/ghc/default.nix): a target libffi.dylib
so ld64 links stage0 correctly when cross-compiling from darwin.

Caveats / follow-ups (this side stays a PoC for now):

  • hl is not yet pinnedarmv6l-linux.nix expects hyper-linux to be
    provided as a pkgsBuildBuild overlay attr (it throws otherwise). Pinning it
    as lazy-inputs/hyper-linux is the remaining integration step, plus adding
    aarch64-multiplatform-musl to ci.nix for aarch64-darwin.
  • In-nix build TH is currently blocked by an upstream hl SIGBUS when
    spawned under the nix-daemon on this host; TH was verified by running the
    build phases via nix develop <drv> as the login user. Needs an upstream hl
    fix.
  • x86_64-unknown-linux-musl compiles fine from Apple Silicon but doesn't
    run under hl on M1 (x86_64 guests need Rosetta's 48-bit IPA VM — an
    M2+ requirement).

v2 slice builder aligned with ghc914-sh

The incremental per-component ("slice") builder now reproduces plan-nix's
UnitIds exactly for stable-haskell compilers, so real projects build against
ghc914-sh on the v2 builder. This needed:

  • unifying the compiler version on 9.14 (it was forking 9.14.0 vs 9.14) —
    this resolves the version-reconciliation follow-up noted above;
  • making the plan-time dummy ghc --info truthful about GHC Dynamic and RTS
    ways for statically-linked stable-haskell GHCs;
  • deterministic --ipid boot-library ids + a -B on the assembled stage2
    ghc wrapper (so unlit resolves);
  • skipping the slice unit-id equality check for style: "local"/"inplace"
    packages (the generated haskell-gi gi-* family), whose -inplace ids the
    slice reproduces as global hashes by construction.

Exercised end-to-end by building the leksah
dev shell against this branch: ghc 9.14 + cabal-install 3.16.1.0 +
haskell-language-server 2.14.0.0 all resolve and the shell enters.


Closes #347: because ghc914-sh is assembled from per-package cabalProject outputs, the RTS and each boot library are their own store paths, so downstream closures can reference just the subset they need (the 2.4 GiB vs 3.9 GiB reduction above) — which is what "put the RTS in a separate output" was after.

hamishmack added 17 commits July 4, 2026 21:32
plan-to-nix capitalizes cabal arch names (ppc → isPpc), but nixpkgs
spells several predicates differently (isPPC, isRiscV64, isLoongArch64).
Map them in fixSystem and keep aliases in cabal-os-arch-comp.nix so
already-generated plan.nix files continue to evaluate.  Covers the
architectures referenced by GHC 9.14's rts.cabal.
Allows pre-built Haskell packages to be made available as dependencies of
individual components (their .conf files are added to the component's
package DB), merging with the project-wide prebuilt-depends.  Config-files
derivations track regularLibDeps separately so prebuilt-depends do not
leak into the setup package DBs of downstream packages.

Used by the sghc914 (stable-haskell GHC) build, where GHC 9.14's
ghc-internal.conf depends on all rts sub-libraries.
Linux: c, rt, dl (provided by the toolchain).
Windows: wsock32, dbghelp, psapi (rts.cabal) and ntdll, ucrt, mingw32
(ghc-internal.cabal) — all provided implicitly by the mingw toolchain.
GHC 9.14's rts.conf depends on libffi-clib at the ghc-pkg level.  Cabal
does not track it in the plan (it is a C library wrapper), so add it to
nonReinstallablePkgs so make-config-files copies its .conf into
per-component package DBs.  Harmless for compilers without it.
A package whose only plan target is disabled (e.g. unix on Windows in a
--disable-tests project) previously crashed evaluation in defaultTargetId
((builtins.head available).id on the reason string: "expected a set but
found a string").  Filter it like TargetNotBuildable.
Compilers that are not built with hadrian (e.g. the cabalProject-built
sghc914) have no hadrianProject passthru and no iserv-proxy-exes entry;
roots' previously assumed both exist.
New cabal-project option (default false): when enabled, direct Hackage
tarball URLs in the cabal.project packages: stanza
(https://hackage.haskell.org/package/NAME-VER/NAME-VER.tar.gz) are
rewritten to local nix store paths fetched via hackage.nix, so
plan-to-nix needs no network and the pinned versions need not be present
at the project index-state.

- overlays/haskell.nix: add reusable haskell-nix.hackageTarball helper
  (fetch+unpack an exact version using the for-stackage db sha256).
- modules/cabal-project.nix: declare replace-hackage-tarball-urls.
- lib/call-cabal-project-to-nix.nix: rewrite rawCabalProject via
  replaceStrings (preserving store-path context).

Used by the stable-haskell GHC (sghc914) build, whose upstream
cabal.project pins its boot libraries this way.
Adds haskell-nix.compiler.sghc914 (and sghc914-stage1), a GHC built from
the stable-haskell/ghc stable-ghc-9.14 branch using haskell.nix's own
cabalProject machinery instead of hadrian, following the branch's
Makefile / cabal.project.* files:

  stage1 - GHC executables built with the boot compiler (ghc9103)
  stage2 - all boot libraries and executables built with stage1
  cross  - target boot libraries built with a cross-targeting stage1;
           the final cross compiler is native stage1 binaries + target
           settings (ghc-toolchain) + the target package DB

Every cabal component is its own derivation; the compiler closure is
2.4 GiB vs 3.9 GiB for the hadrian-built ghc9141 on aarch64-darwin.

Source pins live in lazy-inputs/ (sghc914 plus the sghc914-cabal /
sghc914-hpc-bin / sghc914-hsc2hs source-repository-packages, wired via
inputMap).  The overlay also provides:

- dumpSrc: a raw-src view that completes the plan-time installed-package
  dump with the hackage-URL-pinned boot libraries (no longer in-tree)
- wasm: libffi-wasm built and installed alongside the rts headers
- ghcjs: emscripten toolchain, JS-backend rts-way flags
- Windows: version-aware boot-lib registration (cabal legitimately
  solves a second older universe inside the Win32:hsc2hs:exe scope to
  break the Win32 -> hsc2hs -> process -> Win32 build-tool cycle) and
  cross-host compiler registration so templateHaskell/iserv-proxy-exes
  include sghc914 (with the TH-wrapper recursion cut in crossBootProject)
- dev shells (haskell-nix.sghc914-shells): from-boot and from-stage1,
  for hacking on the GHC tree with cabal

Verified: native aarch64-darwin and wasi32 build and run; all other
hydraJobs targets (ghcjs, musl32/64, static, aarch64-multiplatform(-musl),
android, mingwW64, ucrt64, x86_64-linux) instantiate.
hydraJobs.<system>.unstable.sghc914.native.hello verified building and
running on aarch64-darwin; wasi32.hello runs under wasmtime.
useLocalGhcLib / ghc-lib-reinstallable and ghc-boot-packages consume
ghc.generated during evaluation: plan-to-nix realises the GHC-tree
source-repository-package to compute the v2 UnitId. Because 'generated'
was an output of the full GHC derivation, that forced a from-source
compiler build via IFD whenever it wasn't cached -- the cause of the
multi-hour / hung hydra evaluations on this branch.

Add passthru.generated-light: a lightweight derivation that runs only
hadrian's generated-source targets (genprimopcode / deriveConstants /
template substitution, all stage0Boot) instead of compiling all of GHC.
The primop-*.hs-incl set is discovered per-version from hadrian's
Rules/Generate.hs; targets are built with -j1 to avoid a genprimopcode
scheduling race. Also fixes a pre-existing .hss typo so ghc-boot's
Version.hs is included.

Route consumers (ghc-packages.nix, cabal-project.nix, stack-project.nix,
ghcjs-project.nix) through 'generated-light or generated', and cache it
via roots'. sghc914 has neither attr and is unaffected (it builds lib:ghc
from configuredSrc with build-time generation).

Add test/generated-light comparing generated-light against the full-build
generated byte-for-byte.
…D_RTS in Cmm)

Root cause: the stable-haskell fork's threaded RTS compiled its .cmm with
THREADED_RTS undefined (cc-options reached the C sources but none of the four
stanza forms lands -optc-DTHREADED_RTS on the .cmm compile). So
StgMiscClosures.cmm's stg_WHITEHOLE_info took the non-threaded barf branch inside
the threaded RTS, making any transient WHITEHOLE lock under 'ghc --make -jN'
fatal on aarch64-darwin.

Fixed at the source (stable-haskell/ghc#191, hkm/rts-threaded-cmm-optc):
rts-threaded-flags now also passes cmm-options: -optc-DTHREADED_RTS. Repin
sghc914 to that branch. Verified: stg_WHITEHOLE_info spins, 120-module -j8 repro
0/N on both the debug and non-debug threaded RTS.

overlays/stable-haskell.nix also forces 'package ghc-bin flags: -debug' so the
compiler links the non-debug rts_thr (matching hadrian; the fork's ghc-bin debug
flag defaults True). This is release-shaping only, orthogonal to the crash.

Also carries in-flight branch edits: head.hackage tarball hash bump
(test/cabal.project.local), boot-package guard (overlays/ghc-packages.nix), and
CLAUDE.md notes.
The fork's cabal.project.common disables both (package text flags: -simdutf,
package haskeline flags: -terminfo). Current haskell.nix can build them:
terminfo's ncurses is auto-wired (--extra-{include,lib}-dirs to ncurses-6.6,
setupterm found in -ltinfo) and text's simd path links system-cxx-std-lib.

Override the flags to +simdutf/+terminfo in the overlay cabalProjectLocal, and
register terminfo + system-cxx-std-lib in the final compiler's package DB
(bootLibraries) — without that, text/haskeline are broken by dangling deps
('unusable due to missing dependencies').

Verified: ghc-pkg check clean (no broken packages), a Data.Text UTF-8 roundtrip
program compiles/links/runs, ghci loads, and the 120-module -j8 repro stays 0/N.
Mechanical rename of the stable-haskell GHC 9.14 compiler and its lazy-inputs:
- compiler attr haskell-nix.compiler.sghc914 -> ghc914-sh
- lazy-inputs/{sghc914,sghc914-cabal,sghc914-hpc-bin,sghc914-hsc2hs} ->
  ghc914-sh{,-cabal,-hpc-bin,-hsc2hs} (dirs + flake input names + locks)
- haskell-nix.sources.sghc914* -> ghc914-sh*
- derivation names, internal bindings, ci.nix, CLAUDE.md, generated-light note

Content unchanged; eval resolves (compiler.ghc914-sh.drvPath, sources.ghc914-sh).
stable-haskell/ghc#191 merged into stable-ghc-9.14 (tip 2c2ebfa; tree identical
to the fix branch 0fee7fb). Point the pin back at the mainline branch and drop
the temporary hkm/rts-threaded-cmm-optc reference. narHash uses the flake-compat
FOD value (nix flake update's differs — known gotcha).
The dummy ghc-pkg dump that cabal's solver sees as the installed package
set is synthesised from .cabal files found in the compiler's raw-src.
Two classes of ghc914-sh boot libraries were missing from it:

- Cabal and Cabal-syntax come from a source-repository-package (the
  stable-haskell/Cabal fork), so they are neither in the GHC tree nor
  covered by the hackage-URL overlay in dumpSrc.  The dump then omitted
  them entirely and silently dropped ghc-boot's Cabal-syntax depends
  edge, so user-project plans (e.g. iserv-proxy for cross TH) never
  marked Cabal-syntax pre-existing and the pruned per-component package
  DB failed with 'installed package ghc-boot-9.14 is broken due to
  missing package Cabal-syntax'.  Overlay their .cabal files into
  dumpSrc from the ghc914-sh-cabal lazy-input.

- text is built with +simdutf in stage2, so the installed text depends
  on system-cxx-std-lib, but that build-depends sits behind a flag
  conditional the dump extraction cannot see.  Add the edge in
  call-cabal-project-to-nix.nix gated on a new compiler passthru
  enableTextSimdutf (mirroring the haskeline/terminfo special case).
  Only the native ghc914-sh sets it: the cross boot libs build text
  WITHOUT simdutf.
isNativeMusl only compared linuxArch across build/host/target, but
linuxArch is defined for any OS (aarch64-darwin is "arm64" too), so an
aarch64-darwin -> aarch64-linux-musl cross was treated as native musl.
That enabled ghc-for-component-wrapper.nix's unprefixed unlit/ghc-iserv
alias block, which self-collides for compilers with an empty
targetPrefix ('ln: failed to create symbolic link bin/unlit: File
exists' while building libffi-clib), and would generally let musl
target binaries be treated as runnable on the build machine.

Require buildPlatform.isLinux; on Linux build hosts this is always
true, so existing behaviour is unchanged.
On Linux build hosts, cross target binaries (TH via iserv, tests) run
under qemu user-mode emulation, which does not exist on macOS.
hyper-linux (github:zw3rk/hyper-linux) runs aarch64-linux and
x86_64-linux ELF binaries directly on Apple Silicon via
Hypervisor.framework, and slots into the same emulator seat:

- overlays/linux-cross.nix: the emulator invocation is now a
  pipeCommand argument defaulting to the previous qemu user-mode
  command (when overridden, the qemu argument is never forced, so
  darwin hosts do not build qemu).  Also fix a $@* typo in the test
  wrapper.
- overlays/armv6l-linux.nix: darwin build hosts pass
  pipeCommand = pkgsBuildBuild.hyper-linux + /bin/hl (with a clear
  throw when the hyper-linux overlay attribute is not provided; there
  is no in-repo pin yet).  Extend isLinuxCross to x86_64 targets:
  those are only a cross HOST from a non-Linux or non-x86_64 build
  platform (x86_64-linux -> musl64 stays native musl / isCrossHost
  false, so Linux hosts are unaffected).
- lib/default.nix: qemuByHostPlatform learns x86_64.

Verified end-to-end on aarch64-darwin with ghc914-sh ->
aarch64-unknown-linux-musl: hackage hello and a Template Haskell
example (test/th-dlls-minimal, TH evaluated by iserv-proxy piped
through hl into the static musl iserv-proxy-interpreter) build and run
under hl.  Known issues (upstream hl 0.2.4): hl SIGBUSes when spawned
inside a nix-daemon build, so in-sandbox TH currently fails (works
when the same derivation's phases run as a login user); x86_64 guests
need a 48-bit IPA VM for Rosetta and fail on M1 (M2+ only).
@hamishmack hamishmack changed the title Add stable-haskell GHC (sghc914): GHC 9.14 built with cabalProject instead of hadrian Add stable-haskell GHC (ghc914-sh): GHC 9.14 built with cabalProject instead of hadrian Jul 8, 2026
hadrian configures the stage0 (build platform) in-tree libraries with
the same TARGET --with-ffi-libraries directory as stage1 (the same
stage-conflation bug class as the --with-curses-libraries one noted at
enableTerminfo).  On Linux build hosts the same-arch ELF libffi
happens to satisfy the stage0 link, but when cross-compiling from
darwin, ld64 finds the ELF libffi.so, ignores it as wrong-format and
STOPS searching (verified: later -L directories are not consulted for
that -l), leaving ffi_call & co undefined when linking stage0
executables (e.g. haddock) against the in-tree rts/ghci.

Fix: for cross compilers built on darwin, pass --with-ffi-libraries a
symlinkJoin of the target libffi lib dir and the build-platform
libffi.  Each linker then sees only its own format: ld64 prefers
libffi.dylib, while the target's GNU ld only ever looks for
libffi.so/libffi.a.  Linux builds keep the exact previous path, so no
existing derivation hashes change.

With this, ghc9141 (hadrian) builds as an aarch64-darwin ->
aarch64-unknown-linux-musl cross compiler and its output (including a
Template Haskell example evaluated via iserv-proxy piped through
hyper-linux) runs correctly under hl.
Make the v2 slice builder reproduce plan-nix's UnitIds exactly when the
compiler is a stable-haskell cabalProject GHC (ghc914-sh), so the leksah
dev shell builds end-to-end (ghc 9.14 + cabal 3.16.1 + HLS 2.14).

- stable-haskell.nix: unify every nix-facing surface on ghcVersion="9.14"
  (munged) — passthru.version, lib/ghc-9.14, bin aliases; ghcVersionFull
  "9.14.0" survives only as the stage1 ./configure --with-project-version
  arg. The compiler id is a pkgHash input, so 9.14.0 vs 9.14 forked it.
  Add -B$out/lib/ghc-9.14 to the assembled native stage2 ghc wrapper so
  topdir resolves unlit (GHC honours the last -B, so the component
  wrapper's -B still wins). New passthru projectUnitId="ghc-9.14-inplace".
  stage2Project module gives boot libs deterministic ids via
  --ipid=$pkgid[-inplace] (rts + sublibs and system-cxx-std-lib bare),
  matching what the stage2 plan and hadrian bindists register.

- dummy-ghc.nix: branch on ghc.isStableHaskell so plan-time --info matches
  the real compiler — GHC Dynamic NO and RTS ways "v thr debug thr_debug"
  (statically-linked ghc-bin, no dyn/prof ways); Project Unit Id from
  projectUnitId. Cabal keys --enable-shared/--disable-shared and dep
  unit-ids on these, so a mismatch forks every downstream UnitId.

- comp-v2-builder.nix: skip the slice unit-id check for style=="inplace"
  as well as "local". Under ghc914-sh the -inplace boot libs make cabal
  elaborate the generated haskell-gi gi-* family BuildInplaceOnly (fixed
  <pkgid>-inplace id) even though they're plain hackage deps; the slice
  serves identical source and builds them global-hashed, so the ids
  differ by construction. Also compose source setup-deps' slices and pin
  the transitive setup-dep closure as ownSetupConstraints so setups solve
  against the same deps plan-nix recorded.

- comp-builder.nix: widen the library installCheck conf glob to also
  accept the bare <pkgid>.conf that an explicit --ipid registers, gated
  on components that actually pass --ipid (keeps other derivation hashes).

- test/cabal.project.local: bump the rolling head.hackage --sha256.
@hamishmack hamishmack changed the title Add stable-haskell GHC (ghc914-sh): GHC 9.14 built with cabalProject instead of hadrian Add stable-haskell GHC (ghc914-sh): GHC 9.14 built with cabalProject instead of hadrian; darwin→linux cross via hyper-linux Jul 10, 2026
…ia hyper-linux

Cross-compile Linux (musl) targets from an aarch64-darwin build host, running
the target binaries under hyper-linux (`hl`) on Apple Silicon — qemu user-mode
emulation doesn't exist on macOS.

- Add a `hyper-linux` overlay (pinned `hyper-linux` flake input, nixpkgs
  follows ours) exposing `hl` as `pkgs.hyper-linux`, so darwin -> linux cross
  works out of the box instead of each consumer supplying it.  `overlays/
  armv6l-linux.nix` picks it up as the `pipeCommand` on darwin build hosts.
  Only defined where hyper-linux ships a `default` package (aarch64-darwin);
  lazy and absent elsewhere.

- ci.nix: enable the `aarch64-multiplatform-musl` and `musl64` cross targets
  for unstable ghc9141 (hadrian) and ghc914-sh (stable-haskell) on
  aarch64-darwin.  Build the compile side only — the cross `tests` are gated
  off for darwin -> linux because building their Template Haskell would run
  `hl` inside the nix build, which currently hits an `hl` SIGBUS under the
  nix-daemon; the compile side (roots + ghc + iserv-proxy-exes + hello)
  instantiates cleanly for all three tested combinations.

- scripts/update-ci-status.sh: add `ghc914-sh` to COMPILERS (also realigns the
  ci-status-matrix guard test with the current hydraJobs) and the two new
  aarch64-darwin cross rows to PLATFORMS, so the README CI-status table tracks
  them.
Apple's ranlib (which cabal runs after `ar` on darwin build hosts) rewrites
archives with 8-byte '\n' member padding counted into the header size field.
GHC's archive reader then hands padded wasm cbits objects to emscripten and
wasm-ld fails with "section too large".

Patch GHC.SysTools.Ar to recover a wasm member's true length from its section
table and drop the padding.  Applied for GHC 9.12-9.15 on the JS backend.
head.hackage is a rolling repository, so the FOD that runs cabal v2-update
against it goes stale periodically.  Refresh the pinned hash.
Pull cabal-install/Cabal/Cabal-syntax/cabal-install-solver 3.17 from the
stable-haskell/Cabal fork (the cross "stage" system, haskell/cabal#11179)
via source-repository-package in nix-tools/cabal.project, so make-install-plan
gets `with-build-compiler` and build:/host: stage-qualified constraints.

Port nix-tools' sources to the Cabal-syntax 3.17 API (dropped the constraint
type param from CondTree/CondBranch; CondNode lost its constraints field;
*Sources became [ExtraSource Pkg]; Verbosity split into Verbosity/VerbosityFlags;
plan values are Stage-tagged):
  - lib-cabal2nix/Cabal2Nix.hs
  - make-install-plan/{Freeze,MakeInstallPlan,ProjectPlanOutput}.hs
    (ProjectPlanOutput strips the build:/host: stage qualifier from every
    emitted dependency id so plan.json ids stay bare)
  - setup-ghcjs/Setup.hs

Reconcile the installed-package-id-os-override patch to 3.17 (preserving the
fork's OSX->Long unit-id format), and add a patch porting Cabal-syntax-json
(the cabal2json provider) to the 3.17 CondTree/ExtraSource/Verbosity API.
v2CabalInstall now builds cabal-install 3.17.0.1 from the fork via
source-repository-package (cabalProjectLocal), so the per-slice `cabal
v2-build` has the stage system (--with-build-compiler / build:/host:).

Reconcile the v2 cabal-install patches to the fork's 3.17 source: regenerate
skip-installed-revdeps + setup-build-num-jobs-env at fuzz 0.  The two
prune-unreachable-sublibs patches are temporarily dropped (kept in-tree for
reference) — their reachableSubLibs/reachablePDSubLibs walk `CondNode _ ds _`
for the per-node [Dependency] constraints, which 3.17 removed from CondNode,
so they need a data-model rewrite rather than an arity fix.  They only affect
sublib-heavy hackage packages (e.g. vector's benchmarks-O2), not the cabal
tool or the ghc914-sh boot libs.
…fork

Drive plan-to-nix with the fork-linked nix-tools instead of the pinned static
tarball:
  - overlays/default.nix: nix-tools-unchecked builds from source, bootstrapped
    through the pinned haskell.nix (its own static nix-tools breaks the
    plan-to-nix recursion — make-install-plan AND the hardcoded cabal2json both
    consume nix-tools-unchecked); default-setup kept from the static pin.
  - lib/call-cabal-project-to-nix.nix: add a plan-time `gcc` shim (execs the
    build cc) for the fork's toolchain probe — UnitId-safe, cc isn't hashed;
    add a `withBuildCompiler` path that builds a dummy build-platform ghc/ghc-pkg
    (reporting the build boot libs installed) and passes
    --with-build-compiler/--with-build-hc-pkg to make-install-plan, so the stage
    solver is satisfied without building a real compiler.
  - modules/cabal-project.nix: add the `withBuildCompiler` option.
  - modules/hackage-project.nix: emit `optional-packages: ./*` in the generated
    project so the fork's stricter parser accepts hackage-tool projects
    (Cabal-7168) — UnitId-neutral, matches nothing locally.

Also includes the prepopulateHackageIndex option (minimal empty index for
:none projects) wired through call-cabal-project-to-nix, modules/cabal-project,
and overlays/haskell.nix's dotCabal.
The v2 builder inferred `documentation: True` for any unit whose
plan-nix configure-args carry `--ghc-option=-haddock`.  The marker is
ambiguous: a plain `ghc-options: -haddock` (e.g. haskell-language-
server's `package *` stanza) leaves the identical arg WITHOUT setting
the haddock booleans that enter the UnitId hash (pkgHashDocumentation
et al.).  Misinferring forked every such slice's UnitId from plan-nix
and made `cabal v2-build` run the haddock program, which
stable-haskell compilers don't ship.

plan.json offers no truthful per-unit signal (`haddock-args` is
always the bare verb and the configure-args are byte-identical in
both cases — verified against a `documentation: True` plan), so gate
the inference on the assembled cabal.project + cabalProjectLocal text
actually containing a `documentation: ... True` line.  The text is
threaded as a new pkg-set option `cabalProject` (mirroring
`cabalProjectLocal`); when neither is available the old marker-only
behaviour is kept.  The eventual truthful fix is make-install-plan
emitting `elabBuildHaddocks`, at the cost of a nix-tools
(world-rebuild) change.

Drv-neutral for projects that build today (verified: ghc914-sh native
hello unchanged); with this, leksah's dev shell (HLS master as a
`haskell-nix.tool` with ghc914-sh via v2) builds.

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
The -target cross wrapper runs ghc-toolchain-bin to generate the
target settings.  For the ghcjs target it aborted at "checking for
'ranlib'": ghc-toolchain probes PATH for javascript-unknown-ghcjs-ranlib
/ llvm-ranlib / ranlib, but emscripten ships emar / emranlib / emnm, so
ar/ranlib/nm discovery (all fatal) never found them.

Pass --ar/--ranlib/--nm explicitly for the ghcjs target, mirroring the
AR/RANLIB/NM exports the hadrian build uses for GHC >= 9.12
(compiler/ghc/default.nix).  The non-ghcjs branch is empty so the
wrapper script stays byte-identical for every other target (musl64
hello drvPath unchanged).

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
In a two-stage plan (stable-haskell `-target` cross compiler, whose
target global package db — and hence the plan-time dummy `ghc-pkg dump`
— is EMPTY) a boot library appears twice under one bare id: a
pre-existing BUILD-stage unit (from the native build compiler) and the
configured HOST-stage unit rebuilt from source (e.g. `base-4.22.0.0`).
The dedup that normally drops such a shadow is intentionally skipped
for two-stage plans.

`by-id` is first-wins over the install-plan and the pre-existing entry
comes first, so a HOST consumer's `depends` edge (`hello` → `base`)
resolved to the pre-existing stub and `lookupDependency` dropped it as
compiler-provided — even though the target db is empty.  Every consumer
of a boot lib (the user's exe, and the cross compiler's own Cabal /
ghc-boot / ghci / iserv-proxy slices) then had an empty
buildInputs/depSlices and cabal's solver failed with
`next goal: host:base … fail (backjumping)`.

Resolve host library/exe deps through a host-preferring id map
(`by-id-host` = `by-id // configuredById`) and a matching
`pre-existing-depends-host`, so the configured from-source twin wins
and its slice is composed in; the whole boot closure then resolves from
source.  Setup/build-scope deps keep the pre-existing (build-stage,
compiler-provided) boot lib via the unchanged `by-id`.

Scoped to two-stage plans: for every other plan the host maps are
defined to equal the build maps, so native ghc914-sh, hadrian, and
ordinary projects are byte-identical (verified: ghc914-sh native hello
`7ay62hzh` and hadrian ghc9141 hello `wijb55hj` drvPaths unchanged).
With the fix both ghc914-sh musl64 and ghcjs hello now compose and
build their from-source boot slices (remaining work: cross boot-lib
unit-id alignment vs plan-nix, tracked on the branch).

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
The stable-haskell `-target` cross wrapper builds boot libraries from
source but keeps `targetPrefix = ""` (it is the native compiler driven
with `-target`), so `isCross` is false and the slice builder applied the
strict "slice unit-id must equal plan-nix's" check to global-style
(hashed) units such as libffi-clib.

Those hashed ids are computed by cabal against the REAL cross wrapper,
whose `ghc --info` differs from the plan-time dummy ghc — so they
legitimately diverge from plan-nix's, exactly as on a genuine `isCross`
build (where the check is already skipped).  libffi-clib's slice built
correctly but tripped the check (plan `65bbd76d…` vs slice `11a8cd48…`).

Key the skip off the empty target global db (`emptyGlobalPackageDb`,
the reliable two-stage-cross signal) in addition to `isCross`.
Composition stays consistent: every consumer re-solves the unit from
source against the same wrapper and agrees on the hashed id, and the
target dummy dump is empty so no consumer reads these ids from a dump.
Local/inplace units keep deterministic ids and are still checked.

Drv-neutral for non-two-stage plans (ghc914-sh native hello 7ay62hzh,
hadrian ghc9141 hello wijb55hj unchanged).

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
GHC 9.14's unit wiring (mkUnitState) requires all four rts WAY
sub-libraries (nonthreaded-nodebug, threaded-nodebug, …) to be
resolvable whenever ghc-internal.conf is in a package's per-component
db — which is every package that transitively links base.  A native
compiler satisfies this from its own (fully populated) global db, but a
two-stage `-target` cross project has an EMPTY target global db and
builds the boot libraries from source, so each HOST slice must compose
the ways itself.  Without them, `ghc-prim` (and every other boot lib)
panics: "The RTS for rts:nonthreaded-nodebug is missing from the package
database".

Extend the boot-injection module (already gated on the empty target db
via `shBootInject`) to set `additional-prebuilt-depends = rtsSublibs`
on every HOST-stage package the plan configures — enumerated from
plan-json (a fixed input; reading `config.packages` for the list would
recurse through these very settings), excluding rts:nonthreaded-nodebug's
own dependency closure (rts / libffi-clib / rts-fs / rts-headers) and
BUILD-stage tools (alex / happy / … run natively with the build
compiler, whose global db already has the ways).  Mirrors the identical
mechanism stage2Project uses for the native compiler build
(overlays/stable-haskell.nix); the user's own packages need it too, not
just the boot libs.

`planUnitStage` is sourced from `pkgs.haskell-nix.haskellLib` (the file's
own `haskellLib` module argument is declared but never provided by
callers) and captured in the enclosing `let` so the nested pkg-set module
closes over it rather than requesting it as a module arg.

Drv-neutral for non-two-stage plans (ghc914-sh native hello 7ay62hzh,
hadrian ghc9141 hello wijb55hj unchanged).

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
directory, file-io, process and unix each carry an automatic `os-string`
flag (default False) that selects — in both their .cabal build-depends
and their .hsc/.hs sources — between the modern `os-string` package and
filepath's pre-1.5 bundled `System.OsString.*` modules.  With
filepath-1.5.4.0 (which no longer bundles those modules) only the
`+os-string` branch compiles.

`allow-newer` relaxes the `else` branch's `filepath < 1.5.0.0` upper
bound, so the solver otherwise keeps the default `-os-string` and pairs
it with filepath-1.5.4.0 — a combination that builds a plan but fails at
compile time with "hidden package os-string" / "Could not find module
System.OsString.Data.ByteString.Short" (e.g. unix's
System.Posix.Env.PosixString / System.Posix.PosixPath.FilePath).

Force `+os-string` for these packages in the boot-injection
cabalProjectLocal so plan-nix records the flag (and comp-v2-builder's
flagBlockFor propagates it to each slice), matching how the compiler's
own stage2 (cabal.project.stage2) resolves them.  With the flag on, unix
et al. now build from source under the -target cross wrapper.

Drv-neutral for non-two-stage plans (block gated on the empty target db;
ghc914-sh native hello 7ay62hzh, hadrian ghc9141 hello wijb55hj
unchanged).

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
The stable-haskell cabal fork's `libraryConfigureInputsFromElabPackage`
(cabal-install/src/Distribution/Client/InLibrary.hs) built the in-library
LocalBuildInfo's `platform`/`compiler` from `getStage toolchains Host`
unconditionally, while SetupWrapper builds the package's program db from
`getStage toolchains (elabStage pkg)`.  In a two-stage (cross) plan a
build-stage `build-tool-depends` (e.g. alex) was therefore compiled by the
native build ghc (linux ELF) but named via `exeTargetName (hostPlatform lbi)`
with the Host platform's `.exe` suffix — so a linux `alex` was installed as
`alex.exe`, and the consumer's configure (searching bare `alex`) failed with
`[Cabal-7620] program 'alex' could not be found`.

Patch the fork to take `plat`/`compil` from the package's own stage toolchain,
matching the program db.  No-op for single-stage/native plans (Build == Host).

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
The stable-haskell GHC bootstrapped via haskell.nix's own ghc9103, which is
built from source (~32 derivations on darwin). nixpkgs' ghc9103 is the same
GHC version and is fully cached in cache.nixos.org (0 builds), so route the
boot compiler through the `compilerSelection` hook to nixpkgs
(`p.haskell.compiler`) — the same nixpkgs-boot pattern already used for
v2-cabal-install and the iserv-proxy tools.

Applied to stage1, the alex/happy/genprimopcode/deriveConstants boot tools,
`buildGHC`, the cross `deriveConstants`/`libffi-wasm` tools, and the dev
shells. Verified by dry-run: the boot ghc is now fetched, not built.

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
… fixes

Task #15 (stable-haskell -target two-stage cross). Gated behind
`isTwoStagePlan` / `hostPlatform.isWindows` so native, hadrian and
single-stage-cross slice hashes stay byte-identical (the one exception is
`targetSupportsShared`, which also fixes the dynamic-way decision for
static/musl cross targets).

- comp-v2-builder: `buildToolSourceFrags` contributes a build-tool's SOURCE
  frag (tarball + .cabal) to a consuming slice instead of composing the
  tool's cyclic built slice; drop build-tool-only ids from store composition
  for two-stage plans (avoids host-exe→host-lib eval recursion); expose
  `v2SourceFrag`; `--via-asm` hsc2hs for Windows (Win32 pointer-cast #enums).
- build-cabal-slice: accept `buildToolSourceFrags`, stage them into the
  slicing repo (`extraFragStaging`; empty → hash-neutral otherwise).
- call-cabal-project-to-nix: two-stage host `--with-compiler` reports the
  target platform so os()/arch() conditionals resolve (else Windows plans
  drop Win32 and every slice re-solve fails "unknown package: Win32").
- cabal-project: inject Win32 hackage source as a local packages: entry on
  Windows targets; key the boot-lib `shared:` off the target platform
  (`targetSupportsShared`); Win32 +os-string flag.

Complements the already-committed alex.exe fix (224e400): the source
injection here is what the InLibrary exe-naming patch operates on.

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
…platform

The boot switch (previous commit) routes the boot compiler through nixpkgs'
`p.haskell.compiler`, which — unlike a haskell.nix compiler — has no
`evalWith.${evalSystem}` variant.  lib/dummy-ghc.nix reads `ghc.raw-src` to
source boot-package .cabal files during plan-to-nix, and that source
derivation is system-specific.  So when Hydra evaluates a foreign-system job
on the eval host (the x86_64-linux ghc914-sh jobs are evaluated on
aarch64-darwin), the boot ghc's raw-src was the *build* platform's
(x86_64-linux) — dragging an x86_64-linux realisation into the eval-host
plan-to-nix.

Override `raw-src` in `bootCompilerSelection` to the eval-platform compiler's
copy (GHC source is platform-independent, so the plan content is identical);
the actual build compiler's outPath is untouched by `//`, so it stays
build-platform.  No-op when evalSystem == build system: verified the native
aarch64-darwin ghc914-sh compiler drvPath is unchanged
(0c8qbkqyjdw502b5jpgqaj3i97bsk1hg).

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
The stable-haskell two-stage cross plans (`withBuildCompiler`) were selecting
a target-hosted compiler here via `compilerSelection pkgs` (`pkgs` =
`final.buildPackages`, target = the cross target).  Forcing that compiler
evaluates its host=target splice, which the ghcjs backend rejects outright
("attempt to build ghcjs with ghcjs", overlays/bootstrap.nix) and which never
makes sense for any cross target.

These plans don't need a target-hosted compiler at all: the plan-to-nix dummy
already reports the cross target from `mkDummyGhcPkg`'s `pkgs`, and target-OS
`os()`/`arch()` conditionals (e.g. Win32) are handled by the boot-package
injection in modules/cabal-project.nix.  Drop the special case so `ghc'` uses
the build-hosted compiler uniformly.

Verified: aarch64-darwin ghc914-sh ghcjs.hello now evaluates (was throwing);
musl64.hello and native ghc914-sh drvPaths byte-identical.

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
`overlays/wasm.nix` rebuilds the LLVM `monorepoSrc` (a plain source-tree copy:
`buildPhase = "true"`, `cp -r . $out`) so it can apply the haskell-wasm patch.
It used `final.stdenv` — the wasm *cross* stdenv, whose `cc` is the wasm clang.
But that clang is built FROM this very `monorepoSrc`, so the reference closed a
bootstrap cycle (clang → monorepoSrc → final.stdenv → clang → …) that evaluated
to an infinite recursion.  On aarch64-darwin this made every wasi32 job
(e.g. ghc914-sh.wasi32.hello) fail to evaluate with "infinite recursion
encountered" reached through stdenv-darwin's defaultNativeBuildInputs.

A source copy needs no target compiler: build it with
`final.buildPackages.stdenv` (native, build platform), which breaks the cycle.
Verified: pkgsCross.wasi32.buildPackages.llvmPackages.clang and the full
wasi32.hello job now evaluate (previously infinite recursion).

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
…pkgs

GHC's wasm backend still requires the haskell-wasm wasi-libc fork (ghc-wasm-meta
ships a forked wasi-sdk and states "Upstream wasi-sdk won't work yet"; the
patches are not upstreamed).  The pinned rev (951e93b) and LLVM (21.1.8, via the
haskell-wasm llvm-project patch) already match exactly what the current wasi-sdk
fork uses — nothing was stale.

The nixpkgs bump broke the *build* of that fork in three ways, all now handled
in the `wasilibc` override:

  1. nixpkgs re-packaged its own wasilibc around CMake (wasi-sdk-32).
     `overrideAttrs` inherited cmake/ninja and their setup hooks, so
     cmakeConfigurePhase ran against the fork tree (Makefile only, no
     CMakeLists.txt) and aborted.  Drop cmake/ninja, disable their hooks, and
     drive the fork's own `make default libc_so` with explicit
     configure/build/install phases — reusing the base derivation only for its
     stdenvNoLibc + cross-toolchain libc-bootstrap wiring.

  2. We build through nixpkgs' cc-wrapper (ghc-wasm-meta uses the raw wasi-sdk
     clang), which injects `-rtlib=compiler-rt`; clang-21 flags it as unused on
     `-c` steps and the Makefile compiles with `-Werror`.  Demote it via
     NIX_CFLAGS_COMPILE (appended after the Makefile's flags).  Also drop
     CHECK_SYMBOLS=yes: that self-test diffs clang's predefined macros against a
     set captured with the raw wasi-sdk clang, and clang-21 adds benign extras
     (_LIBCPP_HARDENING_MODE) that fail the diff without affecting the libc.

  3. nixpkgs' wasilibc lost its `share` output and enabled structuredAttrs;
     install via the $out/$dev shell vars and skip the sysroot/share metadata
     (build-verification output, not needed by libc consumers).

Verified on aarch64-darwin: pkgsCross.wasi32.wasilibc and the full wasm clang
wrapper toolchain now build.

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
The Hydra eval of this branch failed outright: the `nix-tools` aggregate
(flake.nix) lists `x86_64-darwin.nix-tools.static.zipped.*` constituents, and
resolving them forced evaluation of x86_64-darwin under the bumped
nixpkgs-unstable, which now throws "Nixpkgs 26.11 has dropped support for
x86_64-darwin".  Hydra reported the constituents as "does not exist" and failed
the whole jobset (no eval produced).

26.05 is the last nixpkgs release supporting x86_64-darwin, so rather than drop
the platform, pin it there:

  * Add a `nixpkgs-2605` input (26.05-darwin); refresh `nixpkgs-2511`.
  * ci.nix: the "Nixpkgs version" matrix uses R2605 (replacing R2511) plus
    `unstable`, but `unstable` is excluded on x86_64-darwin (importing 26.11
    there throws).  So x86_64-darwin builds only the R2605 dimension.
  * flake.nix: `legacyPackages`/`legacyPackagesUnstable` fall back to the 26.05
    pin on x86_64-darwin (unstable can't be imported there); every downstream
    output (packages/checks/devShells/requiredJobs) inherits the working pkgs.
  * The x86_64-darwin static nix-tools constituents stay: the ./nix-tools
    subflake resolves pkgs via its own pinned haskellNix, whose unstable nixpkgs
    predates the drop and still supports x86_64-darwin.  The original failure was
    only the `callFlake` forcing `self.legacyPackages.x86_64-darwin` (26.11),
    which the fallback above fixes.

Verified on aarch64-darwin: the `nix-tools` aggregate, x86_64-darwin static
nix-tools, x86_64-darwin R2605 roots/requiredJobs, and the aarch64-darwin /
x86_64-linux R2605+unstable dimensions all evaluate.

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
nixpkgs 26.05/26.11 renamed several attrs that lib/pkgconf-nixpkgs-map.nix
resolves, so every eval printed:

  evaluation warning: 'xxHash' has been renamed to 'xxhash'
  evaluation warning: 'QuadProgpp' has been renamed to 'quadprogpp'
  evaluation warning: 'librest_1_0' has been renamed to/replaced by 'librest'

`lookupAttrsIn` force-evaluates each mapped attr, which triggers the deprecated
alias' warning.  Point the entries at the new names, but keep working on older
nixpkgs where only the old names exist:

  * xxHash->xxhash, QuadProgpp->quadprogpp: use the existing `prefer new old`
    helper.  The new name appears exactly when the old one becomes an alias
    (both at 26.05), so `prefer` picks the new name on 26.05+ and the still-real
    old name on <=25.11 — warning-free on every pin.

  * librest_1_0->librest: `prefer` is wrong here because `librest` exists on
    every nixpkgs as a *distinct* older package (0.8.x, the 0.7 API) that does
    not provide `rest-1.0.pc` — that is why the map used `librest_1_0`.  The 1.0
    provider was renamed to `librest` and `librest_1_0` became a deprecated
    alias only in 26.11, so gate on the nixpkgs release (`lib.version`): real
    `librest_1_0` through 26.05, `librest` from 26.11 on.

Verified across pins (24.11/25.05/25.11/26.05/unstable): the three warnings are
gone on the CI pins (R2605 + unstable) and each older pin resolves to the
correct, still-present attribute.

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
head.hackage is a rolling repository, so the fixed-output derivation that
runs `cabal v2-update` against it goes stale whenever head.hackage updates.
Refresh the --sha256 to the current content hash.

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
* lib/call-cabal-project-to-nix.nix: for a stable-haskell CROSS plan the
  `-w` dummy must report the TARGET platform (else every arch(javascript)/
  os(ghcjs) conditional resolves for the build host) and present an EMPTY
  global package db so the plan builds rts/base/... from source, as the
  fork's boot-package injection expects.  Gated on `isStableHaskell`: a
  mainline compiler's cross plan needs the opposite (non-empty dump, boot
  libs pre-existing) and keeps the pre-existing behaviour.

* lib/dummy-ghc.nix: match the fork's real `--info` output so
  tests.dummy-ghc-info holds: the fork brands `Edition: Stable Haskell`,
  and every ghc914-sh (native and each -target wrapper) is the SAME
  stage-2 binary, so `Stage` is 2 and `RTS ways` is the baked-in
  `v thr debug thr_debug` across all targets.  Mainline compilers keep
  their per-target values.

* test/dummy-ghc-info: ignore `ld supports verbatim namespace` — a
  per-target linker capability cabal does not consult for elaboration.

* modules/install-plan/redirect.nix: make-install-plan emits targets that
  are not buildable in the current environment as bare status STRINGS in
  `available`; extend the filter to TargetDisabledBySolver and
  TargetNotLocal (e.g. a package marked `buildable: False` for the JS
  target) — previously `defaultTargetId` forced `.id` on the reason
  string and eval failed with "expected a set but found a string".
Two-stage (stable-haskell -target cross) plans build their build-tools
from SOURCE inside the consuming slice.  Complete that machinery and gate
the JS-backend specifics; all changes are byte-identical for slices with
no build-tool source frags and for non-ghcjs targets.

* comp-v2-builder.nix (buildToolSourceFrags): a tool exe built build-stage
  in-slice needs its own LIBRARY deps in the slicing repo too (happy 2.x
  exe:happy build-depends happy-lib).  Walk each tool exe's dep closure on
  the BUILD-twin-preferring plan index and source-stage the configured
  units; exclude the GHC boot-lib set by name — the fork reinstalls boot
  libs from source in both stages, so neither `type` nor
  nonReinstallablePkgs distinguishes them, and their build-stage src is a
  null-hash boot fetchurl that coerces on staging.  nonReinstallablePkgs
  threaded in via hspkg-builder.nix.

* comp-v2-builder.nix (ghcjs): the ghcjs platform has no stdenv.cc, so
  merely realizing a native C lib / framework / pkgconfig dep from the
  target pkg-set throws "no C compiler provided for this platform".  A JS
  slice links no host C libraries — empty `libs`/`frameworks`/`pkgconfig`
  at the source and skip the transitiveDepLibs / pkgcfglessSysLibs walks.

* comp-v2-builder.nix (pkgTarball): keep the direct-`src` shortcut only
  when src is provably a hackage tarball (name ends .tar.gz — their slice
  hashes don't move); otherwise normalize through a v2-tar runCommand
  that packs a DIRECTORY source (a source-repository-package SUBDIR such
  as Cabal-syntax in the stable-haskell Cabal SRP) into a deterministic
  tarball.  Previously the raw directory was symlinked as `.tar.gz` and
  slice staging failed `tar: ...: Cannot read: Is a directory`.

* build-cabal-slice.nix: build-stage tool sub-builds inside a ghcjs-target
  slice need a NATIVE toolchain the slice stdenv doesn't provide — add the
  build-platform cc wrapper + libiconv (darwin `-liconv` via LIBRARY_PATH),
  and make every composed platform db's package.cache writable so the
  build-stage `ghc-pkg recache` doesn't fail flock on a store symlink.
  ghcjsArWrapper now execs emar with `--format=gnu` (llvm-ar's darwin
  default counts 8-byte alignment padding IN the ar size field, which wasm
  consumers read as trailing garbage — "section too large").

* ghc-shim.nix: the `ar command` settings sed expected `", "` but the
  settings file has no space after the comma — the swap never matched and
  the ar wrapper was never used.  Match optional whitespace and assert the
  swap took.  (This shim only engages for prefixed-ghc targets; the
  ghc914-sh JS wrapper's targetPrefix is "" and gets the equivalent fix in
  its own settings assembly.)
* targetSupportsShared: the JS backend is static-only — emscripten's
  wasm-ld rejects the ELF soname flag GHC emits when linking a boot lib's
  shared object (`wasm-ld: error: unknown argument: -h`).  Exclude ghcjs,
  mirroring the Windows/musl exclusions, so the injected `package *`
  config sets `shared: False`.

* Template Haskell: GHC's JS external interpreter resolves the `ghci`
  package BY NAME in the home unit state at splice time
  (GHC/Runtime/Interpreter/JS.hs lookupPackageName) and links
  GHCi.Server.defaultServer from its unit closure.  `ghci` is never a
  build-depends, so no slice's dep db contains it, and the two-stage
  wrapper's target global db is empty — the lookup failed for every
  TH-using package.  Populating the compiler's GLOBAL db with a
  separately-built ghci closure does NOT work: its unit-ids can never
  match the project's own boot-lib builds, so the interpreter session
  loads ghc-internal's jsbits twice (server closure + splice deps) and
  node dies "Identifier 'h$base_o_rdonly' has already been declared".
  Instead compose THE PROJECT'S OWN ghci slice into every host package's
  starting store db via additional-prebuilt-depends (ghcjs only), exactly
  like the rts WAY sub-libs above: the closure propagates with the slice
  and unit-ids agree with the splice code by construction.  ghci's own
  dependency closure is excluded by name to avoid derivation cycles
  (none of those packages run TH — they all build before ghci exists).

* ghc-internal: its configure guards the JS sizeof/offset checks with
  `test "$host" = "javascript-ghcjs"`, but cabal's runConfigureScript
  passes the full triple `--host=javascript-unknown-ghcjs`, so the block
  silently skipped and HsBaseConfig.h shipped `#undef SIZEOF_STRUCT_STAT`
  etc. — the jsbits then die at TH time with "ReferenceError:
  SIZEOF_STRUCT_STAT is not defined".  A prePatch makes the guard accept
  both spellings.  (Proper fix belongs upstream in stable-haskell/ghc
  libraries/ghc-internal/configure.ac.)
Assorted fixes to the javascript-unknown-ghcjs `-target` wrapper, all
gated isGhcjsTarget; other cross targets (wasm etc.) are byte-identical.

* settings `JavaScript CPP command`: ghc-toolchain-bin resolves every
  tool to an absolute path EXCEPT this one (left as bare `emcc`, which is
  not on slice PATHs) — sed it to the same absolute emcc every other
  command uses.

* settings `ar command`: emar is llvm-ar, whose DEFAULT archive format on
  a macOS build host is darwin — member data 8-aligned with '\n' padding
  COUNTED IN the ar size field.  Mach-O readers tolerate the inline pad;
  wasm ones parse to the entry's declared end and fail "section too
  large" (llvm-nm, and wasm-ld at the TH-interpreter and final links, on
  e.g. time's HsTime.o).  Point the setting at a small emar wrapper
  forcing `--format=gnu` (exact member sizes); cabal takes its `ar` from
  this settings entry too, so the one swap covers both GHC-internal and
  cabal archive creation.

* bin/nm: nixpkgs emscripten ships no `bin/emnm`; the raw emnm.py
  self-locates via abspath(__file__) for `from tools import shared`, so a
  symlink breaks it.  Generate a wrapper from the nixpkgs emar wrapper
  (same env setup) that execs emnm.py by its real path, and assert the
  substitution matched.

* ghc-interp.js: TH splices run under node against this interpreter
  script, resolved at `$topdir/ghc-interp.js` — install it into the
  target registration.

* enableShared = false for ghcjs (static-only backend).

* mkRawDrv now takes an optional `populatedDb` (register a prebuilt
  target boot-lib closure into the global db, with the confs' pkgroots
  rewritten absolute).  The ghcjs wrapper itself stays the plain empty-db
  crossCompilerBoot: TH-on-JS is handled at the project level by the
  boot-package injection (see modules/cabal-project.nix) — an earlier
  "bootstrap tier" that baked a separately-built ghci closure into the
  global db could never share unit-ids with the project's own boot libs,
  and the JS interpreter then loaded ghc-internal's jsbits twice.

* rts-configure tool discovery: targetBintoolsUnprefixed exposes
  unprefixed nm/objdump for ghc-toolchain; boot-lib harvest helpers
  (findComponent over versioned hsPkgs keys) support the populatedDb
  path.
The new evaluator runs in impure mode, so `builtins.currentSystem` resolves
to the actual builder's system rather than the intended fixed eval platform.
Hardcode `evalSystem = "aarch64-darwin"` to keep it deterministic.

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
`test/setup-deps/default.nix` takes `evalSystem` as a required argument, but
its call site in `test/default.nix` imported it directly (bypassing `callTest`,
which threads `evalSystem`) and never passed it.  Under the impure Hydra
evaluator — where `evalSystem` no longer defaults to `builtins.currentSystem` —
forcing this job throws:

    error: function 'anonymous lambda' called without required argument 'evalSystem'
        | { pkgs, compiler-nix-name, evalPackages, evalSystem }:

Pass `evalSystem` at the call site.  Verified by forcing every test lambda for
ghc984 and ghc914-sh under a non-native `evalSystem` (67 tests each, 0 failures).

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
…oject-local store, LLD)

* build-cabal-store-sync (shell-for-v2.nix) + compose-store.nix: the
  stable-haskell cabal fork's v2 slices use the staged
  host/<platform>/{package.conf.d,units,lib} store layout, not the old
  ghc-<ver>/package.db.  The sync script scanned only ghc-*/ and so
  installed nothing; teach it (and the compose recache) the host layout,
  add the native build->host stage link, and target the project-local
  <builddir>/store (the fork keeps its store there, not ~/.cabal/store) —
  taking BUILDDIR as an arg (default dist-newstyle).

* shell-for-v2.nix: let a `tools` value be a prebuilt derivation (used
  as-is), so a project can drop in the v2 fork cabal
  (haskell-nix.v2-cabal-install) rather than mainline cabal.

* stable-haskell.nix: on darwin, pass ghc-toolchain-bin
  --cc-link-opt=-fuse-ld=<ld64.lld> so ghc914-sh's settings link with LLD.
  The dev-shell/v2 cctools ld (ld64-956.6 + clang/llvm libLTO) SIGTRAPs on
  any Cocoa link; find_ld / ghc-toolchain refuse to search for lld on
  darwin (GHC #21712) and the build sets --disable-ld-override, so we set
  the link flag directly (the ghc-toolchain analogue of useLdLld's
  CONF_GCC_LINKER_OPTS_STAGE2).
Only conflict was the moving `head.hackage.ghc.haskell.org --sha256:` index
pin in test/cabal.project.local; both sides had re-pinned it independently.
Took master's (2026-07-30, f9230d5) over the branch's older one
(2026-07-21, f942734).
Every conflict was one shape: master added the `testCabalProjectLocal` /
`testInputMap` test arguments (the locally built head.hackage, #2560) where
this branch had added `evalSystem`.  Resolved as the union of both.

- test/default.nix: fold `evalSystem` into master's `intersectAttrs`
  mechanism for optional test arguments, rather than keeping this branch's
  separate `wantsEvalSystem` check -- one mechanism instead of two.
- build.nix: master takes `evalPackages` as an argument, which this branch
  replaced with `evalSystem`.  The automatic merge combined this branch's
  signature with master's new head-hackage wiring, leaving `evalPackages`
  unbound; bind it from `evalSystem` via the memoised per-eval-system
  nixpkgs.
- flake.nix: keep both sides' new inputs (hyper-linux, and head-hackage /
  hackage-overlay-repo-tool); take master's removal of the
  `hydraJobs.nix-tools` aggregate, which supersedes this branch's tweak to
  it.

Verified: every test function applies for ghc984, ghc914-sh and a forced
non-native evalSystem=x86_64-linux (68 tests, 0 failures, at ifdLevel 2 so
no IFD), ci.nix composes for all seven unstable compilers, and shell-for's
shell derivation evaluates through the newly built head.hackage.

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
# compilers) need this to suppress the "WARNING: ghc.cachedDeps not found"
# trace and to give make-config-files.nix correct envDeps/exactDeps data.
haskell-nix = prev.haskell-nix // {
compiler = __mapAttrs (name: ghc:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[diff] reported by reviewdog 🐶

Suggested change
compiler = __mapAttrs (name: ghc:
compiler = __mapAttrs (_name: ghc:

Comment thread overlays/hyper-linux.nix
# attribute is lazy, so on build hosts that never cross-compile to Linux (or use
# qemu) it is never forced.
{ sources }:
(final: prev:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[diff] reported by reviewdog 🐶

Suggested change
(final: prev:
(_final: prev:

ghc914-sh's ghcjs cross builds succeeded but every JS executable it
produced aborted at startup:

  Aborted('HEAP8' was not exported. add it to EXPORTED_RUNTIME_METHODS)
    at h$initEmscriptenHeap -> Module.onRuntimeInitialized -> run

Newer emscripten no longer exports HEAP* by default, and GHC's JS RTS
shim reads `Module.HEAP8`, so `rts/js/mem.js` has to name it in its EMCC
`EXPORTED_RUNTIME_METHODS` pragma.  haskell.nix already carries that fix
as overlays/patches/ghc/ghc-9.12-ghcjs-rts-mem-heap8.patch -- but that
list lives in overlays/bootstrap.nix and only reaches the *hadrian*-built
compilers.  ghc914-sh is built by overlays/stable-haskell.nix straight
from this pin, so it never saw it.  Only build-only ghcjs jobs are green;
anything that runs the output fails, which is why it went unnoticed.

Rather than teach stable-haskell.nix a second patch list, the fixes now
live on the branch: stable-ghc-9.14-hn is stable-ghc-9.14 plus

  - JS: export HEAP8 symbol (#26290)   \ cherry-picked from the fork's
  - JS: export HEAPU8 (#26290)         / master via feat/js-export-heap8
  - compiler: trim Apple ranlib's padding from wasm archive members

The third has no upstream equivalent on either stable-ghc-9.14 or master
(master's 894a04f3a8 is already in 9.14 and fixes archive member size
*writing*, not this padding *reader*), so it is committed on the branch.
The third ghcjs patch in that range, ghc-9.13-ghcjs-rts-types.patch, is
NOT needed: stable-ghc-9.14 already lists rts/Types.h in rts.cabal.

Verified through the pin that both fixes are present in the fetched
source.  The functional check -- rebuilding the compiler and running
ghcjs hello under node -- is a full compiler rebuild and is still in
flight; it is also the first compile-test of the Ar.hs change.

Note the flake-compat narHash gotcha: `nix flake update` records a hash
that flake-compat's fetch disagrees with, so the lock carries the
FOD-reported hash instead.

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
…xes)

Ports the rest of the overlays/bootstrap.nix GHC patches that apply at
9.14 onto the fork.  That list is only consulted for the hadrian-built
compilers, and stable-haskell builds ONE multi-target compiler from this
pin rather than one GHC per target, so a target-guarded patch list cannot
serve it -- the fixes have to be in the source.  Merged as three feature
branches:

  feat/aarch64-elf-linker-pool   contiguous linker pool so aarch64 ADRP
                                 relocations stay in range under qemu
                                 (GHC #24432); inert on aarch64-darwin,
                                 which is Mach-O not ELF
  feat/windows-rts-linker-fixes  MinGW RTS symbols time/unix need, and
                                 keep an import symbol's own SymType on
                                 PEi386 lookup
  feat/android-bionic-support    ELF{32,64}_ST_VISIBILITY fallback for
                                 bionic, bare `android` OS in
                                 GHC_CONVERT_OS, COMPAT_R_ARM_PREL31
                                 relocation, skip loading libc.a

Two deviations from the carried patches, both deliberate:

  - The PEi386 fix assigned `*type` unguarded where that function
    otherwise always checks `type`, so a caller passing NULL (lookupSymbol
    does) would segfault.  Guarded in the ported commit.  The hadrian
    compilers still apply the unguarded version.
  - ghc-9.12-static-linker-script-support.patch is NOT ported.  It is 70
    lines across GHC/Linker/{Static,Unit,Loader}.hs and SysTools/Ar.hs; one
    hunk fails outright and the others only match with fuzz 2-3 at offsets
    up to -219 lines, i.e. at the wrong sites.  Those modules have moved
    too far since 9.12 for a mechanical port, and a wrong port here is a
    compiler compile error.  It needs a hands-on port -- or a check that it
    is still needed at all.  Tracked in the audit.

Verified through the pin that all eight ported fixes are present in the
fetched source.  narHash is again the FOD-reported one, not what `nix
flake update` records (flake-compat disagrees with it).

Claude-Session: https://claude.ai/code/session_015iMn9nFmfb5DPqqW5a12co
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Put the RTS in a separate output

1 participant