Home / Input Output / haskell.nix
May 04, 7-8 PM (6)
May 04, 8-9 PM (0)
May 04, 9-10 PM (0)
May 04, 10-11 PM (0)
May 04, 11-12 AM (0)
May 05, 12-1 AM (2)
May 05, 1-2 AM (0)
May 05, 2-3 AM (0)
May 05, 3-4 AM (0)
May 05, 4-5 AM (1)
May 05, 5-6 AM (2)
May 05, 6-7 AM (0)
May 05, 7-8 AM (0)
May 05, 8-9 AM (0)
May 05, 9-10 AM (2)
May 05, 10-11 AM (1)
May 05, 11-12 PM (0)
May 05, 12-1 PM (0)
May 05, 1-2 PM (0)
May 05, 2-3 PM (1)
May 05, 3-4 PM (0)
May 05, 4-5 PM (0)
May 05, 5-6 PM (0)
May 05, 6-7 PM (0)
May 05, 7-8 PM (0)
May 05, 8-9 PM (0)
May 05, 9-10 PM (0)
May 05, 10-11 PM (0)
May 05, 11-12 AM (0)
May 06, 12-1 AM (1)
May 06, 1-2 AM (0)
May 06, 2-3 AM (0)
May 06, 3-4 AM (0)
May 06, 4-5 AM (0)
May 06, 5-6 AM (0)
May 06, 6-7 AM (0)
May 06, 7-8 AM (0)
May 06, 8-9 AM (0)
May 06, 9-10 AM (0)
May 06, 10-11 AM (0)
May 06, 11-12 PM (0)
May 06, 12-1 PM (0)
May 06, 1-2 PM (0)
May 06, 2-3 PM (0)
May 06, 3-4 PM (0)
May 06, 4-5 PM (0)
May 06, 5-6 PM (0)
May 06, 6-7 PM (0)
May 06, 7-8 PM (0)
May 06, 8-9 PM (0)
May 06, 9-10 PM (0)
May 06, 10-11 PM (0)
May 06, 11-12 AM (0)
May 07, 12-1 AM (1)
May 07, 1-2 AM (0)
May 07, 2-3 AM (0)
May 07, 3-4 AM (0)
May 07, 4-5 AM (0)
May 07, 5-6 AM (1)
May 07, 6-7 AM (0)
May 07, 7-8 AM (1)
May 07, 8-9 AM (0)
May 07, 9-10 AM (0)
May 07, 10-11 AM (4)
May 07, 11-12 PM (0)
May 07, 12-1 PM (0)
May 07, 1-2 PM (0)
May 07, 2-3 PM (0)
May 07, 3-4 PM (0)
May 07, 4-5 PM (0)
May 07, 5-6 PM (0)
May 07, 6-7 PM (0)
May 07, 7-8 PM (0)
May 07, 8-9 PM (0)
May 07, 9-10 PM (0)
May 07, 10-11 PM (0)
May 07, 11-12 AM (1)
May 08, 12-1 AM (2)
May 08, 1-2 AM (2)
May 08, 2-3 AM (3)
May 08, 3-4 AM (1)
May 08, 4-5 AM (1)
May 08, 5-6 AM (0)
May 08, 6-7 AM (1)
May 08, 7-8 AM (1)
May 08, 8-9 AM (2)
May 08, 9-10 AM (1)
May 08, 10-11 AM (0)
May 08, 11-12 PM (0)
May 08, 12-1 PM (0)
May 08, 1-2 PM (0)
May 08, 2-3 PM (0)
May 08, 3-4 PM (0)
May 08, 4-5 PM (0)
May 08, 5-6 PM (0)
May 08, 6-7 PM (0)
May 08, 7-8 PM (0)
May 08, 8-9 PM (0)
May 08, 9-10 PM (1)
May 08, 10-11 PM (0)
May 08, 11-12 AM (1)
May 09, 12-1 AM (1)
May 09, 1-2 AM (0)
May 09, 2-3 AM (0)
May 09, 3-4 AM (1)
May 09, 4-5 AM (1)
May 09, 5-6 AM (0)
May 09, 6-7 AM (0)
May 09, 7-8 AM (0)
May 09, 8-9 AM (0)
May 09, 9-10 AM (0)
May 09, 10-11 AM (2)
May 09, 11-12 PM (0)
May 09, 12-1 PM (0)
May 09, 1-2 PM (0)
May 09, 2-3 PM (0)
May 09, 3-4 PM (0)
May 09, 4-5 PM (0)
May 09, 5-6 PM (0)
May 09, 6-7 PM (0)
May 09, 7-8 PM (0)
May 09, 8-9 PM (0)
May 09, 9-10 PM (0)
May 09, 10-11 PM (0)
May 09, 11-12 AM (0)
May 10, 12-1 AM (3)
May 10, 1-2 AM (0)
May 10, 2-3 AM (0)
May 10, 3-4 AM (0)
May 10, 4-5 AM (0)
May 10, 5-6 AM (0)
May 10, 6-7 AM (0)
May 10, 7-8 AM (0)
May 10, 8-9 AM (1)
May 10, 9-10 AM (0)
May 10, 10-11 AM (2)
May 10, 11-12 PM (0)
May 10, 12-1 PM (0)
May 10, 1-2 PM (4)
May 10, 2-3 PM (0)
May 10, 3-4 PM (0)
May 10, 4-5 PM (0)
May 10, 5-6 PM (0)
May 10, 6-7 PM (0)
May 10, 7-8 PM (0)
May 10, 8-9 PM (3)
May 10, 9-10 PM (1)
May 10, 10-11 PM (4)
May 10, 11-12 AM (0)
May 11, 12-1 AM (2)
May 11, 1-2 AM (0)
May 11, 2-3 AM (0)
May 11, 3-4 AM (0)
May 11, 4-5 AM (0)
May 11, 5-6 AM (0)
May 11, 6-7 AM (0)
May 11, 7-8 AM (0)
May 11, 8-9 AM (0)
May 11, 9-10 AM (0)
May 11, 10-11 AM (0)
May 11, 11-12 PM (0)
May 11, 12-1 PM (1)
May 11, 1-2 PM (0)
May 11, 2-3 PM (0)
May 11, 3-4 PM (0)
May 11, 4-5 PM (0)
May 11, 5-6 PM (0)
May 11, 6-7 PM (0)
May 11, 7-8 PM (0)
61 commits this week May 04, 2026 - May 11, 2026
v2 builder: use .conf 'name:' field over uid prefix for pkg matching
cabal's OS-prefix patch shortens unit-id prefixes (e.g.
`bytrdr-1.0.4-...` for the `byteorder` package), so parsing the
pkg-name from the uid alone misclassified lib units and failed the
post-install captured-unit check with
`slice captured 0 units for the target package 'byteorder'` even
when the captured uid WAS the right one.

