Converting Cabal to Nix!
In last week's article we discussed the basics of the Nix package manager. Nix brings some of the ideas of functional purity to package management in Haskell. For example, when you download a package, there are no side effects on other packages in your system. And any package is a direct product of its dependencies and source code, generating a unique hash.
But Nix doesn't typically work in a standalone fashion with Haskell projects. Like Stack, it actually integrates with Cabal under the hood. We'll continue to use a .cabal
file to describe our package. But a neat tool allows us to get the advantages of Nix in conjunction with Cabal. This week, we'll explore how to make a basic project with this combination.
If you're a beginner to Haskell, you're probably better off starting with Stack, rather than Nix. Read our Liftoff Series to get familiar with the language basics. Then you can take our free Stack mini-course to get acquainted with Stack.
Initializing Our Project with Nix
Last week, we explored the nix-shell
command. This allows you to run commands as if you had certain packages installed. So even if our system doesn't have GHC or Cabal installed, we can still run the following command:
>> nix-shell --packages ghc cabal-install --run "cabal init"
This will open up a shell where Nix gives us the necessary packages. Then it runs the cabal init
package, like we saw in our Cabal tutorial a few weeks back. This gives us a basic Cabal project, which we could build and run using Cabal tools.
Converting to Nix
But instead of doing that, we're going to convert it to a Nix project! There's an excellent resource out there that does this conversion for us. The cabal2nix program can take a Cabal file and convert it into a nix package file. How do we install this program? Well, using Nix of course! We can even skip installing it and run the command through nix-shell
. Note how we pass the current directory "." as an input to the command, and pipe the output to our nix file, default.nix
.
>> nix-shell --packages cabal2nix --run "cabal2nix ." > default.nix
Here's the resulting file:
{ mkDerivation, base, stdenv }:
mkDerivation {
pname = "MyNixProject";
version = "0.1.0.0";
src = ./.;
isLibrary = false;
isExecutable = true;
executableHaskellDepends = [ base ];
license = "unknown"
hydraPlatforms = stdenv.lib.platforms.none;
}
This file contains a single expression in the Nix language. The first line contains the three "inputs" to our expression. Of these, base
is our only Haskell dependency. Then mkDerivation
is a Nix function we can use to make our derivation expression. Finally, there's this stdenv
dependency. This is a special input telling Nix we have a standard Linux environment. Nix assumes it has certain things like GCC and Bash.
Then we assign certain parameters in our call to mkDerivation
. We provide our package name and version. We also state that it contains an executable and not a library (for now). There's a special field where we list Haskell dependencies. This contains only base
for now.
We can't build from this file just yet. Instead, we'll make another file release.nix
. This file contains, again, a single Nix expression. We use an imported function haskellPackages.callPackage
to call our previous file.
let
pkgs = import <nixpkgs> { };
in
pkgs.haskellPackages.callPackage ./default.nix { }
We don't provide any parameters for right now, so we'll leave the empty braces there. For a production project, you would use this file to "pin" the nix packages to a particular channel. But we don't need to do that right now.
We can now build our program using the nix-build
command and the new release file:
>> nix-build release.nix
Building the project puts the results in a result
directory at your project root. So we can then run our simple binary!
>> ./result/bin/MyNixProject
Hello, Haskell!
Adding a Dependency
As we have in previous tutorials, let's make this a little more interesting by adding a dependency! We'll once again use the basic split
package. We'll make a lastName
function in our library like so:
module Lib where
import Data.List.Split (splitOn)
lastName :: String -> String
lastName input = last (splitOn " " input)
Then we'll use this in our executable. Instead of simply printing a message, we'll get the user's name and then print their last name:
module Main where
import Lib (lastName)
main :: IO ()
main = do
putStrLn "What's your name?"
input <- getLine
let lName = lastName input
putStr "Your last name is: "
putStrLn lName
We'll need to update our .cabal
file from its initial state to include the library and the new dependency. Notice we change the executable name to avoid confusion with the library.
library
build-depends:
base >=4.12 && <4.13
, split == 0.2.3.3
...
executable run-nix-project
build-depends:
base >=4.12 && <4.13
, MyNixProject
Now that we've changed our .cabal
file, we must run cabal2nix
again to re-generate default.nix
. We do this with the same nix-shell
invocation we used earlier. (It's a good idea to save this command to an alias). Then we can see that default.nix
has changed.
{ mkDerivation, base, split, stdenv }:
mkDerivation {
pname = "MyNixProject";
version = "0.1.0.0";
src = ./.;
isLibrary = true;
isExecutable = true;
libraryHaskellDepends = [base split ];
executableHaskellDepends = [ base ];
license = "unknown"
hydraPlatforms = stdenv.lib.platforms.none;
}
The new file reflects the changes to our project. The split
package is now an input to our project. So it appears as an "argument" on the first line. We've added a library, so we see that isLibrary
has changed to true
. There's also a new line for libraryHaskellDepends
. It contains the split
package we use as a dependency. Now Nix will handle the task of finding the right dependency on our channel!
We can once again build our code using nix. We'll see the different behavior, now under a different executable name!
>> nix-build release.nix
>> ./result/bin/run-nix-project
What's your name?
John Test
Your last name is: Test
Conclusion
This wraps up our first look at making a Haskell project with Nix. We still have Cabal providing a basic description of our package. But Nix actually provides us with the package database. Next week, we'll go into these details a little more, and take it one step further. We'll show how we can use Stack and Nix together to unify all the different package managers we've looked at.
As I've mentioned before, Nix does have a steeper learning curve than our other tools. In particular, there are not as many clear tutorials out there. So I recommend starting out with Stack before learning Nix. You can do this by taking our Stack mini-course.
Given the lack of clear tutorials, I want to highlight a couple resources that helped me write this article. First, the haskell-nix Github repository, written by Gabriel Gonzalez. Second this blog post by Soares Chen. These are good resources if you want to go a bit further with Haskell and Nix.