Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,71 @@ that will impact users.

## May 23, 2026

**Breaking change:** `cabalProjectLocal` and `cabalProjectFreeze`
no longer auto-load `cabal.project.local` / `cabal.project.freeze`
from the project source. The option types are now `lines`
(default `""`) instead of `nullOr lines` with a `readFile`-based
default.

Projects that relied on the implicit `readFile` behaviour should
set the option explicitly:

```nix
haskell-nix.cabalProject {
src = ./.;
cabalProjectLocal = builtins.readFile ./cabal.project.local;
cabalProjectFreeze = builtins.readFile ./cabal.project.freeze;
# ...
}
```

Reasons for the change:

* The implicit IFD-based default forced every project that
didn't want it (notably internal `hadrian` and
`ghc-extra-projects` builds) to set `cabalProjectLocal = null`
explicitly just to suppress the read.
* The `nullOr lines` type prevented haskell.nix from merging
project-level `cabalProjectLocal` content (`mkBefore` /
`mkAfter`) with explicit user values, which the new
platform-conditional defaults below rely on.

Platform-conditional defaults are now injected into every cabal
project's `cabalProjectLocal`:

* **musl host** — `package * \n executable-static: True`.
comp-builder already adds `--ghc-option=-optl=-static` at
build time; this surfaces the toggle in cabal.project so
plan-to-nix records `--enable-executable-static` for every
unit. Observable build behaviour is unchanged.
* **x86_64-darwin host** — `package * \n library-for-ghci: True`.
Mirrors what comp-builder passes for `!ghcjs && !wasm && !android`,
so plan-nix's recorded UnitIds match the artefacts.
* **android host** — `package * \n ghc-options: -optl-static -optl-ldl`
(plus `-optl-no-pie` on aarch32). Mirrors the
`setupBuildFlags` overrides previously applied only by
`lib/check.nix`'s test-exe re-wrap.
* **wasm GHC ≥ 9.12** — `package * \n shared: True`. Wasm's RTS
linker only loads `.so` files; `--disable-shared` (the cabal
default) would force a `.a`-only install that TH-eval can't
load.

These directives sit at `mkBefore` priority so a project's own
`cabalProjectLocal` overrides them if needed.

**Cache impact:** plan-nix hashes will change for affected
platforms on the next CI run — a one-time rebuild wave.

To opt out of a specific default, override it in your project's
`cabalProjectLocal`:

```nix
cabalProjectLocal = ''
package *
executable-static: False
'';
```