Read the authoritative pkg-name from each unit's `.conf` (which
records the real package name) and fall back to uid parsing only
for bin-only units that don't have a .conf.
v2 builder: inject component.libs paths into post-register .conf
GHC's runtime linker (the one TH eval uses, including under
`-fexternal-interpreter` / `ghc-iserv`) dlopens each loaded
package's `extra-libraries:` entries by bare name (e.g.
HsOpenSSL's `extra-libraries: ssl crypto` → `libssl.so`).
Cabal records empty `library-dirs:` etc. in the per-unit
`.conf` because we deliberately don't push these paths into
the slice's configure args (that would land in
`pkgHashExtraLibDirs` and fork the slice's unit-id from
plan-nix).

v1 baked these into the .conf via `make-config-files.nix`'s
`extra-lib-dirs` flag fed to `Setup configure` (v1 used its
own unit-id scheme so the hash didn't matter).  In v2 we
post-process: after `cabal v2-build` registers the unit, append
the lib paths to the `library-dirs:` / `library-dirs-static:` /
`dynamic-library-dirs:` fields, then `ghc-pkg recache`.

Downstream consumers' unit-ids are unaffected — their config hash
records each dep's unit-id, not the dep's .conf content — so the
chain stays plan-nix-consistent end to end.
v2 builder: tighten unit-id pkg-name parser to a strict version pattern
`iserv-proxy-9.3-011cc258...`'s hash starts with `0`, which the
old `^[0-9]` regex matched, leaving the parser thinking the hash
was the version and producing `iserv-proxy-9.3` as pkg-name.
Use `^[0-9]+(\.[0-9]+)*$` so only true version tokens match
(the hash, being all-hex with mixed letters, never matches).
v2 builder: compose cross build-tool slice (matching uid) on cross
Build-tool slices are the unit cabal needs to find in the
cabal-store to skip re-building.  Previously we composed the
build-build slice (e.g. native hsc2hs), whose plan-nix-computed
uid Y doesn't match what the cross slice's cabal computes for the
tool (uses cross dummy-ghc info, matching real cross-ghc info per
tests.dummy-ghc-info).  cabal didn't recognise the composed slice
and rebuilt the tool from source.

Compose the CROSS hsPkgs's tool slice instead (`targetSlice` in
the new `transitiveBuildToolEntries`).  Its uid matches what the
slice's cabal computes, so cabal recognises the tool as already
installed and skips re-building.  The cross binary isn't runnable
on the build host, but cabal only needs the unit-id-keyed dir
present in the cabal-store; actual invocation goes through
`--with-PROG=<buildSlice>/bin/<exe>` which points at the
build-build binary.

