Converting Cabal to Nix!

nix_cabal.png

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.

Previous
Previous

Using Nix to Fetch C Libraries!

Next
Next

Nix: Functional Package Management!