The post-plan `packages.ghc.src` override that
`modules/configuration-nix.nix` used to apply unconditionally is
now opt-in via the new project-level `useLocalGhcLib` option.
Expand Down
2 changes: 0 additions & 2 deletions compiler/ghc/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,6 @@ let
cd hadrian
'';
}];
cabalProjectLocal = null;
cabalProjectFreeze = null;
src = haskell-nix.haskellLib.cleanSourceWith {
src = {
outPath = pkgsBuildBuild.srcOnly {
Expand Down
2 changes: 1 addition & 1 deletion lib/call-cabal-project-to-nix.nix
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ in let
else cabalProject
}
${
pkgs.lib.optionalString (cabalProjectLocal != null) ''
pkgs.lib.optionalString (cabalProjectLocal != null && cabalProjectLocal != "") ''
-- Added from `cabalProjectLocal` argument to the `cabalProject` function
${cabalProjectLocal}
''
Expand Down
98 changes: 92 additions & 6 deletions modules/cabal-project.nix
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,23 @@ in {
type = nullOr lines;
default = readIfExists config.evalSrc config.cabalProjectFileName;
};
# `cabalProjectLocal` / `cabalProjectFreeze` no longer
# auto-load `cabal.project.local` / `cabal.project.freeze` from
# the project source. Projects that want that behaviour set
# the option explicitly:
#
# cabalProjectLocal = builtins.readFile ./cabal.project.local;
#
# Plain `lines` (not `nullOr lines`) so `lib.mkBefore` directives
# from platform-conditional defaults below merge cleanly with
# whatever the user passes.
cabalProjectLocal = mkOption {
type = nullOr lines;
default = readIfExists config.evalSrc "${config.cabalProjectFileName}.local";
type = lines;
default = "";
};
cabalProjectFreeze = mkOption {
type = nullOr lines;
default = readIfExists config.evalSrc "${config.cabalProjectFileName}.freeze";
type = lines;
default = "";
};
ghc = mkOption {
type = nullOr package;
Expand Down Expand Up @@ -154,7 +164,82 @@ in {
'';
};
};
config = lib.mkIf config.useLocalGhcLib (
config = lib.mkMerge [
# Musl host: every executable should be statically linked.
# comp-builder achieves this at build time by adding
# `--ghc-option=-optl=-static` to the per-component
# configureFlags, which doesn't reach plan-to-nix — plan.json
# keeps `--disable-executable-static` and the misalignment is
# ignored. Surface the toggle in cabal.project so plan-to-nix
# records `--enable-executable-static` for every unit; the
# actual build is unchanged. Set inside `package *` because
# cabal only propagates `executable-static` per-component when
# the directive lives in that block.
(lib.mkIf pkgs.stdenv.hostPlatform.isMusl {
cabalProjectLocal = lib.mkBefore ''
package *
executable-static: True
'';
})
# x86_64-darwin host: enable `library-for-ghci` so cabal emits a
# merged `HS<unit>.o` per unit alongside the `.dylib`.
# comp-builder already passes `--enable-library-for-ghci` for
# !ghcjs / !wasm / !android (which on darwin is always),
# so this matches the on-disk artefacts to what plan-to-nix
# records. Helps TH-eval via `ghc-iserv-dyn` find a merged
# `.o` to load directly, bypassing dyld dylib weirdness under
# Rosetta (Hydra builds x86_64-darwin on aarch64-darwin
# hardware). Scoped to x86_64 — aarch64-darwin runs natively.
(lib.mkIf (pkgs.stdenv.hostPlatform.isDarwin
&& pkgs.stdenv.hostPlatform.isx86_64) {
cabalProjectLocal = lib.mkBefore ''
package *
library-for-ghci: True
'';
})
# Android host: link every exe statically so qemu-user can run
# it on the build host. A dynamic Android binary references
# `/system/bin/linker[64]` at runtime; qemu-arm fails with
# `Could not open '/system/bin/linker': No such file or
# directory` because the build host doesn't ship one.
# `lib/check.nix` papers over this by re-overriding the test
# exe with `setupBuildFlags = ["--ghc-option=-optl-static"]`;
# surfacing the same flags at the project level here keeps
# plan-to-nix's recorded configure-args matching the artefact.
# `-optl-static` makes the exe self-contained, `-optl-ldl`
# pulls in libdl that GHC's RTS still references even under
# static linking, and on aarch32 `-optl-no-pie` disables PIE
# so the static link doesn't trip `dynamic STT_GNU_IFUNC`
# relocation errors.
(lib.mkIf pkgs.stdenv.hostPlatform.isAndroid {
cabalProjectLocal = lib.mkBefore ''
package *
ghc-options: -optl-static -optl-ldl${
lib.optionalString pkgs.stdenv.hostPlatform.isAarch32 " -optl-no-pie"
}
'';
})
# wasm 9.12+: real wasm-ghc reports `target RTS linker only
# supports shared libraries: YES` and no `Support shared
# libraries` field at all. cabal interprets the absence of
# `Support shared libraries` as "no shared support" and
# records `--disable-shared` in plan-nix, but TH-eval / dyld
# on wasm 9.12+ requires `.so` files for every transitively
# reachable lib (the RTS linker only loads shared libs).
# Forcing `shared: True` at the project level keeps plan-nix's
# recorded UnitIds matching what a downstream cabal v2-build
# against the real wasm GHC would compute.
(lib.mkIf (
let ghc = (config.compilerSelection pkgs.buildPackages).${config.compiler-nix-name};
in pkgs.stdenv.hostPlatform.isWasm
&& builtins.compareVersions ghc.version "9.12" >= 0
) {
cabalProjectLocal = lib.mkBefore ''
package *
shared: True
'';
})
(lib.mkIf config.useLocalGhcLib (
let
ghc = (config.compilerSelection pkgs.buildPackages).${config.compiler-nix-name};
ghcSrc = (pkgs.buildPackages.symlinkJoin {
Expand Down Expand Up @@ -211,5 +296,6 @@ in {
${builtins.unsafeDiscardStringContext "${ghcMinRepoUrl}/minimal"} = ghcSrc;
};
}
);
))
];
}
3 changes: 0 additions & 3 deletions overlays/ghc-packages.nix
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,6 @@ in rec {
name = "ghc-extra-projects-${ghc-extra-projects-type proj.ghc}-${ghcName}";
src = proj;
inherit (proj) cabalProject;
# Avoid readDir and readFile IFD functions looking for these files
cabalProjectLocal = null;
cabalProjectFreeze = null;
index-state = final.haskell-nix.internalHackageIndexState;
# Where to look for materialization files
materialized =
Expand Down
Loading