Skip the expected-uid check on cross (`expectedUnitId = null`):
the slice's network unit-id picks up `--with-PROG=PATH` into
pkgHashProgramArgs, which plan-nix doesn't have, so the two uids
legitimately diverge.  The post-install captured-set check now
verifies the slice produced exactly one unit for the target
package plus zero-or-more units for the allowed transitive
build-tools (a safety net for cases where cabal didn't recognise
a composed targetSlice's uid).  The dry-run plan check similarly
tolerates build-tool packages — anything else is still a fail.
v2 builder: keep build-tool tarballs in the slicing repo on cross too
The cabal solver in the slice always plans every transitive
`build-tool-depends:` package, even when `--with-PROG=PATH` is
set — `--with-PROG` only short-circuits at build time, after the
solver has succeeded.  Leaving the build-tool's source out of the
index made the solver fail with
`unknown package: <pkg>:<exe>:exe.<exe> (dependency of ...)`.

Restore the build-tool's transitive tarballs (the tool itself
plus its lib closure) for every build.  On native cabal recognises
the pre-installed bb slice's unit-id and skips re-building; on
cross `withProgFlags` provides `--with-<exe>=<bb-slice>/bin/<exe>`
so cabal short-circuits the build despite the divergent unit-id.

The dry-run `expectedPackage` check is skipped on cross (cabal
*plans* every build-tool there, by design), leaving the
post-install captured-unit-ids count==1 check as the guarantee
that cabal did short-circuit the tool builds.
v2 builder: cross builds steer cabal off build-tool goals via --with-PROG
On native, build-tool source tarballs go in the slicing repo (as
before) so cabal recognises the pre-installed bb slice's unit-id
and skips re-building.  Restored to old behaviour, including
merging `homeBuildToolIds` into `externalDepIds`.

On cross the unit-ids legitimately diverge (cross GHC info ≠
build-platform GHC info), so the bb hsc2hs slice would never
match what cabal-in-slice computes; cabal would plan it from
source and fork.  Instead:

  * keep the build-tool's source OUT of the slicing repo
    (`externalDepIds` skips `homeBuildToolIds`; `depTransitives`
    skips the `buildToolSlices` tarballs);
  * emit `--with-<exe>=<bb-slice>/bin/<exe>` for every transitive
    build-tool reached in `allDepClosure` — cabal skips planning
    the tool from source and uses the explicit path;
  * skip the unit-id MATCH check (`expectedUnitId = null`), but
    `build-cabal-slice` now verifies exactly ONE unit-id is
    captured so the slice still produces a single, well-defined
    `$out/store` content.
