The nixpkgs
repository does a good job of including many Haskell packages already. Indeed, it almost seems like the lazy, functional Nix-based NixOS might have a soft spot for Haskell :). Much of hackage is available with just a few keystrokes, relieving cabal of its package-managing tasks.
However, nixpkgs
does not include all of hackage, and it’s actually not hard run into missing packages during development. For instance, not long ago I wanted to build a toy snap
app as a first project in NixOS. While snap
is available, snap-web-routes
is not.
We’ll explore adding hackage packages to your local nixpkgs
repo (and submitting them upstream) in this installment. In fact, we’re going to add snap-web-routes
to nixpkgs
.
Preparing git
If you followed my earlier installment in this series, you should have a local clone of the nixpkgs
repository. Since we plan to submit a pull request with our changes, we need to take a few steps to prepare git. If you are pretty seasoned with git, you can probably skip this section.
Add and Sync Upstream
We need to add the primary NixOS/nixpkgs
github repository to our remotes. This will enable us to pull in updates to the nix expressions easily.
#!bash
git remote add upstream https://github.com/NixOS/nixpkgs.git
git fetch upstream
git merge upstream/master
The last two lines are what we’ll execute whenever we want to pull in updates for our custom nix-search
and nix-install
calls.
Make a New Branch
Next we need to create a new branch for adding our new expression. This will isolate our updates when we submit a pull request; otherwise, any additional, unrelated updates we make to our local repository and push to origin
will get added to the pull request! I learned this the hard way.
Make a new branch and check it out using checkout
and -b
.
#!bash
git checkout -b add-haskell-snap-web-routes
Adding Expressions
Nixpkgs Expression Hierarchy
The nix expressions for all the available package are included in our local nixpkgs
repository. Each nix expression is specified using a path. If no file name is provided in the path, then the file name of default.nix
is assumed. For example, nixpkgs/
has a default.nix
, which has the following line
#!nix
import ./pkgs/top-level-all-packages.nix
The default.nix
expression imports all the expressions listed in pkgs/top-level/all-packages.nix
. This expression, in turn, imports expressions from other locations in the nixpkgs
repository. The one we are interested in is pkgs/top-level/haskell-packages.nix
. Here’s just one excerpt from the haskell-packages.nix
file.
#!nix
snap = callPackage ../development/libraries/haskell/snap/snap.nix{}
So, when we invoke nix-install haskellPackages.snap
, Nix calls the expression located at in the ../development/libraries/haskell/snap/snap.nix
file. Thus, to add a new packages to our repo, we need to
- Write a nix expression file and put it in a logical spot in the
nixpkgs
repository. - Put a line in
haskell-packages.nix
we can use to call the expression file.
A Nix Expression for a Cabal Package
To add snap-web-routes
to the hierarchy, we need to write a nix expression for it. What does a nix expression look like? Here’s the one for snap
given in the nix file mentioned above.
#!nix
{ cabal, aeson, attoparsec, cereal, clientsession, comonad
, configurator, directoryTree, dlist, errors, filepath, hashable
, heist, lens, logict, MonadCatchIOTransformers, mtl, mwcRandom
, pwstoreFast, regexPosix, snapCore, snapServer, stm, syb, text
, time, transformers, unorderedContainers, vector, vectorAlgorithms
, xmlhtml
}:
cabal.mkDerivation (self: {
pname = "snap";
version = "0.13.2.7";
sha256 = "1vw8c48rb1clahm1yw951si9dv9mk0gfldxvk3jd7rvsfzg97s4z";
isLibrary = true;
isExecutable = true;
buildDepends = [
aeson attoparsec cereal clientsession comonad configurator
directoryTree dlist errors filepath hashable heist lens logict
MonadCatchIOTransformers mtl mwcRandom pwstoreFast regexPosix
snapCore snapServer stm syb text time transformers
unorderedContainers vector vectorAlgorithms xmlhtml
];
jailbreak = true;
patchPhase = ''
sed -i -e 's|lens .*< 4.2|lens|' snap.cabal
'';
meta = {
homepage = "https://snapframework.com/";
description = "Top-level package for the Snap Web Framework";
license = self.stdenv.lib.licenses.bsd3;
platforms = self.ghc.meta.platforms;
};
})
Whoa, that’s a lot to take in. Let’s go through the main points.
Dependencies List
The big list at the top are the dependencies for the nix expression. When Nix runs this expression, it first ensure that all dependencies are available and placed on the path. Anything not in the dependencies list is removed from the path. If a necessary dependency is not specified in this list, Nix will refuse to run the expression.
One thing to note about these dependencies: they look like Haskell package names, but technically they are nix expressions in the same namespace as the snap.nix
expression. By convention, nix expressions for Hackage packages are named slightly differently.
Cabal.mkDerivation
The call to cabal.mkDerivation
is a function call defined elsewhere in the expression hierarchy, and each item specified within the curly braces is a named input argument to the function. The important items are mostly self-explanatory, but a couple could use some elaboration:
- sha256 – a hash of the source code used to build the package. Nix checks the hash for consistency before building the package.
- jailbreak – strips out the dependency version bounds from the cabal file before building.
Generating a Nix Expression with cabal2nix
Specifying a nix expression like the one for snap
from scratch would be a huge PITA. Thankfully, NixOS has a sweet utility function called cabal2nix
that essentially handles everything for us.
#!bash
nix-install haskellPackages.cabal2nix
First, make sure your hackages list is up to date.
#!bash
cabal update
Next, call cabal2nix
and specify the hackage package name.
#!bash
[dan@nixos:~/Code/nixpkgs]$ cabal2nix cabal://snap-web-routes
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 7161 0 0 12992 0 --:--:-- --:--:-- --:--:-- 12992
path is ‘/nix/store/wyilc14fjal3mbhw0269qsr5r84c5iva-snap-web-routes-0.5.0.0.tar.gz’
{ cabal, heist, mtl, snap, snapCore, text, webRoutes, xmlhtml }:
cabal.mkDerivation (self: {
pname = "snap-web-routes";
version = "0.5.0.0";
sha256 = "1ml0b759k2n9bd2x4akz4dfyk8ywnpgrdlcymng4vhjxbzngnniv";
buildDepends = [ heist mtl snap snapCore text webRoutes xmlhtml ];
meta = {
homepage = "https://github.com/lukerandall/snap-web-routes";
description = "Type safe URLs for Snap";
license = self.stdenv.lib.licenses.bsd3;
platforms = self.ghc.meta.platforms;
};
})
Boom. We get a complete nix expression without doing any work. Now we just need to put it in the right spot in the hierarchy. The rest of the hackage packages are in pkgs/development/libraries/haskell
, so we’ll follow suit.
#!bash
[dan@nixos:~/Code/nixpkgs]$ mkdir pkgs/development/libraries/haskell/snap-web-routes
[dan@nixos:~/Code/nixpkgs]$ cabal2nix cabal://snap-web-routes > pkgs/development/libraries/haskell/snap-web-routes/default.nix
Now we can check that our expression actually runs. The nix-install
function we added isn’t smart enough to handle --dry-run
, so we’ll use the nix-env
command directly.
#!bash
[dan@nixos:~/Code/nixpkgs]$ nix-env -iA haskellPackages.snapWebRoutes --dry-run
installing `haskell-snap-web-routes-ghc7.6.3-0.5.0.0'
these derivations will be built:
/nix/store/f42ybqmcmq44nr73l6ap90l5wnm3s4kq-haskell-snap-web-routes-ghc7.6.3-0.5.0.0.drv
these paths will be fetched (21.88 MiB download, 416.87 MiB unpacked):
/nix/store/0kw75f2qqx19vznsckw41wcp8zplwnl7-haskell-errors-ghc7.6.3-1.4.7
/nix/store/14rjz7iaa9a1q8mlg00pmdhwwn7ypd4x-haskell-distributive-ghc7.6.3-0.4.4
/nix/store/1jnipx1swkivg1ci0y7szdljklaj9cx1-haskell-skein-ghc7.6.3-1.0.9
...
So snap-web-routes
will be built, and all of its dependencies will be fetched from pre-built binaries. Sweet! If the dry-run
looks good, you can run the command again without the --dry-run
option or use nix-install
like we have previously. I omit that call here.
Note that this will install the package to your user environment. Remember: installing a package means making it active in the user environment. If you don’t want to install it, you can just build it with nix-build
, which will put it in your store but it won’t be installed for the user.
#!bash
nix-build -A haskellPackages.snapWebRoutes
Make the Pull Request
Again, if you’re already adept at git and github, you can skip this section. We’re going to push our changes up to github and make a pull request. The process is largely automatic! Add the new .nix file to the git repo, commit the changes, and then push the branch to github.
#!bash
git add pkgs/development/libraries/haskell/snap-web-routes/default.nix
git commit -a -m "Added snapWebRoutes to haskellPackages."
git push origin add-haskell-snap-web-routes
Go to your github repo and you should see a prompt asking if you want to create a pull request; something like NixOS:master -> fluffynukeit:add-haskell-snap-web-routes
. Follow the prompts and you’re done!
That’s it for this installment! New installments to come if/when I encounter new problems to write about!