Home / Input Output / haskell.nix
May 03, 4-5 PM (0)
May 03, 5-6 PM (0)
May 03, 6-7 PM (0)
May 03, 7-8 PM (0)
May 03, 8-9 PM (0)
May 03, 9-10 PM (0)
May 03, 10-11 PM (1)
May 03, 11-12 AM (0)
May 04, 12-1 AM (1)
May 04, 1-2 AM (2)
May 04, 2-3 AM (0)
May 04, 3-4 AM (1)
May 04, 4-5 AM (0)
May 04, 5-6 AM (0)
May 04, 6-7 AM (0)
May 04, 7-8 AM (0)
May 04, 8-9 AM (0)
May 04, 9-10 AM (0)
May 04, 10-11 AM (0)
May 04, 11-12 PM (0)
May 04, 12-1 PM (0)
May 04, 1-2 PM (0)
May 04, 2-3 PM (0)
May 04, 3-4 PM (0)
May 04, 4-5 PM (0)
May 04, 5-6 PM (0)
May 04, 6-7 PM (0)
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)
60 commits this week May 03, 2026 - May 10, 2026
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.
plan-nix: gate `-inplace` on GHC ≥ 9.8
GHC 9.8 added an `-inplace` suffix to its boot-package UnitIds
(`base-4.19.2.0-inplace` vs `base-4.18.3.0` on 9.6) and started
emitting a `Project Unit Id` field in `ghc --info`.  The dummy
ghc/ghc-pkg in `lib/call-cabal-project-to-nix.nix` was hardcoding
both, so cabal computed UnitIds against the dummy with
`-inplace` even on GHC 9.6.  When the slice's real ghc 9.6 then
returned ids without `-inplace`, the slice's UnitId for any
package depending on a boot package (`colour` ↔ `base`) diverged
from plan-nix and the slice failed its UnitId check.

Verified empirically that `-inplace` appears starting at GHC 9.8:
  ghc967  → id: base-4.18.3.0
  ghc984  → id: base-4.19.2.0-inplace
  ghc9103 → id: base-4.20.2.0-inplace
  ghc9124 → id: base-4.21.2.0-inplace
  ghc9141 → id: base-4.22.0.0-inplace

Make both the `Project Unit Id` field and the `-inplace` suffix
conditional on `versionAtLeast ghc.version "9.8"`.

Verified against `tests.shell-for.env` on ghc967 (which exercises
the colour slice that was failing in CI).
test: migrate `.profiled` callers to sibling cabal.project
v2 slices no longer ship `.profiled` as an overlay rebuild — see
`docs/dev/profiling.md`.  Migrate the remaining tests that
referenced `…components.<kind>.<name>.profiled`
(`js-template-haskell`, `th-dlls-minimal`, `gi-gtk`) to a sibling
project whose `cabalProjectLocal` enables profiling, matching the
shape already used by `exe-dlls` / `exe-lib-dlls` / `th-dlls`.

Expose each new project's `plan-nix` under a distinct
`ifdInputs` key so the materialised plan-nix expressions are
covered by CI:

  plan-nix             — non-profiled (default)
  plan-nix-ei          — externalInterpreter variant (th-dlls /
                          th-dlls-minimal)
  plan-nix-profiled    — profiled
  plan-nix-profiled-ei — profiled + externalInterpreter