v2 builder: split sibling exe-deps from lib-deps
The v2 slice handles sibling `depends` and `exe-depends` from
plan-json differently:
  * lib-deps land in `externalDepIds`, drive the slicing repo
    (via `depTransitiveTarballsOf`), and get pinned in
    `libConstraintPins`.
  * exe-deps (build-tools) need their exe on PATH but their
    source MUST stay out of the slicing repo's index — otherwise
    cabal's solver in the slice plans the tool from source for
    `build-tool-depends: foo:foo`-style deps and forks the
    tool's unit-id (cross GHC info ≠ build-platform GHC info).

Split the plan-json walk in hspkg-builder into `homeDependIds`
(lib) and `homeBuildToolIds` (exe).  The v2 builder's
`homeDepExeSlices` now feeds from the latter; `externalDepIds`
naturally drops exe-only entries.
v2 builder: drop build-tool source tarballs from the slicing repo
Build-tool exes (hsc2hs / alex / happy / ...) reach the slice's
build environment via `extraNativeBuildInputs` (PATH).  Their
source tarballs were also being added to the slicing repo's index
via `++ lib.concatMap (s: s.passthru.transitiveTarballs) buildToolSlices`,
which let cabal's solver see hsc2hs in the index and plan a
from-source rebuild for `build-tool-depends: hsc2hs:hsc2hs` —
forking the tool's unit-id (cross GHC info ≠ build-platform GHC
info) and failing the expected-package check.

