This directory contains the simplest possible Haskell project and the simplest possible Nix derivation to build that project. You can think of a derivation as a language-agnostic recipe for how to build something (such as a Haskell package).
You can build this Haskell package by running:
$ nix-build release0.nix
these derivations will be built:
/nix/store/3l7f5v3aibz9rnxxafm6afvihjc04aiq-project0-1.0.0.drv
building path(s) ‘/nix/store/x28vx2rfnffl1clmxn5054bxwqyln2j0-project0-1.0.0’
...
Configuring project0-1.0.0...
Dependency base <5: using base-4.9.0.0
...
Building project0-1.0.0...
Preprocessing executable 'project0' for project0-1.0.0...
[1 of 1] Compiling Main ( Main.hs, dist/build/project0/project0-tmp/Main.dyn_o )
Linking dist/build/project0/project0 ...
...
Installing executable(s) in
/nix/store/x28vx2rfnffl1clmxn5054bxwqyln2j0-project0-1.0.0/bin
...
/nix/store/x28vx2rfnffl1clmxn5054bxwqyln2j0-project0-1.0.0
I've highlighted the parts that matter the most for our simple example. Don't expect the hashed paths in the above example output to necessarily match the ones you get (See below for more details about why they might differ).
The project0.cabal
file only specifies a single dependency of base < 5
and
Nix picks the latest base
package
below version 5 (in this case, base-4.9.0.0
)
to satisfy that dependency. Nix then builds the project, stores the build
output in /nix/store/x28vx2rfnffl1clmxn5054bxwqyln2j0-project0-1.0.0
and
creates a symlink in the current directory named result
pointing to that
directory in the /nix/store
:
$ readlink result
/nix/store/x28vx2rfnffl1clmxn5054bxwqyln2j0-project0-1.0.0
Right now the contents of that directory are just an executable file and an empty library directory:
$ tree result
result
├── bin
│ └── project0
└── lib
└── links
3 directories, 1 file
As the project grows more complex we'll see additional build outputs in this directory.
We can run the executable stored in the result/bin
subdirectory:
$ result/bin/project0
Hello, world!
This output makes sense since our Main.hs
file is just a simple
"Hello, world!" program:
module Main where
main :: IO ()
main = putStrLn "Hello, world!"
What happens if we try to build the project again?
$ nix-build release0.nix
these derivations will be built:
/nix/store/vvw0v8ys7dadck747vj48vb0jgs7isqm-project0-1.0.0.drv
building path(s) ‘/nix/store/ysrqcpdl51jaa4gqzx1xmb7m0h05rdwq-project0-1.0.0’
setupCompilerEnvironmentPhase
...
[1 of 1] Compiling Main ( Main.hs, dist/build/project0/project0-tmp/Main.dyn_o )
Linking dist/build/project0/project0 ...
...
/nix/store/ysrqcpdl51jaa4gqzx1xmb7m0h05rdwq-project0-1.0.0
This might seem odd at first since you'd expect Nix to reuse the cached result
from our first build. However, Nix does not reuse the first build because our
project subtly changed: our nix-build
deposited a result
symlink which was
not there before! Our Nix derivation depends on the current project directory,
so if anything in the current directory changes then Nix performs a complete
rebuild of our project.
We can verify this by removing the symlink and then performing the build again:
$ rm result
$ nix-build release0.nix
/nix/store/x28vx2rfnffl1clmxn5054bxwqyln2j0-project0-1.0.0
This time we get a cache hit and reuse the first build since our directory is
now bit-for-bit identical to when we first ran nix-build
.
These wasteful rebuilds are one reason that I don't recommend using
nix-build
to build the root Haskell project. Instead, the next section
describes how to use cabal
with Nix to avoid the issue of wasteful rebuilds.
If you ever need to bootstrap your own project using cabal init
,
then run:
$ nix-shell --packages ghc --run 'cabal init'
cabal init
requires ghc
to be on the executable search path, but we do not
plan to install GHC globally. Instead, we can use a nix-shell
to transiently
provide ghc
just for the duration of a cabal init
command. Later on we
rely on the project's Nix configuration to provide the desired GHC for
project development.
You can open up a development environment for this project inside of a "Nix shell" by running:
$ nix-shell --attr env release0.nix
Normally nix-shell
wouldn't require the --attr
flag since nix-shell
is
designed to automatically compute the necessary development environment from
the original derivation. However, Haskell derivations are different and
nix-shell
doesn't work out of the box on them.
Haskell derivations are records that include an env
field which nix-shell
can use to compute the correct development environment. In Nix a field is
called an "attribute" so we pass the --attr env
flag to specify that
nix-shell
should compute the development environment from the derivation
record's env
"attribute".
Once we open up the development environment we can use cabal
to build and run
the project0
executable:
$ cabal configure
Resolving dependencies...
Configuring project0-1.0.0...
$ cabal run project0
Preprocessing executable 'project0' for project0-1.0.0...
[1 of 1] Compiling Main ( Main.hs, dist/build/project0/project0-tmp/Main.o )
Linking dist/build/project0/project0 ...
Running project0...
Hello, world!
You might get the following warning if you are using cabal-install
version
2.*
:
Warning: The configure command is a part of the legacy v1 style of cabal
usage.
Please switch to using either the new project style and the new-configure
command or the legacy v1-configure alias as new-style projects will become the
default in the next version of cabal-install. Please file a bug if you cannot
replicate a working v1- use case with the new-style commands.
For more information, see: https://wiki.haskell.org/Cabal/NewBuild
If you get that warning, use the corresponding cabal v1-*
command (e.g.
cabal v1-build
) in the interim and see
this issue for details about
Cabal's "new-build" support for Nix.
Unlike Nix, cabal
will be smart and won't wastefully rebuild things that
haven't changed. This means we can safely re-run cabal
without rebuilding the
entire project from scratch:
$ cabal run project0
Up to date
Hello, world!
You can exit from the Nix shell using the exit
command or typing Ctrl-D
.
This Nix shell provides all necessary dependencies for your project and the
Haskell toolchain except for cabal
. For example, inside the Nix shell you
will see that you are using a ghc
provided by Nix, regardless of whether or
not you have a global ghc
installed:
$ which ghc # The exact hash in the path might differ
/nix/store/dg7ak1hvlj66vgn4fwvddnnr4pfncd04-ghc-8.0.1/bin/ghc
The cabal configure
step automatically picks up the ghc
tool-chain and
package database provisioned by Nix and uses them for all subsequent cabal
commands.
The nixpkgs
manual notes that if you only have Haskell dependencies you
can also just run the following command once:
$ nix-shell --attr env release0.nix --run 'cabal configure'
... and then run all the other cabal
commands without the Nix shell. However,
if you have non-Haskell dependencies then this won't work. When in doubt, just
get used to development inside of a Nix shell since that habit will translate
well to non-Haskell projects managed by Nix.
NOTE: I recommend using cabal
to build the root project during Haskell
package development, but subsequent examples will still use nix-build
to
keep the examples short.
The release0.nix
file specifies how to build the project using Nix:
let
pkgs = import <nixpkgs> { };
in
pkgs.haskellPackages.callPackage ./project0.nix { }
I don't recommend reusing the above derivation for your Haskell projects. There are several ways that we can improve upon this derivation that we'll address in later examples.
This derivation begins by importing nixpkgs
, which is a Nix channel. You can
find all the officially supported Nix channels here:
The default Nix installation will subscribe you some channel (i.e. release) of Nixpkgs (a package repository for Nix).
You can tell which release of Nixpkgs you are using by running this command:
$ nix-instantiate --eval --expr 'builtins.readFile <nixpkgs/.version>'
"18.03\n"
... and you can also obtain the exact git revision of the Nixpkgs repository that the channel was cut from using this command:
$ nix-instantiate --eval --expr 'builtins.readFile <nixpkgs/.git-revision>'
"411cc559c052feb6e20a01fc6d5fa63cba09ce9a"
You should probably use the default channel selected for you. If you are using a Linux operating system other than NixOS, you can safely change to a stable channel if you prefer by running:
$ nix-channel --add https://nixos.org/channels/nixos-18.09-small nixpkgs
$ nix-channel --update nixpkgs
... replacing 18.09
with whatever stable release version you wish to use.
However, you should be very careful about using a stable release on OS X because
the public binary cache only caches OS X build products for the unstable
channel. If you try to use a stable channel on OS X you run a very high risk of
compiling things from scratch (including ghc
).
Even "stable" channels are still not frozen. Stable channels are like major
releases and they periodically receive minor releases for security patches, bug
fixes, and new packages that successfully build. However, you have to
specifically opt in to channel updates by running
nix-channel --update nixpkgs
. If you do nothing then your channel will
remain frozen at whatever minor release you downloaded when you first installed
Nix. When you do upgrade your channel you can only upgrade to the latest minor
release.
Nix's channel mechanism works okay for personal or open source development, but does not work well in a corporate environment, since you can't easily ensure that every person or deployment is on the exact same minor release.
In a corporate environment, you can pin nixpkgs
to a specific git
revision
as illustrated in release1.nix
:
let
bootstrap = import <nixpkgs> { };
nixpkgs = builtins.fromJSON (builtins.readFile ./nixpkgs.json);
src = bootstrap.fetchFromGitHub {
owner = "NixOS";
repo = "nixpkgs";
inherit (nixpkgs) rev sha256;
};
pkgs = import src { };
in
pkgs.haskellPackages.callPackage ./project0.nix { }
... where nixpkgs.json
was generated using the nix-prefetch-git
tool:
$ nix-prefetch-git https://github.com/NixOS/nixpkgs.git 2c288548b93b657365c27a0132a43ba0080870cc > nixpkgs.json
$ cat nixpkgs.json
{
"url": "https://github.com/NixOS/nixpkgs.git",
"rev": "2c288548b93b657365c27a0132a43ba0080870cc",
"date": "2017-01-02T00:10:04+01:00",
"sha256": "1a365am90a1zy99k4qwddj8s3bdlyfisrsq4a3r00kghjcz89zld"
}
Replace 2c288548b93b657365c27a0132a43ba0080870cc
with the git
revision that
you want to pin nixpkgs
to. You can also omit the revision to pin to the
current master
.
However, if you choose to go this route then you will need to set up an internal Hydra server to build and cache your project.
Without an internal cache your developers will likely need to build these tools
from scratch whenever your pinned nixpkgs
drifts too far from the publicly
cached channels. ghc
in particular is very expensive to rebuild.
This guide does not (yet) cover how to set up an internal Hydra server for this
purpose, but may do so in a future draft. Until then, the remaining examples
will not use a pinned nixpkgs
for simplicity.
The second half of our release0.nix
derivation contains the instructions to
build our project:
let
pkgs = import <nixpkgs> { };
in
pkgs.haskellPackages.callPackage ./project0.nix { }
This references another file in this same project called project0.nix
. This
file was generated using cabal2nix
by running:
$ cabal2nix . > project0.nix
... and the generated project0.nix
file for this project is:
{ mkDerivation, base, lib }:
mkDerivation {
pname = "project0";
version = "1.0.0";
src = ./.;
isLibrary = false;
isExecutable = true;
executableHaskellDepends = [ base ];
license = lib.licenses.bsd3;
}
All that cabal2nix
does is translate our project0.cabal
file into a
corresponding Nix expression. For comparison, here is the original
project0.cabal
file that project0.nix
was generated from:
name: project0
version: 1.0.0
license: BSD3
license-file: LICENSE
cabal-version: >= 1.18
build-type: Simple
executable project0
build-depends: base < 5
main-is: Main.hs
default-language: Haskell2010
Any time you update a Haskell project's cabal
file you need to regenerate the
project0.nix
file using cabal2nix
.
release2.nix
illustrates the next improvement we can make to our project's
Nix derivation:
let
pkgs = import <nixpkgs> { };
in
{ project0 = pkgs.haskellPackages.callPackage ./project0.nix { };
}
The only difference is that now our file returns a "set" (the Nix term for a
dictionary) of derivations. This "set" only has one "attribute" (i.e. key)
named project0
whose value is the derivation to build our Haskell project.
The main motivation for this change is that Hydra (Nix's continuous integration server) requires that project build files are sets of derivations with one attribute per build product. If you try to build a naked derivation with Hydra you will get weird errors.
A second lesser reason for this change is that this makes it easy to add additional build products to your project. This comes in handy when you want to build and test dependencies or extra tools that you rely on.
You can still build the project using nix-build
either by specifying to build
all derivations in the set:
$ nix-build release2.nix
... or by specifying the attribute of the derivation you want to build using
the same --attr
flag we introduced before for nix-shell
:
$ nix-build --attr project0 release2.nix
This --attr
flag specifies that we only want to build the project0
field
of the record, and this flag comes in handy once the record has more than one
field.
You can also still open up a Nix shell, but you need to change the attribute you
pass on the command line from env
to project0.env
:
$ nix-shell --attr project0.env release2.nix
Like before, nix-shell
and nix-build
take slightly different attributes:
we specify the project0
attribute when using nix-build
and the
project0.env
attribute when using nix-shell
.
You can also avoid having to type this every time you initialize the project by
creating the following shell.nix
file:
(import ./release2.nix).project0.env
... replacing release2.nix
with the name of your project's derivation file.
Then you can just type:
$ nix-shell
... and that will automatically use the contents of shell.nix
Note that cabal2nix
provides a --shell
option to generate a shell.nix
file suitable for the current project. However, this does not play nice with
advanced dependency management (covered in the next section) so I do not
recommend this approach in general.
That concludes Nix workflow basics for Haskell development. The next section covers dependency management.