Nix: Functional Package Management!

nix_logo.png

In the past few weeks we've gone over the two primary build systems people use for Haskell, Stack and Cabal. These systems are specific to Haskell though. You couldn't, for instance, make a C++ or Node.js project using Cabal. This week though, we're going to discuss Nix, a more broadly applicable package manager.

We could use Nix for a project in virtually any programming language. It tends to attract a disproportionate amount of attention from Haskell developers though. There are a couple reasons for this. First, other languages tend to have more mature tool-chains than Haskell. So there's less of a need to explore alternative solutions like Nix. Second, the authors of Nix describe it as a "pure functional" package manager. So its design and functionality incorporate a lot of ideas that attract Haskellers.

So in this article, we're going to discuss the basics features of Nix. It has a few particular advantages due to its functional nature. We'll also do a crash-course in some of the basic commands. Next week, we'll start exploring some more Haskell-specific tasks with it.

While Nix has a lot of cool features, it also has a steep learning curve. So if you're new to Haskell, I strongly recommend you start out with Stack. Read our Liftoff Series and take our free Stack mini-course to learn more!

Installing and Basic Commands

To start out, let's get Nix installed. This is simple enough on a Linux or MacOS machine. You just need to curl the install script and run it:

bash <(curl https://nixos.org/nix/install)

Once you have Nix, you'll install packages from "channels". A channel is a mechanism that allows you to get pre-built binaries and libraries. In some ways, it works like Hackage, but for all kinds of programs. By installing, you'll get a stable default channel.

The primary command you'll use is nix-env. This lets you interact with packages on the channel and on your system. For instance, nix-env -qa will list all the available packages you can install from the channel. To actually install a package, you'll use the -i option (or --install). This command will install a basic GNU "hello" program:

>> nix-env -i hello
...
Installing 'hello-2.10'
>> hello
Hello, world!

You can also uninstall a program with the -e option. If you would rather test out a program without installing it, you can make a nix shell for it! The nix-shell command for will bring up a shell that has the program installed for you:

>> nix-env -e hello
>> hello
No such file or directory
>> nix-shell -p hello
[nix-shell:~]$ hello
Hello, world!

[nix-shell:~]$ exit

These simple commands allow you to download other peoples' packages. But the real fun comes with using Nix to make our own packages. We'll get to that next week!

Windows Compatibility

As the name suggests, Nix's authors designed it around Unix-based operating systems. Thus it doesn't exist for Windows. If you program on a Windows machine, your best bet is to use the Windows Subsystem for Linux (WSL). The other option would be some kind of virtual box, but these will slow you down a lot.

On my machine, I found I needed a bit of a hack to get Nix installed on WSL. Instead of installing normally, I followed the advice in this pull request. When installing, you'll want the following line in a pre-existing /etc/nix/nix.conf file.

use-sqlite-wal = false

You can do this by hand, or use this command as you install:

>> echo 'use-sqlite-wal = false' | \
  sudo tee -a /etc/nix/nix.conf && sh <(curl https://nixos.org/nix/install)

After doing this, everything should work!

Sandboxing on Steroids

Now let's dig into the functionality of Nix a little more. What does it mean that Nix is a "functional" package manager? Well, first off, our actions shouldn't have "side effects". In the case of package management, this means that we can install a program without impacting any other programs on our machine. It's essentially an extreme form of sandboxing.

When you install Nix, it will create a special /nix/store folder at the root of your system. When you use Nix, all packages you download and build on your machine get their own sub-directory. For example, when we installed hello, we get the following directory:

/nix/store/234v87nsmj70i1592h713i6xidfkqyjw-hello-2.10

Notice our package has a special hash associated with it. This is important. We could potentially update our channel to check for a newer version. Then we could install that as well. This could create a directory like so, with a different (hypothetical) hash:

>> nix-env --upgrade hello
>> ls /nix/store
/nix/store/abcd123smj70iqaswe713i6xidfpogjq-hello-2.11
...

But the original version of hello would still be in our store! The update and reinstall would have no effect on it, and it would work as it used to. And if we didn't like the new version, we could roll it back!

nix-env --rollback

Inputs and Outputs

Now let's investigate what's going on with those hashes. Each nix package is also "functional" in the sense that it is a function of its particular inputs. That is, every package has some list of dependencies as well as the source code. And we can reproduce the output binary from these inputs in a deterministic way.

The hash we get in front of the package gets constructed from all the different inputs. So if we change the dependencies for our package, we'll get a new hash, since the result could be different. And if we change our source code, our hash should also be different, as we're making a new program.

So when we define our own Nix package, we'll describe its dependencies using the Nix Language. This will include listing the versions and build configurations in a .nix file. This allows us to create a "derivation" of our program. And the real upshot is that this derivation will be the same no matter what machine you're on! Nix will ensure that it downloads all the same dependencies and compiles them in the same way.

Conclusion

Hopefully you've now got a good grounding in the basics of Nix. Next week, we'll start exploring how we can make our own Nix projects. We'll look at combining the powers of Cabal and Nix to create a Haskell project!

Previous
Previous

Converting Cabal to Nix!

Next
Next

Hpack: A Simpler Package Format