Drop the line and rely on cabal's legacy PATH fallback for build-tool
resolution.  The tool's lib closure (process, directory, ...) still
reaches the slicing repo when those libs are also lib-deps of the
target package; otherwise they're not needed (the slice doesn't
re-solve the tool).
v2 builder: skip exe entries when forming libConstraintPins
The bb-plan walk added by the previous commit pulled exe entries
(hsc2hs / alex / happy / ...) into `allDepClosure`.  Pinning them
in `libConstraintPins` lands them in `extra-packages:`, which
makes cabal's solver in the slice plan the build-tool from source
instead of using the pre-installed exe slice.  Skip closure
entries whose `component-name` starts with `exe:` — their lib
closures (process, directory, ...) still get pinned via the
`libDepsOf` walk in bb-plan.
v2 builder: route exe-depends closure walk through pkgsBuildBuild's plan
Build-tool exes (hsc2hs / alex / happy / ...) run on the build
platform; their unit-ids and lib-dep closures belong to the
build-build project's plan, not the cross plan.  Previously
\`mkClosureFrom\` resolved every \`exe-depends\` id against the cross
plan-json — pulling in cross-target unit-ids and (transitively) the
wrong versions of process / directory / etc. for the slice's
\`libConstraintPins\` and \`sourceRepoEntries\`.

Tag every closure node with its plan ("cross" or "bb"), and have
\`exe-depends\` ids cross over to the bb plan via direct id lookup
(rare hit) or pkg-name fallback through \`package-ids-by-name\`.
Lib-deps of bb-tagged nodes stay in bb-plan; lib-deps of cross-tagged
nodes stay in cross.  \`sourceRepoEntries\` now dispatches on
\`e.plan\` for the \`hsPkgs\` lookup.

\`libConstraintPins\` reverts to \`allDepClosure\` so cabal in the
slice pins the bb plan's versions for an exe's lib closure (matching
what the bb slice was built against).

Plumbed via \`hsPkgs.{plan-json-by-id, package-ids-by-name}\` exposed
in component-driver — \`hsPkgs.pkgsBuildBuild.<idx>\` reaches the
bb project's indexes naturally.
dummy-ghc: report Host=buildPlatform; fix ghcjs/wasm interpreter
Driven by `tests.dummy-ghc-info` failures.  Two fixes:

  * Real cross-GHC reports `Host platform = buildPlatform` (where
    ghc itself runs), NOT the cross-target platform.  `pkgs.stdenv.hostPlatform`
    in a cross-pkgs context IS the cross target, so the previous
    `Host platform = pkgs.stdenv.hostPlatform` was wrong.  Use
    `buildPlatform` for both `Build platform` and `Host platform` —
    `Target platform` carries the cross target.

  * ghcjs / wasm cross-GHCs report `Have interpreter: NO`,
    `Use interpreter: NO` (the JS / WASM backends don't have a
    GHCi-style bytecode interpreter).  Dummy was emitting `YES`
    for both.  Flip them to match.

Verified `tests.dummy-ghc-info` passes on the
`aarch64-darwin.unstable.ghc9141.{native,ghcjs}` jobsets.  The
test will now catch any future divergence between dummy-ghc and
real GHC's `--info` output.
dummy-ghc: extract to its own file + add cross-compiling field + test
Move the eval-time `dummy-ghc` script construction from
`lib/call-cabal-project-to-nix.nix` to a standalone
`lib/dummy-ghc.nix` so it can be tested independently.

Add a new test `tests.dummy-ghc-info` that diffs `dummy-ghc --info`
against the real GHC's `--info` (after stripping nix-store paths
and a hand-curated list of fields cabal-install does not consult
for elaboration / unit-id hashing).  The test fails on any
remaining divergence, surfacing where `dummy-ghc.nix` needs to
mirror real GHC more closely.

Two divergences caught and fixed by the new test on the native
ghc9141 jobset:

  * Real GHC always emits `("cross compiling","YES"/"NO")`; dummy
    didn't.  Cabal-install reads this field to decide whether to
    apply cross-toolchain detection.  Add it, gated on
    `buildPlatform.config != targetPlatform.config`.

  * Real GHC 9.14.1 omits the legacy `Support shared libraries`
    field on native targets — cabal infers shared-lib support from
    `Support dynamic-too` + `RTS ways`.  Dummy was emitting
    `("Support shared libraries","YES")` in its "otherwise" branch,
    creating a phantom field real GHC doesn't have.  Drop it.

Plus minor cosmetic: dummy emitted the alist's first entry as
`("target os", "OSDarwin")` (with a space after the comma); real
GHC omits the space.  Tighten dummy to match.

`tests.dummy-ghc-info` is registered alongside other tests in
`test/default.nix` so it runs across every cross-target jobset
(armv7a-android, x86_64-musl, ghcjs, wasi, mingwW64, …).  Future
divergences land as test failures rather than silent UnitId forks
between plan-nix and the v2 slice.
dummy-ghc: extend Android branch to cover pkgsStatic
The Android dummy-ghc branch added in 491a91d13 reports a stage-1
GHC built without dynamic support: no `_dyn` RTS ways,
`GHC Dynamic: NO`, no `Support shared libraries` field.  The same
shape applies to nixpkgs's `pkgsStatic` (the haskell.nix `static`
jobset) — its real GHC is also built without dynamic support.
The only difference is `Stage`: Android cross is stage-1, the
static GHC is stage-2.

Without this branch, plan-to-nix records `--enable-shared` for
static-target packages while the slice's real ghc silently flips
to `--disable-shared`, forking the UnitId on `pkgHashSharedLib`
and breaking the slice's expected-package check.

Extend the Android branch's predicate to
`isAndroid || isStatic` and emit the right `Stage` per platform.
Reference: `pkgsStatic.haskell-nix.compiler.ghc9141 --info` on
the live `x86_64-unknown-linux-musl` derivation.

Verified: `tests.js-template-haskell.check` on
`x86_64-linux.unstable.ghc9141.static` builds end-to-end (slices
align, the test exe links statically as expected, the `.check`
derivation completes).
shellFor: route through buildPackages so cross targets evaluate
`shellFor` (both v1 and v2) ran into "no C compiler provided for
this platform" under cross targets where the host stdenv has no
cc — most visibly ghcjs, also wasm and android-prebuilt.  Three
distinct issues, all fixed here:

1. `mkShell` itself was `pkgs.mkShell` (host-stdenv-bound).
   `pkgs.bashInteractive`'s eval reads `stdenv.cc.isClang`, and
   under ghcjs cross there is no cc → throw.  Pass
   `mkShell = pkgs.buildPackages.mkShell` to both shellForV1 and
   shellForV2 so the shell drv lives on the build platform
   regardless of the host (the user opens the shell on their
   build host anyway).

2. `pkgs.runCommand` was building `packageEnv` and `wrappedGhc`
   (the wrapped GHC the shell exports as `shell.ghc`) against
   the cross stdenv.  Once these landed in the shell's
   `nativeBuildInputs`, mkShell's dep-splicing eventually
   re-evaluated the cross stdenv's bash via setup hooks, hitting
   the same cc.isClang.  Switch both to
   `pkgs.pkgsBuildBuild.runCommand` and `pkgs.pkgsBuildBuild.makeWrapper`
   so the wrappers themselves are build-platform drvs.  (Native
   builds are unaffected — `pkgsBuildBuild == pkgs` there.)

3. `store = composedStore` was a top-level attr to mkShell.
   Recent nixpkgs's `mkDerivation` folds unknown top-level attrs
   into `env` and type-checks each value, and `isDerivation
   composedStore` forces evaluation of the cross stdenv's bash
   the same way.  Move `store` into `passthru.store` — mkDerivation
   lifts passthru into the result drv anyway, so `shell.store`
   continues to resolve.

Verified `tests.with-packages.test-shell` now evaluates under both
the `aarch64-darwin.unstable.ghc9141.ghcjs` (cross) and `.native`
jobsets, and `shell.store` still resolves on both.
dummy-ghc: add Android branch matching real cross-ghc capabilities
`dummy-ghc` is the eval-time stand-in cabal-install runs against
during plan-to-nix.  Its `--info` output shapes plan.json's
per-unit `configure-args` (which feed `pkgHashConfigInputs` and
therefore the UnitId).  When the dummy reports different
capabilities than the real cross-ghc the slice rebuilds with,
plan-nix and slice compute different UnitIds and the slice's
expected-package check fails.

The "otherwise" branch (Linux / Darwin / native) reports
`Support shared libraries: YES`, `GHC Dynamic: YES`, RTS ways
including the full `_dyn` family, and `Stage: 2`.  Real Android
cross-ghc (`aarch64-unknown-linux-android-ghc 9.14.1`) is a
stage-1 NDK build with no `_dyn` ways and no `Support shared
libraries` field at all (its `GHC Dynamic` is `NO`).  Falling
through to "otherwise" made plan-nix record `--enable-shared`
for Android packages while the slice's real ghc silently flipped
to `--disable-shared`, forking the UnitId on
`pkgHashSharedLib`.

Add an explicit Android branch.  Reference values cross-checked
against `aarch64-unknown-linux-android-ghc --info` on the live
9.14.1 cross GHC derivation; key fields:
  * Support dynamic-too: YES   (GHC's flag, kept like real ghc)
  * GHC Dynamic: NO            (was YES — drove --enable-shared)
  * RTS ways: no `_dyn` family
  * Stage: 1
  * No `Support shared libraries` field (cabal interprets
    absence as no-shared-libs, matching real ghc)

Verified `tests.extra-hackage` slices for
`x86_64-linux.unstable.ghc9141.aarch64-android-prebuilt` no
longer fail the slice's UnitId-alignment check.  (The `.run`
step still fails because qemu-aarch64 can't load Android's
`/system/bin/linker64` to actually exec the binary — that's a
pre-existing runtime issue, not a v2 alignment problem.)

For musl, real and dummy ghc already agree
(`Support dynamic-too: YES, GHC Dynamic: YES`); the
`executable-static: True` injection in
`modules/cabal-project.nix` stays — it's a user-opt-in flag,
not capability-derived from `ghc --info`.
cabal-project: inject `executable-static: True` for musl hosts
Plan-to-nix's cabal-install records `--disable-executable-static`
in `configure-args` for every component when the project hasn't
explicitly enabled it.  v1's `builder/comp-builder.nix:384`
papered over this by injecting `--ghc-option=-optl=-static` (and
`-optl=-pthread`) at comp-builder time — outside plan.nix's view.
v1 doesn't enforce UnitId alignment with plan.json, so the
post-plan flag insertion is harmless there.

Under v2 the slice mirrors plan.json's `configure-args` exactly,
so without something at the project level the slice links
dynamically and the `tests.c-ffi.run` musl assertion (`grep "not
a"` on glibc-ldd output) fails.

