Some notes on using nix
Recently I started using a Mac for the first time. The biggest downside I’ve noticed so far is that the package management is much worse than on Linux. At some point I got frustrated with homebrew because I felt like it was spending too much time upgrading when I installed new packages, and so I thought – maybe I’ll try the nix package manager!
nix has a reputation for being confusing (it has its whole own programming language!), so I’ve been trying to figure out how to use nix in a way that’s as simple as possible and does not involve managing any configuration files or learning a new programming language. Here’s what I’ve figured out so far! We’ll talk about how to:
- install packages with nix
- build a custom nix package for a C++ program called paperjam
- install a 5-year-old version of hugo with nix
As usual I’ve probably gotten some stuff wrong in this post since I’m still pretty new to nix. I’m also still not sure how much I like nix – it’s very confusing! But it’s helped me compile some software that I was struggling to compile otherwise, and in general it seems to install things faster than homebrew.
(note from 18 months later in August 2024: I’ve mostly switched back to Homebrew, nix was interesting but overall I think it’s not worth the complexity for me)
what’s interesting about nix?
People often describe nix as “declarative package management”. I don’t care that much about declarative package management, so here are two things that I appreciate about nix:
- It provides binary packages (hosted at https://cache.nixos.org/) that you can quickly download and install
- For packages which don’t have binary packages, it makes it easier to compile them
I think that the reason nix is good at compiling software is that:
- you can have multiple versions of the same library or program installed at a time (you could have 2 different versions of libc for instance). For example I have two versions of node on my computer right now, one at
/nix/store/4ykq0lpvmskdlhrvz1j3kwslgc6c7pnv-nodejs-16.17.1
and one at/nix/store/5y4bd2r99zhdbir95w5pf51bwfg37bwa-nodejs-18.9.1
. - when nix builds a package, it builds it in isolation, using only the
specific versions of its dependencies that you explicitly declared. So
there’s no risk that the package secretly depends on another package on your
system that you don’t know about. No more fighting with
LD_LIBRARY_PATH
! - a lot of people have put a lot of work into writing down all of the dependencies of packages
I’ll give a couple of examples later in this post of two times nix made it easier for me to compile software.
how I got started with nix
here’s how I got started with nix:
- Install nix. I forget exactly how I did this, but it looks like there’s an official installer and an unofficial installer from zero-to-nix.com. The instructions for uninstalling nix on MacOS with the standard multi-user install are a bit complicated, so it might be worth choosing an installation method with simpler uninstall instructions.
- Put
~/.nix-profile/bin
on my PATH - Install packages with
nix-env -iA nixpkgs.NAME
- That’s it.
Basically the idea is to treat nix-env -iA
like brew install
or apt-get install
.
For example, if I want to install fish
, I can do that like this:
nix-env -iA nixpkgs.fish
This seems to just download some binaries from https://cache.nixos.org – pretty simple.
Some people use nix to install their Node and Python and Ruby packages, but I haven’t
been doing that – I just use npm install
and pip install
the same way I
always have.
some nix features I’m not using
There are a bunch of nix features/tools that I’m not using, but that I’ll mention. I originally thought that you had to use these features to use nix, because most of the nix tutorials I’ve read talk about them. But you don’t have to use them.
- NixOS (a Linux distribution)
- nix-shell
- nix flakes
- home-manager
- devenv.sh
I won’t go into these because I haven’t really used them and there are lots of explanations out there.
where are nix packages defined?
I think packages in the main nix package repository are defined in https://github.com/NixOS/nixpkgs/
It looks like you can search for packages at https://search.nixos.org/packages. The two official ways to search packages seem to be:
nix-env -qaP NAME
, which is very extremely slow and which I haven’t been able to get to actually worknix --extra-experimental-features 'nix-command flakes' search nixpkgs NAME
, which does seem to work but is kind of a mouthful. Also all of the packages it prints out start withlegacyPackages
for some reason
I found a way to search nix packages from the command line that I liked better:
- Run
nix-env -qa '*' > nix-packages.txt
to get a list of every package in the Nix repository - Write a short
nix-search
script that just grepspackages.txt
(cat ~/bin/nix-packages.txt | awk '{print $1}' | rg "$1"
)
everything is installed with symlinks
One of nix’s major design choices is that there isn’t one single bin
with all
your packages, instead you use symlinks. There are a lot of layers of symlinks. A few examples of symlinks:
~/.nix-profile
on my machine is (indirectly) a symlink to/nix/var/nix/profiles/per-user/bork/profile-111-link/
~/.nix-profile/bin/fish
is a symlink to/nix/store/afkwn6k8p8g97jiqgx9nd26503s35mgi-fish-3.5.1/bin/fish
When I install something, it creates a new profile-112-link
directory with new symlinks and updates my ~/.nix-profile
to point to that directory.
I think this means that if I install a new version of fish
and I don’t like it, I can
easily go back just by running nix-env --rollback
– it’ll move me to my previous profile directory.
uninstalling packages doesn’t delete them
If I uninstall a nix package like this, it doesn’t actually free any hard drive space, it just removes the symlinks.
$ nix-env --uninstall oil
I’m still not sure how to actually delete the package – I ran a garbage collection like this, which seemed to delete some things:
$ nix-collect-garbage
...
85 store paths deleted, 74.90 MiB freed
But I still have oil
on my system at /nix/store/8pjnk6jr54z77jiq5g2dbx8887dnxbda-oil-0.14.0
.
There’s a more aggressive version of nix-collect-garbage
that also deletes old versions of your profiles (so that you can’t rollback)
$ nix-collect-garbage -d --delete-old
That doesn’t delete /nix/store/8pjnk6jr54z77jiq5g2dbx8887dnxbda-oil-0.14.0
either though and I’m not sure why.
upgrading
It looks like you can upgrade nix packages like this:
nix-channel --update
nix-env --upgrade
(similar to apt-get update && apt-get upgrade
)
I haven’t really upgraded anything yet. I think that if something goes wrong with an upgrade, you can roll back (because everything is immutable in nix!) with
nix-env --rollback
Someone linked me to this post from Ian Henry that
talks about some confusing problems with nix-env --upgrade
– maybe it
doesn’t work the way you’d expect? I guess I’ll be wary around upgrades.
next goal: make a custom package of paperjam
After a few months of installing existing packages, I wanted to make a custom package with nix for a program called paperjam that wasn’t already packaged.
I was actually struggling to compile paperjam
at all even without nix because the version I had
of libiconv
I has on my system was wrong. I thought it might be easier to
compile it with nix even though I didn’t know how to make nix packages yet. And
it actually was!
But figuring out how to get there was VERY confusing, so here are some notes about how I did it.
how to build an example package
Before I started working on my paperjam
package, I wanted to build an example existing package just to
make sure I understood the process for building a package. I was really
struggling to figure out how to do this, but I asked in Discord and someone
explained to me how I could get a working package from https://github.com/NixOS/nixpkgs/ and build it. So here
are those instructions:
step 1: Download some arbitrary package from nixpkgs on github, for example the dash
package:
wget https://raw.githubusercontent.com/NixOS/nixpkgs/47993510dcb7713a29591517cb6ce682cc40f0ca/pkgs/shells/dash/default.nix -O dash.nix
step 2: Replace the first statement ({ lib , stdenv , buildPackages , autoreconfHook , pkg-config , fetchurl , fetchpatch , libedit , runCommand , dash }:
with with import <nixpkgs> {};
I don’t know why you have to do this,
but it works.
step 3: Run nix-build dash.nix
This compiles the package
step 4: Run nix-env -i -f dash.nix
This installs the package into my ~/.nix-profile
That’s all! Once I’d done that, I felt like I could modify the dash
package and make my own package.
how I made my own package
paperjam
has one dependency (libpaper
) that also isn’t packaged yet, so I needed to build libpaper
first.
Here’s libpaper.nix
. I basically just wrote this by copying and pasting from
other packages in the nixpkgs repository.
My guess is what’s happening here is that nix has some default rules for
compiling C packages (like “run make install
”), so the make install
happens
default and I don’t need to configure it explicitly.
with import <nixpkgs> {};
stdenv.mkDerivation rec {
pname = "libpaper";
version = "0.1";
src = fetchFromGitHub {
owner = "naota";
repo = "libpaper";
rev = "51ca11ec543f2828672d15e4e77b92619b497ccd";
hash = "sha256-S1pzVQ/ceNsx0vGmzdDWw2TjPVLiRgzR4edFblWsekY=";
};
buildInputs = [ ];
meta = with lib; {
homepage = "https://github.com/naota/libpaper";
description = "libpaper";
platforms = platforms.unix;
license = with licenses; [ bsd3 gpl2 ];
};
}
Basically this just tells nix how to download the source from GitHub.
I built this by running nix-build libpaper.nix
Next, I needed to compile paperjam
. Here’s a link to the nix package I wrote. The main things I needed to do other than telling it where to download the source were:
- add some extra build dependencies (like
asciidoc
) - set some environment variables for the install (
installFlags = [ "PREFIX=$(out)" ];
) so that it installed in the correct directory instead of/usr/local/bin
.
I set the hashes by first leaving the hash empty, then running nix-build
to get an error message complaining about a mismatched hash. Then I copied the correct hash out of the error message.
I figured out how to set installFlags
just by running rg PREFIX
in the nixpkgs repository – I figured that needing to set a PREFIX
was
pretty common and someone had probably done it before, and I was right. So I
just copied and pasted that line from another package.
Then I ran:
nix-build paperjam.nix
nix-env -i -f paperjam.nix
and then everything worked and I had paperjam
installed! Hooray!
next goal: install a 5-year-old version of hugo
Right now I build this blog using Hugo 0.40, from 2018. I don’t need any new features so I haven’t felt a need to upgrade. On Linux this is easy: Hugo’s releases are a static binary, so I can just download the 5-year-old binary from the releases page and run it. Easy!
But on this Mac I ran into some complications. Mac hardware has changed in the
last 5 years, so the Mac Hugo binary I downloaded crashed. And when I tried to
build it from source with go build
, that didn’t work either because Go build
norms have changed in the last 5 years as well.
I was working around this by running Hugo in a Linux docker container, but I didn’t love that: it was kind of slow and it felt silly. It shouldn’t be that hard to compile one Go program!
Nix to the rescue! Here’s what I did to install the old version of Hugo with nix.
installing Hugo 0.40 with nix
I wanted to install Hugo 0.40 and put it in my PATH as hugo-0.40
. Here’s how
I did it. I did this in a kind of weird way, but it worked (Searching and installing old versions of Nix packages
describes a probably more normal method).
step 1: Search through the nixpkgs repo to find Hugo 0.40
I found the .nix
file here https://github.com/NixOS/nixpkgs/blob/17b2ef2/pkgs/applications/misc/hugo/default.nix
step 2: Download that file and build it
I downloaded that file (and another file called deps.nix
in the same directory), replaced the first line with with import <nixpkgs> {};
, and built it with nix-build hugo.nix
.
That almost worked without any changes, but I had to make two changes:
- replace
with stdenv.lib
towith lib
for some reason. - rename the package to
hugo040
so that it wouldn’t conflict with the other version ofhugo
that I had installed
step 3: Rename hugo
to hugo-0.40
I write a little post install script to rename the Hugo binary.
postInstall = ''
mv $out/bin/hugo $out/bin/hugo-0.40
'';
I figured out how to run this by running rg 'mv '
in the nixpkgs repository and just copying and modifying something that seemed related.
step 4: Install it
I installed into my ~/.nix-profile/bin
by running nix-env -i -f hugo.nix
.
And it all works! I put the final .nix
file into my own personal nixpkgs repo so that I can use it again later if I
want.
reproducible builds aren’t magic, they’re really hard
I think it’s worth noting here that this hugo.nix
file isn’t magic – the
reason I can easily compile Hugo 0.40 today is that many people worked for a long time to make it possible to
package that version of Hugo in a reproducible way.
that’s all!
Installing paperjam
and this 5-year-old version of Hugo were both
surprisingly painless and actually much easier than compiling it without nix,
because nix made it much easier for me to compile the paperjam
package with
the right version of libiconv
, and because someone 5 years ago had already
gone to the trouble of listing out the exact dependencies for Hugo.
I don’t have any plans to get much more complicated with nix (and it’s still very possible I’ll get frustrated with it and go back to homebrew!), but we’ll see what happens! I’ve found it much easier to start in a simple way and then start using more features if I feel the need instead of adopting a whole bunch of complicated stuff all at once.
I probably won’t use nix on Linux – I’ve always been happy enough with apt
(on Debian-based distros) and pacman
(on Arch-based distros), and they’re
much less confusing. But on a Mac it seems like it might be worth it. We’ll
see! It’s very possible in 3 months I’ll get frustrated with nix and just go back to homebrew.
5-month update: rebuilding my nix profile
Update from 5 months in: nix is still going well, and I’ve only run into 1
problem, which is that every nix-env -iA
package installation started failing
with the error “bad meta.outputsToInstall”.
This script from Ross Light fixes that problem though. It lists every derivation installed in my current profile and creates a new profile with the exact same derivations. This feels like a nix bug (surely creating a new profile with the exact same derivations should be a no-op?) but I haven’t looked into it more yet.