A NixOS-style module system for building Haskell projects. One declarative project configuration drives interchangeable build backends ("drivers"):
- haskell.nix: IOG's haskell.nix. Full cabal solving against a pinned Hackage, per-component builds, first-class cross-compilation.
- nixpkgs: the Haskell infrastructure of nixpkgs
(
haskell.packages.<compiler>,callCabal2nix,shellFor). No solver; dependency versions come from the nixpkgs package set, and most of the dependency closure comes straight from cache.nixos.org.
Every option of the common module is honored by every driver. A check
enforces that totality (see Checks). Driver-specific
configuration lives under the driver's own namespace (haskell-nix.*,
nixpkgs.*).
The common options are also mirrored under each driver's namespace, seeded with the project-wide values. A definition there overrides the common value for that driver only.
packages.reflex-dom.flags.webkit2gtk = false; # both drivers
nixpkgs.packages.reflex-dom.flags.webkit2gtk = false; # nixpkgs driver onlylet nix-haskell = import ./deps/nix-haskell {};
in nix-haskell { src = ./.; }Pin this repository together with its submodules. The repository keeps haskell.nix under pins/ as a git submodule. A pin that fetches only a tarball leaves pins/haskell-nix empty.
Use the pin form that fetches submodules:
- For a nix-thunk, set
fetchSubmodules = true. - For a flake input, use
git+https://github.com/reflex-frp/nix-haskell?submodules=1.github:reflex-frp/nix-haskelldoes not work.
The result is an attribute set:
{
config # Evaluated module configuration
pkgs # The nixpkgs package set of the evaluation
haskell-nix # haskell.nix driver (.project, .ghcWithPackages)
nixpkgs # nixpkgs driver (.project, .ghcWithPackages)
project # Per-driver projects (project.haskell-nix, project.nixpkgs)
ghcWithPackages # Per-driver ghcWithPackages
manual # Documentation (manual.man, manual.md, manual.view)
}Both projects support .override for composing additional configuration:
let project = (nix-haskell ./project.nix).nixpkgs.project;
in project.override { ghcOptions = [ "-O2" ]; }Overrides use recursive merge: lists are concatenated, attrsets are merged recursively.
{
inputs.nix-haskell.url =
"git+https://github.com/reflex-frp/nix-haskell?submodules=1";
outputs = { nix-haskell, ... }:
let lib = nix-haskell.lib.x86_64-linux;
project = lib.nix-haskell ./project.nix;
in {
packages.x86_64-linux.default = project.haskell-nix.project;
};
}All attributes from default.nix are available as functions in lib.<system>:
lib.config module # (nix-haskell module).config
lib.haskell-nix module # (nix-haskell module).haskell-nix
lib.nixpkgs module # (nix-haskell module).nixpkgs
# etc.Applicable to every driver. The full reference is in the manual.
| Option | Type | Default | Description |
|---|---|---|---|
name |
nullOr str |
from src |
Project name |
src |
path |
none | Project source directory |
system |
str |
builtins.currentSystem |
Build system |
compiler |
submodule |
the driver's own | GHC to build with: a name, a package from outside the driver's sets, per platform |
clean-src |
bool |
true |
Filter src through its .gitignore |
clean-src-ignore-files |
listOf str |
[ "/.gitignore" ] |
Ignore files to read, relative to the tree root |
clean-src-patterns |
lines |
"" |
Extra gitignore patterns |
ghcOptions |
listOf str |
[] |
Project-wide GHC flags |
cabalProject |
nullOr lines |
null |
cabal.project content (replaces the file) |
cabalProjectLocal |
nullOr lines |
null |
cabal.project.local content |
cabalProjectFileName |
str |
"cabal.project" |
Name of the project file |
extraCabalProject |
listOf lines |
[] |
Lines appended to cabal.project |
inputMap |
attrs |
{} |
URL to source mappings |
sha256map |
nullOr attrs |
null |
Hashes for sources named in cabal.project |
packages |
attrsOf submodule |
{} |
Per-package customization |
platforms |
attrsOf submodule |
{} |
Per-package customization for one cross platform only |
source-repository-packages |
attrsOf (path | attrs) |
{} |
Local packages to include |
hackage-overlays |
listOf attrs |
[] |
Packages not on Hackage |
shell |
submodule |
Development shell | |
optimizations |
submodule |
off | GHC optimization flag presets |
wasm-opt |
submodule |
-O2, shrunk |
What wasm-opt does to a built wasm binary |
closure-compiler |
submodule |
ADVANCED |
What closure-compiler does to a built jsexe |
inputs |
attrsOf raw |
pins/ |
Dependency sources |
Naming one of the driver's own compilers is all most projects need:
compiler.name = "ghc912";A compiler from outside those package sets, such as a bindist or a cross
toolchain, is given as a package instead. Both drivers read a handful of
attributes off a compiler, and a bindist does not carry them, so they are
given alongside it. toolchain names the C tools the compiler was
configured with. Everything built with the compiler is pointed at those
tools:
compiler.platforms.wasi32 = {
package = wasm-ghc;
version = "9.12.4.20260731";
targetPrefix = "wasm32-wasi-";
enableShared = true;
haskell-nix.libDir = "lib";
nixpkgs.enableExternalInterpreter = false;
toolchain = {
package = wasi-sdk;
cc = "wasm32-wasi-clang";
ar = "llvm-ar";
ld = "wasm-ld";
strip = "llvm-strip";
};
};| Field | Read by | Meaning |
|---|---|---|
name |
both | The compiler's name in the driver's package sets, and the name packages are pinned under |
package |
both | A compiler used directly instead of one of the driver's |
version |
both | Its version, and the release whose package set and shell tools stand in for what cannot be built with it |
targetPrefix |
both | The prefix its executables carry |
enableShared |
both | Whether it builds shared libraries |
toolchain |
both | The C tools it was configured with (package, cc, ar, ld, strip) |
haskell-nix.libDir |
haskell.nix | Where its package database and settings live, relative to its store path |
haskell-nix.extraNonReinstallablePkgs |
haskell.nix | Packages taken from its own database rather than built, on top of the usual ones |
nixpkgs.haskellCompilerName |
nixpkgs | Its cabal name, which names package database directories and is cabal2nix's --compiler |
nixpkgs.enableExternalInterpreter |
nixpkgs | Whether Template Haskell splices are proxied to the target |
platforms is keyed by pkgs.pkgsCross platform name, the same keys
shell.crossPlatforms and projectCross use. An entry is additive: a
platform without one uses the compiler above the table. The fields an entry
leaves unset are resolved from its own package, not inherited.
Describe such a compiler once. The modules under nix-haskell-compilers
are ready-made entries, imported like the patch modules:
{ nix-haskell-compilers, ... }:
{
imports = [
(import "${nix-haskell-compilers}/ghc-wasm-meta" {
flavour = "9.12";
version = "9.12.4.20260731";
})
];
}Tweaks for any package in the final package set, keyed by cabal package name. Entries for packages that do not exist are silently ignored:
packages = {
splitmix.patches = [ ./splitmix-js.patch ];
reflex-dom-core.doCheck = false;
my-app.flags.production = true;
my-app.ghcOptions = [ "-Werror" ];
};Fields: flags, patches, ghcOptions, configureFlags,
setupBuildFlags, setupHaddockFlags, doCheck, doHaddock, doCoverage,
doHoogle, doHyperlinkSource, doQuickjump, dontStrip,
enableDeadCodeElimination, enableLibraryProfiling, enableProfiling,
profilingDetail, enableShared, enableStatic,
enableSeparateDataOutput, enableLibraryForGhci, hardeningDisable,
src, the phase hooks preUnpack, postUnpack, prePatch,
postPatch, preConfigure, postConfigure, preBuild, postBuild,
preCheck, postCheck, preHaddock, postHaddock, preInstall,
postInstall, and the bundle optimizer settings wasm-opt,
closure-compiler and components.exes.<exe>.{wasm-opt,closure-compiler}.
The hooks diverge in one way: haskell.nix runs them for each component derivation of the package, nixpkgs once in the single package derivation.
The same fields can be given for one cross platform only, keyed by
pkgs.pkgsCross platform name, and are merged over the project-wide ones:
platforms.wasi32.packages.reflex-dom.flags.use-warp = false;This is how a platform conditional in a cabal file or project file is expressed for the nixpkgs driver, which has no solver to follow one. The flags in particular decide a package's dependencies, not merely its configuration. They take effect where the package's expression is generated, not on a package already built. The haskell.nix driver follows such conditionals itself, and applies an entry given here in the project whose target is that platform.
A driver gives back a cross build as the artifact it linked: a .wasm
binary, or a .jsexe directory. Three read-only options turn that into
what gets shipped:
| Option | Takes | Gives |
|---|---|---|
wasm-optimize |
{ platform, package, exe, wasm } |
the binary through wasm-opt, then stripped of its custom sections |
wasm-jsffi |
{ ghc, wasm } |
the ghc_wasm_jsffi.js a GHC-built wasm module cannot be instantiated without |
js-optimize |
{ platform, package, exe, jsexe } |
the directory with its all.js closure-compiled |
let exe = project.projectCross.wasi32.hsPkgs.frontend.components.exes.frontend;
wasm = "${exe}/bin/frontend.wasm";
named = { platform = "wasi32"; package = "frontend"; exe = "frontend"; };
in pkgs.runCommand "frontend.wasm-bundle" {} ''
mkdir -p $out
cp ${config.wasm-jsffi {
ghc = config.haskell-nix.cross-compiler "wasi32";
inherit wasm;
}} $out/ghc_wasm_jsffi.js
cp ${config.wasm-optimize (named // { inherit wasm; })} $out/frontend.wasm
''Run wasm-jsffi on the binary as linked, since wasm-optimize strips the
sections it reads. Its compiler has to be the one that built the binary.
Each driver's cross-compiler names that compiler, by pkgs.pkgsCross
platform name.
Naming an executable gets it a bundle without calling anything. For the
haskell.nix driver, it also installs the executable's .jsexe: that driver
installs only the bundled bin/<exe> for a javascript target, and
closure-compiler needs the directory the linker leaves beside it. The
nixpkgs builder copies that directory out on its own.
platforms.wasi32.packages.frontend.components.exes.frontend = {};
platforms.ghcjs.packages.frontend.components.exes.frontend = {};The result is then on the tree. Read it through a driver, the only thing that knows what it built:
let onTarget = config."haskell-nix".platforms.wasi32.packages.frontend;
in {
wasm = onTarget.components.exes.frontend.bundles.optimized;
jsffi = onTarget.components.exes.frontend.bundles.jsffi;
everyExe = onTarget.bundles; # the same, keyed by executable name
}optimized is the executable through that target's optimizer. jsffi is
the ghc_wasm_jsffi.js a wasm binary needs, and null for every other
target. Read anywhere but through a driver, as config.platforms.…, both
are null: there is no project to ask what it built. Neither is read-only.
A project with something else to ship can define either.
cross-exe is what each driver builds an executable into. The bundles
optimize it, and anything else that wants a cross build by name can use it.
The flags come from wasm-opt and closure-compiler, and five layers can
state them. The names a transform is given are only lookup keys for the
settings, and any of them can be left out:
# whatever the target, for everything
wasm-opt.level = "2";
# whatever the target, for one package and then one executable of it
packages.frontend.wasm-opt.extraFlags = [ "--converge" ];
packages.frontend.components.exes.frontend.wasm-opt.level = "z";
# for one target, and the same two layers under it
platforms.wasi32.wasm-opt.level = "z";
platforms.wasi32.packages.frontend.wasm-opt.extraFlags = [ "--low-memory-unused" ];
platforms.wasi32.packages.frontend.components.exes.frontend.wasm-opt.enable = false;The most specific layer that states a field decides it. null states
nothing, and only wasm-opt and closure-compiler themselves hold every
field. The order, from most specific to least:
- an executable of a package on one target
- that package on that target
- that target
- the same executable, then the same package, whatever the target
- the tool's own settings
enable = false copies the input through instead, so the caller installs
the same path either way.
source-repository-packages accepts either a path or an attrset with src,
optional subdir and optional condition:
source-repository-packages = {
reflex-dom = {
src = ./deps/reflex-dom;
subdir = [ "reflex-dom" "reflex-dom-core" ];
};
obelisk-backend = {
src = deps.obelisk + "/lib/backend";
condition = "!arch(javascript)";
};
};Make custom packages visible to dependency resolution:
hackage-overlays = [
{
name = "my-package";
version = "0.1.0";
src = pkgs.fetchFromGitHub { ... };
}
];shell = {
crossPlatforms = ps: with ps; [ ghcjs wasi32 ];
packages = ps: with ps; [ common frontend "backend" ];
tools = { cabal = "latest"; };
buildInputs = [ pkgs.postgresql ];
shellHook = "echo hello";
withHoogle = false;
};crossPlatforms selects over pkgs.pkgsCross platform names. When a GHCJS
or wasm target is selected, Node.js is added to buildInputs.
Sandstone (pins/sandstone) makes one content-addressed derivation for each
module of a package, through Nix dynamic derivations. The package's own
build then restores those artifacts and only links, so a change to one
module rebuilds one module. The option is common, and both drivers honor
it:
fine-grained.enable = true;fine-grained.packages names the packages built this way. null, the
default, takes every local package, and [] takes none. Like every common
option, a field set under nixpkgs.fine-grained or
haskell-nix.fine-grained overrides it for that driver only. Cross
platforms are never built this way: a plan runs the Setup of the build
platform, and its modules compile where the build runs.
The feature is experimental, and off by default, because it needs more than a stock Nix:
- Evaluation reads
builtins.outputOf, so the evaluator needs thedynamic-derivationsexperimental feature. - A plan is a text-hashed content-addressed derivation, so the store needs
ca-derivationsanddynamic-derivations. A daemon decides that for itself, and a client flag does not override it. - A plan's builder speaks
builder-rpc-v0, a system feature that only the Nix sandstone pins carries.
fine-grained.nix is that Nix, and fine-grained.run wraps it as
bin/fine-grained-nix; the example exposes both:
$ nix-build examples/fine-grained -A run
$ ./result/bin/fine-grained-nix build -f examples/fine-grained library-nixpkgs
$ ./result/bin/fine-grained-nix build -f examples/fine-grained library-haskell-nixThe wrapper drives a store of its own, which is what lets it work while the
daemon carries none of the features. NIX_DYNAMIC_DRV_STORE names that
store. Unset, it is .nix/store under the project root, the first
directory upward from the working directory that holds .nix,
cabal.project, or .git. The machine's own store fills it, so a path
built here is copied rather than downloaded.
A plan has to configure the package the way the package's own build
configures it. Otherwise ghc turns the modules down and compiles them
again, which costs time and never correctness. Each driver's
fine-grained.configure-flags step reproduces its own builder's flags: the
nixpkgs one from the packages.<name> fields, the haskell.nix one from the
library component's config. Neither can read an argument set through
nixpkgs.options.package-arguments or nixpkgs.options.overrides, so
replace the step where one of them changes a build way.
GHC reports the mismatch. Build with -L and read the package's own log: a
module it takes from the plan is not listed at all, and a module it turns
down reads Compiling <module> ... [Flags changed]. Every module listed
that way is work the plan did twice.
Profiling is the usual case of that. Nixpkgs builds profiling libraries by default, Cabal compiles that way after the way a plan captures, and the package's own build then compiles every module a second time. haskell.nix leaves profiling off by default. The drivers warn, and the remedy is per package:
packages.<name>.enableLibraryProfiling = false;Documentation costs the same under the nixpkgs driver, for another reason. Haddock reads sources rather than compiled modules, so it reads every module again whatever a plan holds. Turn it off per package where that matters. The haskell.nix driver builds haddock as a separate derivation, which changes nothing for its component builds.
packages.<name>.doHaddock = false;Under the haskell.nix driver the selection reads the cabal plan, so a stack
project selects packages explicitly or not at all. The restore lands on the
library component's preBuild and replaces any other definition of it,
except the package-level hook, which it re-includes. A package whose cabal
file hpack generates is skipped with a warning, unless the selection names
it. A plan fails loudly where configure needs more than the tree, a
pkgconfig-depends probe or a backpack instantiation among them. A module
that reads a file at compile time, through embedFile say, works when the
cabal file states the file under extra-source-files or data-files;
those ride into every module's derivation. An unstated file fails its
module loudly.
A build can also resume from the tree of an earlier one, without sandstone:
packages.<name>.previousIntermediates takes a path carrying
share/haskell/<ghc-version>/<pname>-<version>/dist/build, and the build
restores it before Setup build. The nixpkgs driver restores the whole
package's tree, and the haskell.nix driver the library component's. A
fine-grained plan replaces the value for the packages it selects.
(nix-haskell ./project.nix).haskell-nix.projectThe project is haskell.nix's: hsPkgs.<pkg>.components.exes.<exe>,
projectCross.<platform>, shell, etc.
Driver configuration:
| Option | Description |
|---|---|
haskell-nix.options.* |
Any haskell.nix project option (index-state, cabalProjectFreeze, extra-hackages, pkg-def-extras, shell.exactDeps, shell.withHaddock, ...) |
haskell-nix.overrides |
haskell.nix modules to add to the project (lists concatenate when composed) |
haskell-nix.extraSrcFiles |
Extra files for the strictly tracked component builds |
haskell-nix.compiler-version |
The version of the compiler this driver builds with |
haskell-nix.cross-compiler |
platform to the compiler this driver builds that target with |
haskell-nix.cross-exe |
{ platform, package, exe } to what this driver builds that executable into |
haskell-nix.overrides = [
({ pkgs, lib, ... }: {
packages.obelisk-command.components.library.build-tools = with pkgs; [ ghcid ];
packages.reflex-dom-core.components.tests.gc.buildable = lib.mkForce false;
})
];(nix-haskell ./project.nix).nixpkgs.projectThe project:
{
packages # The project's own packages (packages.<name>)
haskellPackages # The full extended package set
shell # shellFor development shell
projectCross # Per pkgsCross platform (best effort)
ghcWithPackages
}haskell.nix's hsPkgs.<name>.components.exes.<exe> corresponds to
packages.<name> here, with the executable at $out/bin/<exe>.
Driver configuration:
| Option | Description |
|---|---|
nixpkgs.compiler.name |
Per-driver override of the compiler, when the project's has no nixpkgs equivalent |
nixpkgs.compiler-version |
The version of the compiler this driver builds with |
nixpkgs.cross-compiler |
platform to the compiler this driver builds that target with |
nixpkgs.cross-exe |
{ platform, package, exe } to what this driver builds that executable into |
nixpkgs.pkgsCross |
Cross package sets for projectCross, replacing the ones from pkgs.pkgsCross |
nixpkgs.options.exact-configuration |
Tell Cabal every dependency and flag, so it resolves nothing and reads no version bound (follows use-plan) |
nixpkgs.options.overrides |
Overlays over the package set, applied last |
nixpkgs.options.packages |
Explicit local package map (bypasses discovery) |
nixpkgs.options.use-plan |
Take the project structure from the cabal plan of the haskell.nix driver |
nixpkgs.options.extra-package-defaults |
Jailbreak/check/haddock defaults for fetched packages |
nixpkgs.options.cross-package-defaults |
Jailbreak/haddock/profiling defaults for a cross set the driver builds itself |
nixpkgs.options.tool-packages |
Overrides for shell.tools resolution, cabal among them by default |
nixpkgs.options.shellFor-args |
Extra shellFor arguments |
Local packages are the package at the root of src by default.
source-repository-package stanzas in the project text (the project file
or cabalProject, plus cabalProjectLocal and extraCabalProject) are
parsed with haskell.nix's parser and honored: sources resolve through
inputMap, then fetchgit with hashes from --sha256 comments or
sha256map. For multi-package projects either list the packages
explicitly:
nixpkgs.options.packages = {
common.subdir = "common";
frontend.subdir = "frontend";
};or set nixpkgs.options.use-plan = true to reuse cabal's own reading of
cabal.project (exact globs, optional-packages, conditionals) at the cost
of evaluating the haskell.nix toolchain.
This driver has no solver. It enforces a version bound written before the
compiler in hand instead of reasoning about it, and allow-newer in a
cabal.project means nothing to it. jailbreak lifts the bounds a cabal
file states outright, but cannot reach a bound inside a conditional stanza.
nixpkgs.options.exact-configuration removes the problem: Cabal is told
every direct dependency, by the id its package database records, and every
flag the package declares. Cabal then resolves nothing and reads no bound.
The haskell.nix driver configures every package this way, so allow-newer
takes effect there.
nixpkgs.options.exact-configuration =
lib.versionAtLeast config.nixpkgs.compiler-version "9.14";Caveats, by construction of nixpkgs' Haskell infrastructure:
- No version solving: dependency versions are those of the nixpkgs pin.
index-stateandcabalProjectFreezedo not exist here. Only thesource-repository-packagestanzas of the project text are interpreted. Arch-conditionalpackageflag stanzas are not. Flags that differ per driver go into the mirrorednixpkgs.packages.<name>.flags. - Test suites run inside the package build. Disable them per package with
packages.<name>.doCheck = false. ghcOptionsapplies to the project's own packages only, so the binary cache stays valid for the dependency closure.shell.toolsversions are not solvable. Tools resolve by name frompkgsand the package set.- Cross-compilation mirrors
pkgs.pkgsCross, which supports far fewer targets than haskell.nix. Acompiler.platformsentry carrying a toolchain gets a package set built with that toolchain instead. A target nixpkgs cannot assemble a working set for needs exactly that. A toolchain on the compiler above the table is not honored here, since only a cross set can be given one.
The nixpkgs driver reads the project, builds a package set from it, and
assembles a shell. Every step of that is an option under nixpkgs.options
whose default is what the driver would do anyway. Assigning one replaces
that step and leaves the rest alone:
nixpkgs.options = {
# reach a cabal2nix flag the driver never emits
cabal2nix-options = args:
"--jailbreak " + import "${nix-haskell-libs}/nixpkgs/cabal2nix-options.nix" { inherit lib; } args;
# add to the shell's inputs instead of replacing them, which is all
# `shellFor-args` can do
shell-arguments = { args }: args // {
nativeBuildInputs = args.nativeBuildInputs ++ [ pkgs.sqlite ];
};
};The steps, in the order the driver runs them:
- reading the project:
discover-packages,project-text,evaluate-condition,fetch-stanza-source - building the package set:
haskell-packages-for,cabal2nix-options,package-steps,exact-configuration-hook,project-overlays - the shell:
resolve-shell-tool,cross-ghc-env,shell-arguments
The haskell.nix driver generates three things before handing the project to haskell.nix, and each is an option as well:
haskell-nix.stages.src: the source with the generatedcabal.projectlines appendedhaskell-nix.stages.source-repository-packages: the stanzas and their pinshaskell-nix.stages.hackage: the index that makeshackage-overlaysvisible to the solver
The manual carries each step's call shape, its default, and an example.
The two targets this library ships, wasm and GHCJS, are rows handed to a factory. A project adds one of its own the same way, by importing that factory with a row of its own:
{ nix-haskell-libs, pkgs, lib, ... }: {
imports = [
(import "${nix-haskell-libs}/cross/target-module.nix" {
name = "android";
flag = "isAndroid";
matches = target: target.isAndroid;
selected = names: builtins.elem "aarch64-android" names;
selectedText = "whether `shell.crossPlatforms` selects `aarch64-android`";
target = "Android";
node = false;
optimizer = "android-strip";
optimize = "android-optimize";
artifact = "binary";
extension = "";
examplePlatform = "aarch64-android";
lead = "The built executable with its symbols stripped.";
optimizer-fields.enable = {
type = lib.types.bool;
default = true;
description = "Whether `android-optimize` strips the binary.";
};
optimize-defaultText = lib.literalMD "`strip` on the built executable";
mkOptimize = { pkgs, lib, settings }:
{ platform ? null, package ? null, exe ? null, binary }:
let stated = settings { inherit platform package exe; };
in pkgs.runCommand "android-optimized" {} (
if stated.enable
then "${pkgs.binutils}/bin/strip -o $out ${binary}"
else "cp ${binary} $out");
})
];
}That import declares three options of the project's own (isAndroid,
android-strip and android-optimize) and registers the row.
platforms.<platform>.packages.<name>.bundles.<exe>.optimized then
dispatches to it for every platform the row matches.
<nix-haskell>/libs/cross/targets.nix documents every field a row carries.
Four things a target of a project's own does not get:
- no
translationentries: those tables are this library's contract with its own common options - no per-driver mirror:
nixpkgs.isAndroiddoes not exist, and neither doesnixpkgs.isWasm - no new
bundlesfields: those are option names, and an option name cannot come from configuration, sojsffistays wasm-only - no
.jsexe-style install step on the haskell.nix driver: a target that needs one adds it throughhaskell-nix.overrides
Settings for the target's optimizer are stated at the top level, and per
platform, package or executable through bundle-optimizers:
android-strip.enable = false;
platforms.aarch64-android.bundle-optimizers.android-strip.enable = true;Every driver declares a translation table: one entry per common option,
recording how it is honored. nix flake check verifies:
translation-totality: the table keys of every driver equal the set of user-settable common options, in both directions. Adding a common option without teaching every driver about it fails evaluation.every-option-<driver>: a fixture setting every common option instantiates through the driver's whole translation.hello-<driver>: the hello example builds with each driver.
The haskell.nix checks want the IOG binary cache (configured in the flake's
nixConfig; pass --accept-flake-config if it is not in your nix.conf).
release.nix gathers those checks together with the reflex-todomvc example
built for every driver, compiler and cross target it is meant to work for.
Each combination builds twice: as the drivers build it, and as a person
would inside the project's shell with the cross target's own cabal. It is a
tree, reached as legacyPackages.<system>.release through the flake:
nix-build release.nix -A checks
nix-build release.nix -A reflex-todomvc.build.haskell-nix.ghc912.wasi32
nix-build release.nix -A all # everything, one symlink treeThe matrix stays out of nix flake check, which builds only the repo's own
checks and stays quick.
Dependencies live under inputs, one entry per pin in pins/. An entry
accepts whatever a flake input can be: a flake input, a store path, a checkout,
or a packed thunk.
{
inputs.haskell-nix = ./dep/your-haskell-nix;
inputs.nixpkgs = inputs.nixpkgs; # a flake input
}The pins in pins/ supply nixpkgs, haskell-nix, ghc-wasm-meta and
sandstone. Add entries of your own freely. They resolve the same way.
Flake inputs arrive without any wiring, so inputs.nixpkgs follows the
consuming flake's nixpkgs. Precedence runs
pins/ < flake inputs < whatever you set explicitly.
The result attrset and some option spellings changed when the nixpkgs driver was introduced:
| Old | New |
|---|---|
(nix-haskell m).nixpkgs (the package set) |
(nix-haskell m).pkgs |
overrides |
haskell-nix.overrides |
extraSrcFiles |
haskell-nix.extraSrcFiles |
cabalProjectFreeze, index-state |
haskell-nix.options.<same> |
extra-hackages, extra-hackage-tarballs, pkg-def-extras |
haskell-nix.options.<same> |
shell.withHaddock, shell.exactDeps, shell.allToolDeps, ... |
haskell-nix.options.shell.<same> |
overrides = [ { ghcOptions = [...]; } ] |
ghcOptions = [...] |
overrides = [ { packages.<n>.patches = [...]; } ] |
packages.<n>.patches = [...] |
compiler-nix-name = "ghc912" |
compiler.name = "ghc912" |
compiler = <package> |
compiler.package = <package> |
compiler = { <platform> = ...; } |
compiler.platforms.<platform> = { ... } |
{ config, nix-haskell-patches, ... }:
{
imports = [
(import "${nix-haskell-patches}/js/splitmix" { drivers = [ "haskell-nix" ]; })
];
name = "reflex-todomvc";
src = ./.;
source-repository-packages = {
reflex-dom = {
src = ./deps/reflex-dom;
subdir = [ "reflex-dom" "reflex-dom-core" ];
};
};
nixpkgs = {
# webkitgtk (via jsaddle-webkit2gtk) still links libsoup 2
pkgs = import config.inputs.nixpkgs {
inherit (config) system;
config.permittedInsecurePackages = [ "libsoup-2.74.3" ];
};
options.overrides = [
# A test dependency of reflex-dom-core that lives in the reflex-dom
# repository. It is never built, since checks are off for fetched
# packages.
(_: _: { chrome-test-utils = null; })
];
};
haskell-nix.extraSrcFiles = {
library.extraSrcFiles = [ "static/style.css" ];
exes.reflex-todomvc.extraSrcFiles = [ "static/style.css" ];
};
haskell-nix.options.shell.withHaddock = false;
shell = {
crossPlatforms = ps: with ps; [ ghcjs wasi32 ];
packages = ps: with ps; [ reflex-todomvc ];
withHoogle = false;
};
}nix run --no-write-lock-file github:reflex-frp/nix-haskell#manual-view
P.S. The name is nothing clever: just the generic
{tool}-{lang}pattern (nix-haskell, nix-rust, ...). The resemblance to haskell.nix, one of the drivers here, is a coincidence of convention, not imitation.