Inject `executable-static: True` into `cabalProjectLocal` when
`stdenv.hostPlatform.isMusl`.  Plan-to-nix then records
`--enable-executable-static`, v2's
`comp-v2-builder.nix:projectConfigPragmas` round-trips it through,
and cabal links the executable statically.  v1 is unaffected — its
post-plan `-optl=-static` injection still fires, redundant but
harmless.

Verified by running `tests.c-ffi.run` on the
`x86_64-linux.unstable.ghc9141.musl64` jobset
(commit `d027fbe4e` + this patch in a fresh worktree).
Replace post-plan ghc.src override with project-level useLocalGhcLib
`modules/configuration-nix.nix` used to unconditionally `mkForce`
`packages.ghc.src` to a symlinkJoin of `(configured-src + generated)`
under `compiler/`, redirecting `lib:ghc`'s source to the local GHC
tree *after* plan-nix had been computed against hackage's
`ghc-X.Y.Z.tar.gz`.  v1 (Setup.hs) didn't care — it builds whatever
`src` it's given.  v2 forks the slice's UnitId from plan-nix's
recorded one (the slice rebuilds from a different source than the
planner saw) and the slice fails its UnitId check.

Replace with an opt-in project-level flag, `useLocalGhcLib`, that
exposes the GHC compiler tree to the planner up-front.  Mechanism
differs by project type:

  * Cabal projects (modules/cabal-project.nix) inject a
    `source-repository-package` block + `allow-boot-library-installs:
    True` into `cabalProjectLocal`, with an `inputMap` entry keyed
    by `<url>/<ref>` (string-context-stripped) so haskell.nix
    short-circuits the `builtins.fetchGit` path.  Plan-to-nix and
    the v2 slice both see the same wrapped repo, cabal hashes the
    same content into `pkg-src-sha256`, and UnitIds align.

  * Stack projects (modules/stack-project.nix) re-add the
    `packages.ghc.src` override as a contributed module, gated on
    `useLocalGhcLib`.  Stack-to-nix's input shape doesn't fit the
    cabal source-repo path; instead we lean on stack only supporting
    v1 for now (`builderVersion = mkForce 1` in stack-project.nix's
    config) — v1 doesn't enforce UnitId alignment, so the post-plan
    swap is fine.