test/with-packages: use \`project.shellFor\` instead of \`comp.shell\`/\`comp.env\`
v2 slices don't expose v1's per-component \`.shell\` / \`.env\`
attributes — \`shell\` is provided at the project level via
\`project.shellFor\`, and that mode covers what this test needs
(a wrapped \`ghc\` / \`runghc\` that can see the package's deps).
\`exposePackagesVia = "ghc-pkg"\` makes \`shell.ghc\` a wrapped
GHC stacking the composed package.db via \`GHC_ENVIRONMENT\`, so
\`runghc ./Point.hs\` and \`ghc Point.hs\` resolve the deps the
same way v1's \`library.env\` did.

Works under both v1 and v2 — no per-component shell required.
v2 builder: trim dist-newstyle/ from slice $out
The slice's `$out/dist-newstyle/` carried cabal's source tarballs
and the build tree — hundreds of MB to GB per slice for nontrivial
projects.  Nothing downstream of a successfully-built slice reads
it: subsequent slices only pull from `$out/store/`, and the
diagnostic `checkAgainstPlan` is a separate derivation with its
own dist-newstyle.

Trim it at the end of `installPhase`, after
`comp-v2-builder.nix`'s test/bench install step (which does need
to find the unpinged binary in `dist-newstyle/build/.../<exe>`).
Lift `cache/plan.json` to `$out/plan.json` so it stays available
for human debugging — the rest of `cache/` is internal.

Verified on the ghc967.ghcjs.hello slice: $out shrinks from
~7.9GB to ~1MB, with `bin/`, `store/`, `plan.json`, `unit-ids`,
and `nix-support/` retained.
v2 builder: drop \`.jsexe/all.js\` exeExt under cabal v2-build
v1 (Setup.hs install) for ghcjs preserves the
\`bin/<exe>.jsexe/all.js\` directory layout, which is why
\`comp-builder.nix:450\` appends \`.jsexe/all.js\` to the exe name
for \`isGhcjs && ghc < 9.8\`.  v2 builds via \`cabal v2-build\`,
whose install step bundles the \`.jsexe/\` contents into a single
self-contained \`#!/usr/bin/env node\` script at \`bin/<cname>\` —
there is no \`.jsexe/\` directory in the slice's output.  Use
the bundled-file path so v2's \`exePath\` / \`find\` lookup land
on the file cabal actually produced.

Verified against \`aarch64-darwin.unstable.ghc967.ghcjs.hello\`
(slice surfaced \`bin/hello\` cleanly, no placeholder fallback).
v2 builder: align slice/check derivation names with v1
Drops the `cabal-slice-` prefix and reorders the slice's `name`
attribute from `<pkg>-<ver>-<ctype>-<cname>` to v1's shape
`<pkg>-<ctype>-<cname>-<ver>` (mirrors `comp-builder.nix:268`).
Same for the auxiliary `check-*` and `store-*` derivations.

Callers that hardcoded v1 derivation names — most prominently
`tests.coverage.run`, which checks for
`tix/pkgb-test-tests-0.1.0.0-check/tests.tix` — keep working
without per-test edits when projects flip
`builderVersion = 1 → 2`.

Also surface the lib slice's `.mix` files under
`<pkg>-<ver>-inplace/<Module>.mix` so they line up with the
inplace UnitId that test-slice-built `tests.tix` files reference
(`Tix [ TixModule "<pkg>-<ver>-inplace/<Module>" … ]`); the .mix
content itself is byte-identical between the lib slice's
hash-named UnitId build and the test slice's inplace rebuild.

Move the coverage-test's `doCoverage` modules into
`cabalProjectLocal` (`package <pkg>\n  coverage: True`) so v2's
plan-nix records `--enable-coverage` and the slice actually
produces .mix/.tix output.  The module-level overrides are kept
for the v1 builder.

Verified against `tests.coverage.run`.
configure-args: pick up profiling/coverage from plan.json
Plan-to-nix's `modules/install-plan/configure-args.nix` already
translates `--ghc-option=` and `--configure-option=` entries from
plan.json's per-pkg `configure-args` into haskell.nix module
options.  Extend it to also pick up the `--enable-{profiling,
library-profiling, coverage}` toggles so v1's `comp-builder`
(which reads `enableProfiling`, `enableLibraryProfiling`,
`doCoverage` straight off the component) honours
`package <pkg>\n  profiling: True` set in cabal.project — without
needing module-level overrides.

v2 already reads these from plan.json directly via its own
`projectConfigPragmas`; the picked-up values are merely
consistent there.

Migrate the `tests.{exe,exe-lib,th}-dlls.{check-,build-}profiled`
variants to a sibling project whose `cabalProjectLocal` enables
profiling — the v2 builder no longer ships `.profiled` as an
overlay rebuild (see `docs/dev/profiling.md`).
v2 builder: error on `.profiled` with migration hint
v1 slices provided `.profiled` as an overlay rebuild with
`enableLibraryProfiling = true`.  v2 reads configure-args from
plan.json, so an overlay would emit `--enable-profiling` toggles
that plan-nix doesn't know about and the slice's UnitId would
diverge.

Replace the (missing) attribute with a `throw` that points users
at `cabal.project` / `cabalProjectLocal`:

    package <pkgname>
      profiling: True
      library-profiling: True

so plan-nix records the toggles and the slice's UnitId stays
aligned.  Set the `throw` at both the top level and inside
`passthru` because mkDerivation only lifts passthru → top level at
derivation creation time; post-hoc `// { passthru = ... }` doesn't
re-lift.

Adds `docs/dev/profiling.md` with the migration recipe.
v2 builder: surface HPC artefacts and src dir for cover.nix
Three incremental improvements toward making `lib/cover.nix` work
against v2 lib/test slices.  The end-to-end coverage test still
fails because v2's lib slices use a hashed UnitId
(`pkgb-0.1.0.0-<hash>`) while a sibling test slice rebuilds the
lib inplace and emits `.tix` referencing `pkgb-0.1.0.0-inplace`,
so HPC can't match them up.  That alignment is a deeper change;
land the supporting plumbing first:

  * Add `srcSubDir` / `srcSubDirPath` to v2 slice passthru so
    `lib/cover.nix:19`'s `map (l: l.srcSubDirPath) mixLibraries`
    no longer hits "attribute missing".

  * Library slices now copy each `extra-compilation-artifacts/hpc/<way>/{mix,tix}`
    subdir up to `$out/share/hpc/<way>/{mix,tix}/<pkg>-<ver>/`
    (the predictable name `lib/cover.nix` and the cover-report
    tests expect — v1 does this too in `comp-builder.nix:831`).

  * v2's `lib/check.nix` branch copies `<exeName>.tix` from the
    test run to `$out/share/hpc/vanilla/tix/<exeName>/`, mirroring
    v1's `lib/check.nix:119`.
v2 dev shell: speed up haskell-nix-cabal-store-sync
The script ran a per-conf `diff -q` and a per-unit `diff -qr` on
every shell entry — for cardano-wallet's ~240 slices that meant
forking hundreds of `diff` invocations and walking each unit's
file tree.  Two short-circuits:

  * Per-composedStore marker file at
    `~/.cabal/store/.haskell-nix-shell-markers/<basename-of-src>`,
    written after a successful run.  On re-entry the marker is
    present and the script exits before the scan.  Different
    shells have distinct basenames, so alternating shells don't
    invalidate each other's markers.

  * Readlink fast-path in the scan: confs and lib files are
    installed as symlinks to `$src/...`, and unit dirs are lndir
    trees of symlinks under `$src/$ghcName/$unitId/`.  When the
    target is already in the right shape, skip the per-file `diff`
    fork.  Only when the cheap check fails does the script fall
    back to `diff -q` / `diff -qr` to detect genuine conflicts.

Measured on cardano-wallet's shell (~240 slices, fresh CABAL_DIR):
first run 24.5s, second run 0.012s.