With a few lines you can setup your Haskell programming environment (compilers, packages, databases, browsers, and other tools from different ecosystems) with having only the Nix Package Manager installed.
You can check this environment into source control, share it with colleagues and bring them up to speed in seconds. No manually installing apps and—if gone all the way—no “It works on my machine!” due to different package versions.
More: How to install Nix
Nix installs the terminal app nix-shell
which creates an environment. In this tutorial we will see how to use it by slowly progressing from simple to intermediate usage.
tldr: Go to final usage example
Try Out Haskell Tools In Nix-Shell
Once the Nix Package Manager is installed, you need to have or create a project folder:
~$ mkdir myproject && cd myproject
In order to compile Haskell files, you need a Haskell compiler like Ghc and preferably a build tool like Cabal.
To get those tools with Nix run:
~/myproject$ nix-shell -p cabal-install ghc
Element | Note |
---|---|
~/myproject
|
The folder your are supposed to be in. |
$
|
Interactive shell prompt symbol. It indicates that you can/should run the text on the right of it in your terminal. |
nix-shell
|
Helper terminal app from the Nix package manager. It creates temporary shell environements. Official Wiki |
-p
|
Flag for nix-shell . Short for --packages . It makes the following Nix packages (=usually apps) available in the resulting shell environment.
|
cabal-install
|
Nix package name for the Haskell build tool (terminal app) cabal . See the official website.
|
ghc
|
Nix package name for the Haskell compiler (terminal app with the same name) ghc that is used by Cabal to compile. See the official website.
|
This will download some necessary files and place you in a new shell environment—indicated by changing the shell prompt to [nix-shell:~/myproject]$
—where the terminal apps ghc
and cabal
are available:
[nix-shell:~/myproject]$ cabal --version
cabal-install version 3.0.0.0
Isn’t that convenient? The best thing about nix-shell
is that you can make any app in any version available that exists in the Nix Package Repository (e.g. Git and Visual Studio).
Now you can create a basic project folder structure with:
[nix-shell:~/myproject]$ cabal init
You can play around with the Main.hs
Haskell file and the project definition file myproject.cabal
. You can download any required Haskell package from that definition file by running cabal update
, interactively test your app withcabal new-repl
, build it with cabal new-build
, and run it with the cabal new-run myproject
command. These commands work, because Cabal can call the ghc
compiler app, which is now also available in the shell environment.
More: A short intro to Cabal (Prefer the newer cabal new-*
commands)
Another huge benefit of nix-shell
is that it installs Ghc and Cabal locally. It doesn’t modify or pollute the rest of your system. If you run
[nix-shell:~/myproject]$ exit
you will be back in your normal shell environment where Ghc and Cabal don’t exist:
~/myproject$ cabal
cabal: command not found
Think about it: If you work on multiple project that use different versions of Ghc, packages or any other app, these versions will never come into conflict, because they only exist locally in separate environments.
Getting Started On a Haskell Project with a shell.nix file
In most projects you will need more than just a few tools. Remembering what they are and typing them out by hand into a nix-shell -p ...
quickly becomes unusable. For that you can specify all your apps, tools and dependencies in a shell.nix
file in your project root folder:
# minimal ~/myproject/shell.nix file
let
myNixPkgs = import <nixpkgs> {};
in
myNixPkgs.mkShell {
nativeBuildInputs = with myNixPkgs; [
app cabal
cabal-install # terminal ghc # Haskell compiler
];
}
Element | Note |
---|---|
let .. in ..
|
Define variables in the let-part that you want to use in the in-part. |
myNixPkgs
|
Custom variable for our extended collection with all available Nix packages (more than 60000). Think of it as a huge Json object { cabal-install:.., ghc:.., git:.. } of packages.
|
import
|
Load the content of the file represented by the next argument. |
<nixpkgs>
|
Short Nix notation to a file from the Nix package manager that holds all Nix packages. |
{}
|
Empty argument to <nixpkgs> , which is actually not a list, but a function that returns a list when you give an argument.
|
import <nixpkgs> {}
|
Read it as (import <nixpkgs>) {} , which first loads the function (import <nixpkgs>) that will give you a package list when you give it an argument. And then you give it an argument {} to actually get the package list.
|
mkShell
|
A helper function from myNixPkgs to create a shell environment. So actually, myNixPkgs not only contains packages but also helper functions.
|
nativeBuildInputs
|
List of Nix packages that you should be able to call from the terminal in the shell environment. There are many other things you can configure. |
with myNixPkgs; [ghc git]
|
Short Nix notation for [myNixPkgs.ghc myNixPkgs.git] for readability.
|
[ghc git]
|
A list in the Nix language. Nix doesn’t use commas in between! |
Of course you can add other apps and dev tools to your environment:
# minimal ~/myproject/shell.nix file
...
nativeBuildInputs = with myNixPkgs; [
tools
# General dev git
ripgrep # Fast text search alternative to grep. Terminal command: "rg"
vscode # Visual Studio Code. Terminal command: "code"
# Haskell dev tools
cabal-install # terminal command: "cabal"
ghc # Haskell compiler
haskellPackages.ghcid # Haskell Code checker
];
...
These apps and tools will be available after running nix-shell
.
This file is written in the Nix (programming) language and defines the same shell environment (where Cabal and Ghc exist) as the nix-shell -p cabal-install ghc
command . You can get into this environment with:
~/myproject$ nix-shell
If you don’t pass any arguments to nix-shell
, it will automatically load the environment from the shell.nix
file if it exists.
This setup works for most Haskell packages but not all.
Some popular Haskell packages like digest
for cryptographically strong hashing or JuicyPixels
for image manipulation require some external C libraries like zlib.h
(for fast compression algorithms) that our currently loaded Ghc doesn’t know about. So, if you mention those packages as dependencies in your myproject.cabal
file, you will get a compile error that a library is missing:
[nix-shell:~/myproject]$ cabal new-build
...
cabal: Missing dependency on a foreign library:
* Missing (or bad) header file: zlib.h
* Missing (or bad) C library: z
...
Usually, you would now have to install those libraries by hand. To learn how to avoid any manual work by letting Nix manage those Haskell packages dependencies, read on.
Currently, only Cabal and Ghc are managed by Nix. Haskell packages are managed by Cabal, which is bad for reproducibility.
When something is managed by Nix, Nix decides where to download from, what version to use and where to store them on your system to not to interfer with the rest of your system. However, when you call cabal update
, Cabal decides where to download packages from, which version to use and where to install them. Nix is very good at creating independent environments and reproducibility: If done right, Nix can be configured to recreate an bit-by-bit identical environment for the same shell.nix
file. Cabal is less capable. For example, it currently installs packages “globally” into a folder like ~/.cabal/packages
.
Let Nix manage Haskell packages for Cabal
You can let Nix manage not only your tools like Ghc and Cabal, but also dependencies like Haskell packages. Nix will also take care to download and provide any external libraries that a package may need. So you don’t need to do that manually.
If you have followed along, you should already have a myproject.cabal
file in your project root folder. Then you can just replace your shell.nix
with the following:
# extended "~/myproject/shell.nix" file
let
myNixPkgs = import <nixpkgs> {
overlays = [myNixPkgsOverlay];
};
myNixPkgsOverlay = (nixSelf: nixSuper: {
myHaskellPackages = nixSelf.haskellPackages.override (oldHaskellPkgs: {
overrides = nixSelf.lib.composeExtensions (oldHaskellPkgs.overrides or (_: _: {})) myHaskellPkgsOverlay;
});
});
myHaskellPkgsOverlay = (hSelf: hSuper: {
# "myproject" is the first part of the "myproject.cabal" project definition file
myProject = hSelf.callCabal2nix "myproject" ./. {};
});
in
myNixPkgs.myHaskellPackages.myProject.env
Element | Note |
---|---|
let .. in ..
|
Define variables in the let-part that you want to use in the in-part. |
myNixPkgs
|
Custom variable for our extended collection with all available Nix packages (more than 60000). Think of it as a huge Json object { myHaskellPackages:.., ghc:.., git:.. } of packages now containing our extended Haskell package list.
|
import
|
Load the content of the file represented by the next argument. |
<nixpkgs>
|
Short Nix notation to a file from the Nix package manager that holds all Nix packages. |
{ overlays = ...}
|
Overlay argument to <nixpkgs> to modify/extend it. <nixpkgs> is actually not a collection, but a function that returns a collection when you give it an argument.
|
import <nixpkgs> { ...}
|
Read it as (import <nixpkgs>) {} , which first loads the function (import <nixpkgs>) that will give you a package collection when you give it an argument. And then you give it an argument {...} to actually get the collection.
|
myNixPkgsOverlay
|
Overlay function to add a new Nix package myHaskellPackages to the already huge collection of Nix packages.
|
myHaskellPackages
|
The new package that we want to add to Nix packages. It is actually not a single package, but itself a collection of Nix Haskell packages. It is the default collection nixpkgs.haskellPackages plus our new Haskell package myProject , which we add again through an overlay on haskellPackages .
|
haskellPackages.override
|
A helper function to create a copy of haskellPackages with the desired modifications. Our modification is that we add our myProject Haskell package through the myHaskellPkgsOverlay overlay.
|
myHaskellPkgsOverlay
|
Overlay function to add a new Nix Haskell package myproject to the collection of Nix Haskell packages.
|
myproject
|
Our new Nix Haskell package. |
callCabal2nix
|
Helper function from nixpkgs.haskellPackges to create a Nix Haskell package from the myproject.cabal file and let Nix manage your Haskell dependencies instead of Cabal. It takes 3 arguments: The name of the Cabal file, where to look for it (./. is the current folder) and a collection of options (no options {} ).
|
myproject.env
|
Definition of a shell environment where all the Haskell dependencies of that package are available to the terminal. Haskell packages by convention have an env attribute (normal Nix packages don’t).
|
An interesting concept here is that of an overlay: It is a Nix pattern to sanely modify a package list where the packages can have each other as dependencies. By convention it is a function with two arguments: a self
and a super
which both are lists of Nix packages (so there is e.g. self.ghc
and super.ghc
), and returns a set of packages that you want to add or modify. Rule of thumb: If you want to use Ghc as a dependency or create a new Ghc variant package, use self.ghc
. If you want to modify Ghc itself, use super.ghc
as in { ghc = super.ghc.override... }
.
The important line that tells Nix to manage your Haskell packages is:
myProject = hSelf.callCabal2nix "myproject" ./. {};
It creates a new Haskell package myproject
that requires those packages as dependencies which you mention in your myproject.cabal
file. By convention Haskell packages (and those created with callCabal2nix
) have an attribute .env
which contains a description for a shell environment with all necessary dependencies to develop that package. This environment is what we load with the last line:
myNixPkgs.myHaskellPackages.myProject.env
This includes Ghc, but unfortuneately not Cabal, because Cabal is not strictly necessary to compile a package/project. You could do it with Ghc alone. To see how to add Cabal and other tools to this environment, read on.
If you haven’t created/initialized a project yet, you can run
~/myproject$ nix-shell -p cabal-install ghc --run "cabal init"
to create a myproject.cabal
file. This creates a temporary shell environment where Cabal and Ghc exist, runs the given command cabal init
inside it, and exits.
Due to the compactness of the Nix language we could have reduced the whole extended shell.nix
file to a single line:
((import <nixpkgs> {}).haskellPackages.callCabal2nix "myproject" ./. {}).env
However, this is hard to read and hard to extend with other customizations.
Final Bootstrable shell.nix File
A convenient shell.nix
to start with is the following:
# full "~/myproject/shell.nix" file
let
myNixPkgs = import <nixpkgs> {
overlays = [myNixPkgsOverlay];
};
myNixPkgsOverlay = (nixSelf: nixSuper: {
myHaskellPackages = nixSelf.haskellPackages.override (oldHaskellPkgs: {
overrides = nixSelf.lib.composeExtensions (oldHaskellPkgs.overrides or (_: _: {})) myHaskellPkgsOverlay;
});
});
myHaskellPkgsOverlay = (hSelf: hSuper: {
# "myproject" is the first part of the "myproject.cabal" project definition file
myProject = hSelf.callCabal2nix "myproject" ./. {};
});
myDevTools = with myNixPkgs; [
cabal-install
haskellPackages.ghcid];
myShellHook = ''
alias repl="cabal new-repl"
'';
in
myNixPkgs.myHaskellPackages.myProject.env.overrideAttrs (oldEnv: {
nativeBuildInputs = oldEnv.nativeBuildInputs ++ myDevTools;
shellHook = myShellHook;
})
Element | Note |
---|---|
let .. in ..
|
Define variables in the let-part that you want to use in the in-part. |
myNixPkgs
|
Custom variable for our extended collection with all available Nix packages (more than 60000). Think of it as a huge Json object { myHaskellPackages:.., ghc:.., git:.. } of packages now containing our extended Haskell package list.
|
import
|
Load the content of the file represented by the next argument. |
<nixpkgs>
|
Short Nix notation to a file from the Nix package manager that holds all Nix packages. |
{ overlays = ...}
|
Overlay argument to <nixpkgs> to modify/extend it. <nixpkgs> is actually not a collection, but a function that returns a collection when you give it an argument.
|
import <nixpkgs> { ...}
|
Read it as (import <nixpkgs>) {} , which first loads the function (import <nixpkgs>) that will give you a package collection when you give it an argument. And then you give it an argument {...} to actually get the collection.
|
myNixPkgsOverlay
|
Overlay function to add a new Nix package myHaskellPackages to the already huge collection of Nix packages.
|
myHaskellPackages
|
The new package that we want to add to Nix packages. It is actually not a single package, but itself a collection of Nix Haskell packages. It is the default collection nixpkgs.haskellPackages plus our new Haskell package myProject , which we add again through an overlay on haskellPackages .
|
haskellPackages.override
|
A helper function to create a copy of haskellPackages with the desired modifications. Our modification is that we add our myProject Haskell package through the myHaskellPkgsOverlay overlay.
|
myHaskellPkgsOverlay
|
Overlay function to add a new Nix Haskell package myproject to the collection of Nix Haskell packages.
|
myproject
|
Our new Nix Haskell package. |
callCabal2nix
|
Helper function from nixpkgs.haskellPackges to create a Nix Haskell package from the myproject.cabal file and let Nix manage your Haskell dependencies instead of Cabal. It takes 3 arguments: The name of the Cabal file, where to look for it (./. is the current folder) and a collection of options (no options {} ).
|
myDevTools
|
List of Nix packages we want to also make available in the shell environment. |
with myNixPkgs; [ghc git]
|
Short Nix notation for [myNixPkgs.ghc myNixPkgs.git] for readability.
|
[ghc git]
|
A list in the Nix language. Nix doesn’t use commas in between! |
myShellHook
|
A shell script that is will be run in the shell environment once we enter it. |
'' alias ... ''
|
A multi-line string (two single quoutes'' ) in the Nix language. Here we write a shell alias so that we can writerepl instead of the long cabal new-repl command in the shell environment.
|
myproject.env
|
Definition of a shell environment where all the Haskell dependencies of that package are available to the terminal. Haskell packages by convention have an env attribute (normal Nix packages don’t).
|
env.overrideAttrs
|
Function (overrideAttrs means override attributes) to modify the (Json-like object) env . It takes another function (oldEnv: ..) as argument that produces a subset of those env attributes that shall be changed. In Nix all variables are immutable. SooverrideAttrs doesn’t modify env but creates a completely new environment object with the modifications.
|
nativeBuildInputs
|
List of Nix packages that you should be able to call from the terminal in the shell environment. We take the old attributes oldEnv.nativeBuildInputs and add our own list of tools myDevTool with ++ . There are many other things you can configure.
|
shellHook
|
A shell script that is will be run in the shell environment once we enter it. It is normally empty, but we let it run our myShellHook script.
|
An interesting concept here is that of an overlay: It is a Nix pattern to sanely modify a package list where the packages can have each other as dependencies. By convention it is a function with two arguments: a self
and a super
which both are lists of Nix packages (so there is e.g. self.ghc
and super.ghc
), and returns a set of packages that you want to add or modify. Rule of thumb: If you want to use Ghc as a dependency or create a new Ghc variant package, use self.ghc
. If you want to modify Ghc itself, use super.ghc
as in { ghc = super.ghc.override... }
.
Again, this shell.nix
files requires that you have myproject.cabal
file. If you don’t, you can create one with:
~/myproject$ nix-shell -p cabal-install ghc --run "cabal init"