The option itself lives in `modules/project-common.nix` so both
project types see it without duplication.

`builder/comp-v2-builder.nix` emits `allow-boot-library-installs:
True` in the slice's cabal.project whenever the target or any non-
pre-existing lib dep is on cabal-install's hard-coded
non-reinstallable list (`ghc`, `template-haskell`, `Cabal`,
`Cabal-syntax`, `ghc-prim`, `ghc-bignum`, `ghc-boot`,
`ghc-boot-th`, `ghc-heap`, `base`, `ghci`, `ghc-internal`, `rts`).
Without it the slice's solver rejects the source instance with
"constraint from non-reinstallable package requires installed
instance" once `ghc` is being source-built.

`test/ghc-lib-reinstallable/cabal.nix` and
`test/ghc-lib-reinstallable/stack.nix` opt in via
`useLocalGhcLib = true`.  The stack test's resolver is bumped to
`nightly-2026-05-03` (latest known to the pinned stackage.nix at
the time of this commit).

Verified end-to-end:
  * `tests.ghc-lib-reinstallable-cabal.run` (v2, ghc9141) — passes
  * `tests.ghc-lib-reinstallable-cabal.run` (v1, ghc9141) — passes
  * `tests.ghc-lib-reinstallable-stack.run` (v1, ghc9124) — passes
v2 builder: fix `ghcShim` collision when cross GHC ships unprefixed tools
GHC 9.14.1's `armv7a-android` cross GHC ships *both*
`<prefix>deriveConstants` and an unprefixed `deriveConstants` in
its `bin/` (the latter for build-host use).  `ghcShim`'s
single-pass loop iterated `${ghc}/bin/*` alphabetically, hit
`armv7a-...-deriveConstants` first, and the case branch
synthesised an unprefixed-fallback alias
`$out/bin/deriveConstants -> <prefix>deriveConstants` (the
`[ -e ]` guard was for that fallback, not the raw link).  When
the loop later reached the real `deriveConstants`, the raw
`ln -s "$f" "$out/bin/$base"` crashed with "File exists".

Earlier cross GHCs only shipped `<prefix>deriveConstants`, so
the fallback was the sole producer of `$out/bin/deriveConstants`
and never collided.  GHC 9.14.1 added the unprefixed sibling and
exposed the race.

Switch to a two-pass shape: pass 1 links every source bin/ entry
under its own name, pass 2 synthesises unprefixed aliases only
for prefixed names that have no real unprefixed sibling.  Real
files always win; aliases only fill genuine gaps.

Same fix in both `builder/build-cabal-slice.nix` and
`builder/shell-for-v2.nix` (which carries an identical shim for
the dev shell's cross-cabal wrapper).
v2 builder: expose `.doc` via sibling `cabal v2-haddock` slice
Under v1 every component's `.doc` was a sibling Setup haddock
derivation that shared the lib's UnitId.  Under cabal v2-build
this no longer holds: `cabal v2-haddock` flips
`elabBuildHaddocks` (and the haddock-html / haddock-hscolour /
... family) on every unit's `ElaboratedConfiguredPackage`, which
all land in `pkgHashConfigInputs`.  Calling `cabal v2-haddock`
against a plan that didn't already have `documentation: True`
forks every UnitId in the closure and triggers a from-source
rebuild.

Round-tripping `documentation: True` through plan.json's
`--ghc-option=-haddock` / `configure-args.nix` /
`ghc-options: -haddock` is NOT equivalent: the ghc-option keeps
haddock comments in `.hi` files but doesn't set the haddock-config
booleans, so the slice's `pkgHashConfigInputs` diverges from
plan-nix's and the dep closure's UnitIds fork (observed on
OneTuple in `tests.sublib-docs`: plan-nix `9a847723` vs slice
`f546bd36`).

Approach:

  * `comp-v2-builder.nix` detects per-package `--ghc-option=-haddock`
    in plan.json (the `documentation: True` signal cabal-install
    surfaces) and emits `package <pkg>\n  documentation: True\n`
    in the slice's cabal.project for every documented package.
    `-haddock` is filtered out of the per-pkg `ghc-options:` block
    so cabal doesn't see it twice in `pkgHashGhcOptions`.

  * Each library slice exposes `.doc`, a sibling derivation that
    runs `cabal v2-haddock` against the already-built unit (no
    closure rebuild).  `.doc` throws with a migration hint when
    documentation isn't in the project's plan-json — that's the
    only shape where the UnitIds align.

  * Doc slices propagate `(map d: d.doc)` for `docEnabled` deps
    only, so cross-package hyperlinks resolve.  Mixed projects
    (some packages docs, others not) keep working because non-doc
    deps come in as plain slices.

  * `build-cabal-slice.nix` keeps cabal's native unit-dir layout
    (`$out/store/<ghc>/<unit-id>/share/doc/html/`) so doc slices
    `lndir` into `~/.cabal/store/` as drop-in replacements, and
    cross-package hyperlinks (absolute
    `file:///nix/store/<doc-slice>/store/<ghc>/<dep-uid>/...`)
    resolve back into the slice's own tree.  Non-target unit
    haddocks are stripped from `$out` to keep each slice lean —
    deps' html lives in the deps' own `.doc` slices.

`docs/dev/haddock.md` documents the v2 semantics, the
`documentation: True` requirement, and why there's no
`slice.haddockDir` (local plan-nix UnitIds use `<pkg>-<ver>-inplace`
form while the slice's cabal-store uses cabal's mangled hash form,
so there's no eval-time-stable html path — callers `find` it
under the doc slice).

Verified `tests.sublib-docs.run` builds Lib.html and Slib.html
under `slice.doc`, and OneTuple's UnitId in plan.json matches
plan-nix's recorded id.