I don’t feel like I know enough about Nix to manage my own build dependencies with it, but I do feel like I know enough to use Nix as a simple package manager. I use Homebrew on my laptop, and at this point I definitely know more about Nix than I do about Homebrew.1

Why would I switch away from Homebrew? Mostly because I think it will be educational. I don’t really have anything against Homebrew. It works. It has a lot of the packages I want. It does break more often than I’d like – not, like, often, but any time I go more than a year or so without using a particular computer I know that I’m going to return to some bizarre Ruby error that I have to spend half an hour figuring out before I can do anything.

And there’s no way that Nix would ever play me like that.

Homebrew doesn’t seem to distinguish between “packages I want to have installed” and “packages I have to have installed because they are dependencies of other packages.”

I remember this bothering me before. It’s bothering me a lot right now, as I try to get rid of it.

When I type brew list to see installed packages, I get like hundreds of entries – only a couple dozen of which I actually remember installing. I have to go through it by hand to figure out what I actually want to have.

Here’s what I find:

cabal-install
curl
fzf
git
haskell-stack
hexedit
htop
hugo
imagemagick
jq
mercurial
nasm
nmap
node
nodenv
opam
openssl
pandoc
pngcrush
pv
pyenv
python@2
python@3.9
rbenv
ripgrep
sqlite
terminal-notifier
tmux
tree
xz
yarn
yasm
zsh

33 packages. Not that many! I’m a little surprised. But I haven’t really done any development on this computer in a few years. Whatever I don’t need to justify myself to you.

Let’s try to nix-env -iA nixpkgs.* them.

$ nix-env -iA nixpkgs.{cabal-install,curl,fzf,git,haskell-stack,...}

And… it worked? Mostly?

I am kind of surprised to report that almost all of these just worked. Some of the packages had different names – node is called nodejs, and python is python2 and python3, for example – but otherwise it was super smooth. The only missing packages are nodenv and pyenv – which seems fair. If you’re using Nix, you don’t need those.

Well, no, that’s not true: you might want to make something that works for people who aren’t using Nix, so you still want to distribute a .python-version file and test that it does the right thing or whatever. It’s still a useful package.

And, oddly, rbenv is here… hmm. Maybe pyenv and nodenv are just missing, and not philosophically boycotted.

I’m kind of surprised that terminal-notifier is in here: I thought that was a macOS-specific thing. Maybe it is, and I am not the only person using Nix on macOS.

But yeah, that went very smoothly. Everything mostly just worked. Great experience no notes.

Oh, except for one little thing:

$ nix-env -iA nixpkgs.mercurial
installing 'mercurial-5.6'
these paths will be fetched (3.45 MiB download, 14.95 MiB unpacked):
  /nix/store/7x5s4k57cw0a8nldypmm1y5f763k01kl-mercurial-5.6
copying path '/nix/store/7x5s4k57cw0a8nldypmm1y5f763k01kl-mercurial-5.6' from 'https://cache.nixos.org'...
Assertion failed: (size_ < capacity_), function push_back, file src/libexpr/attr-set.hh, line 54.
[1]    1968 abort      nix-env -iA nixpkgs.mercurial

Oh dear. Mercurial is not some weird obscure package, either. It’s… it’s a pretty big one. I find a GitHub issue opened (at time of writing) 20 days ago:

https://github.com/NixOS/nixpkgs/issues/112465

Yikes. Yikes. Okay. So that’s not great.

I am using the “unstable” Nixpkgs channel, but I didn’t choose to live life on the edge. That’s just the default thing that you get when you install Nix!

So… well, yep, I guess that’s a pretty well-named channel. But 20 days of instability is pretty surprising. I have no idea what the error means, or what I would do to fix it. I’m not even trying to build it! I just want to download it from the cache!

So I cannot get rid of Homebrew completely.2 But I uninstall everything I can, and my brew list is now just:

autoconf
mercurial
nodenv
pkg-config
python@3.9
sqlite
xz
gdbm
node-build
openssl@1.1
pyenv
readline
tcl-tk

Everything left there is a dependency of mercurial, nodenv, or pyenv. That is the closure of those packages, if you will. Did I do it? Did I use the words?

zsh is a little interesting, since I use that as my login shell. So I’ll have to figure that out.3

How do I print out the path to a Nix package? I want to see the path to zsh. This whole time I’ve just been doing which foo, but which zsh gives me /bin/zsh, so I need to figure out the right way.

I look through nix --help and try a promising command:

$ nix path-info nixpkgs.zsh
/nix/store/bainh95kdn1h0gy5rdayjj7qq15a6q46-zsh-5.8

Neat. I add that to /etc/shells so I can use the installation from Nix.

Except… hmm. That’s not gonna work very well for me, is it? If I ever upgrade zsh, I’m going to have a new hash, and it will eventually be garbage-collected, and I will have no login shell.

To be safe, I add it as a GC root. And I guess… I’ll have to manually chsh any time I upgrade zsh? Okay. Seems… tolerable. Definitely not great. But how often can zsh change? No way there will be some critical security vulnerability and I’ll forget that I ever did this.

But alright. I did the thing. I migrated. But that was the easy part.

homebrew, as she is played

There’s something that I’ve wanted to do from the very beginning of this series: write – for myself, mostly – a translation table between Homebrew commands and Nix commands. I thought that by the end of the manual I would be able to do this no problem. But that is not actually the case…

I look through zsh_history to see what brew commands I use most frequently. I only have ten thousand total history entries, most of which are not brew, so this is just a sampling of recent usage. But here’s what I see (after filtering out typos and commands that no longer exist):

138 info
135 uninstall
108 install
 63 search
 41 upgrade
 22 list
 19 update
 15 outdated
  7 link
  7 doctor
  5 leaves
  5 cleanup
  4 untap
  3 tap
  3 reinstall

Wait. brew leaves? You can list just installed packages? I actually googled this, and couldn’t find it. Boo. I guess that past Ian knew this though, at some point.

Huh. Okay. I take it back. I’m sorry I doubted you, Homebrew.

Anyway: the command I use the most often is info. And… wow.

Off to a bad start.

I don’t know what the Nix equivalent of brew info is.

Umm.

Hmm.

Welp! That’s not great. Nothing in nix --help sounds plausible – no nix show or nix info or anything. nix-env -q sounds sort of maybe like it could do this – I’m looking to do a type of query. man nix-env tells me about --meta:

$ nix-env -qaA nixpkgs.git --meta
git-2.30.0

Cool cool thanks? Alright. Oh. I should have kept reading:

This option is only available with --xml or --json.

But it’s not an error if you pass it anyway! It’s just silently not what you want! Gross.

$ nix-env -qaA nixpkgs.git --meta --json
{
  "nixpkgs.git": {
    "name": "git-2.30.0",
    "pname": "git",
    "version": "2.30.0",
    "system": "x86_64-darwin",
    "meta": {
      "available": true,
      "broken": false,
      "changelog": "https://raw.githubusercontent.com/git/git/2.30.0/Documentation/RelNotes/2.30.0.txt",
      "description": "Distributed version control system",
      "homepage": "https://git-scm.com/",
      "insecure": false,
      "license": {
        "deprecated": true,
        "fullName": "GNU General Public License v2.0",
        "spdxId": "GPL-2.0",
        "url": "https://spdx.org/licenses/GPL-2.0.html"
      },
      "longDescription": "Git, a popular distributed version control system designed to\nhandle very large projects with speed and efficiency.\n",
      "maintainers": [ /* ... */ ],
      "name": "git-2.30.0",
      "outputsToInstall": [
        "out"
      ],
      "platforms": [ /* ... */ ],
      "position": "/nix/store/q9vdjh5mh5f2cypycbqxdiwls44b1n5i-nixpkgs-21.05pre272788.870dbb751f4/nixpkgs/pkgs/applications/version-management/git-and-tools/git/default.nix:335",
      "unfree": false,
      "unsupported": false
    }
  }
}

Okay. That’s… way too much stuff.

Let’s compare that to brew info git:

$ brew info git
git: stable 2.30.1 (bottled), HEAD
Distributed revision control system
https://git-scm.com
Not installed
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/git.rb
License: GPL-2.0-only
==> Dependencies
Required: gettext ✘, pcre2 ✘
==> Options
--HEAD
    Install HEAD version
==> Caveats
The Tcl/Tk GUIs (e.g. gitk, git-gui) are now in the `git-gui` formula.
==> Analytics
install: 216,142 (30 days), 554,354 (90 days), 2,371,732 (365 days)
install-on-request: 213,566 (30 days), 547,255 (90 days), 2,290,105 (365 days)
build-error: 0 (30 days)

Huh. That actually also looks like way too much stuff.

So it’s worth asking what I care about here. Why do I use brew info? I ponder this for a moment, and come up with a few reasons:

  • to check if a specific package name is the actual thing I want before I install it (i.e. cabal vs cabal-install)
  • to read the package’s description (usually because I encountered some unfamiliar command in a blog post somewhere: this is a very fast way to find out what it is)
  • to see what version of the package Homebrew has (especially in the case of compilers and development tools, where I care deeply about the exact version I’m using)

Those are the only reasons I can think of. So honestly the first two lines of brew info’s output cover 100% of what I want.

I don’t think I ever really realized that. Turning back to Nix, how can I get those two lines?

$ nix-env -qaA nixpkgs.git --description
git-2.30.0  Distributed version control system

Huh. Maybe… that’s all I need? It’s pretty darn close. And it’s only 190% more characters to type than brew info git!

I see there is a meta.longDescription. I would weakly prefer something like this:

$ nix-env -qaA "nixpkgs.$1" --json \
  | jq -r '.[] | .name + " " + .meta.description,
           "",
           (.meta.longDescription | rtrimstr("\n"))'
git-2.30.0 Distributed version control system

Git, a popular distributed version control system designed to
handle very large projects with speed and efficiency.

So I add this to my script directory as sd nix info, to reduce wear on my fingertips.

Alright. Let’s move on: uninstall and install. I know this:

$ nix-env -iA nixpkgs.git
$ nix-env -e git

Except that the string “git” means a different thing when you use -iA than it does when you use -e.4 You install packages by their “Nixpkgs attribute name” or whatever, but you uninstall them by their “name.” You can install by “name” but it takes 30 seconds so you will never do that. There is no way to uninstall by “attribute.”

I understand why this is, now: the attribute path in Nixpkgs is not an intrinsic part of the package. The package doesn’t know that it’s called nixpkgs.git. You could evaluate nixpkgs.git and find its store path, but that wouldn’t work in the case that nixpkgs.git has changed since you installed it (because of nix-channel --update, say, or because you renamed the channel, or whatever).

So like… this is a weird gross problem that Homebrew doesn’t have.

Or does it? Have you ever tried to uninstall a package that has been removed from Homebrew? Or renamed? Or to uninstall a package that was installed with an older version of Homebrew and now you’re getting weird Ruby errors because the format of casks has changed in a backwards-incompatible way?

You haven’t? Oh. Yeah; to be fair it is pretty rare.

As a side note, I really don’t like that nix-env -e foo just silently does nothing of you have a typo, or mix up the two kinds of “name”:

$ nix-env -e foo

$ echo $?
0

But I guess… it’s unlikely to cause real problems? It was slightly confusing while I was building and installing my own derivations to “practice” Nix, but I don’t think it would actually be confusing in real life.

brew search git is nix search gitnix wins this one hands down. Here’s brew search git:

$ (time brew search git) | head
==> Formulae
bagit
bash-git-prompt
bit-git
cgit
digitemp
easy-git
git
git-absorb
git-annex
brew search git  6.46s user 3.82s system 58% cpu 17.704 total

And our challenger:

$ (time nix search git) | head
warning: using cached results; pass '-u' to update the cache
* nixpkgs.act (act-0.2.20)
  Run your GitHub Actions locally

* nixpkgs.argocd (argocd-1.8.5)
  Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes

* nixpkgs.axoloti (axoloti-1.0.12-2)
  Sketching embedded digital audio algorithms.  To fix permissions of the Axoloti USB device node, add a similar udev rule to <literal>services.udev.extraRules</literal>: <literal>SUBSYSTEM=="usb", ATTR{idVendor}=="16c0", ATTR{idProduct}=="0442", OWNER="someuser", GROUP="somegroup"</literal>

* nixpkgs.bat (bat-0.17.1)
nix search git  0.90s user 0.05s system 89% cpu 1.063 total

Much more useful output – I don’t need to run brew info on all of those to see what they are, and because it’s locally cached, it’s much faster – at the cost of having to invalidate it myself, with which I am totally fine.

Alright. brew upgrade is nix-env -u. Pretty easy.5

Where are we now?

138 ✓ info
135 ✓ uninstall
108 ✓ install
 63 ✓ search
 41 ✓ upgrade
 22   list
 19   update
 15   outdated
  7   link
  7   doctor
  5   leaves
  5   cleanup
  4   untap
  3   tap
  3   reinstall

Thanks.

brew list is… I don’t know.

I want to say nix-env -q, but I now know that that’s actually equivalent to brew leaves. That’s really what I want when I say brew list, though.

It’s nice seeing dependencies as well, I guess, but sort of useless without seeing why they’re there. So I guess:

$ nix-store --query --tree -u ~/.nix-profile/ | head
/nix/store/5cmxacs3grxnyqcc8a58hqg0i2w8xza3-user-environment
+---/nix/store/0drrznwp316knss36m177bk3r3pgj31i-xz-5.2.5-bin
|   +---/nix/store/5y6lcfjghk5kbv4782vi7w79vz60gsyn-bash-4.4-p23
|   |   +---/nix/store/5y6lcfjghk5kbv4782vi7w79vz60gsyn-bash-4.4-p23 [...]
|   +---/nix/store/awgjr2jdyr2gqhkd8nb03yh8bzcg1zci-xz-5.2.5
|       +---/nix/store/awgjr2jdyr2gqhkd8nb03yh8bzcg1zci-xz-5.2.5 [...]
+---/nix/store/1s7qdbi6fprms0hljsx9rq2fwr3fz1gi-nix-2.3.10-man
+---/nix/store/298mjs1y9r3yxri5sjhsqakkzv0arcnx-stack-2.5.1.1
|   +---/nix/store/7r6hlpv7z1myj2nn4lvxmhga3q9s3k8x-libffi-3.3
|   |   +---/nix/store/7r6hlpv7z1myj2nn4lvxmhga3q9s3k8x-libffi-3.3 [...]

Is sort of… anything? This is a stretch. I’m going to call it nix-env -q and leave it at that.

brew update is nix-channel --update. No; there’s no nix-channel -u, insanely.

And it seems like there’s no good equivalent of brew outdated. I like running brew outdated as a nice way to just see what packages are under active development. I like to read the release notes of, like, git before I upgrade it. There’s nix-env -u --dry-run

$ nix-env -u --dry-run
(dry run; not doing anything)
upgrading 'python3-3.8.7' to 'python3-3.10.0a4'
these derivations will be built:
  /nix/store/sfkdnpn3klqb7iymg520m33c3pp7zjk0-python3-3.10.0a4.drv
these paths will be fetched (17.86 MiB download, 17.92 MiB unpacked):
  /nix/store/01h4dkp0nyrsw0zawmasxawhfik474ma-Python-3.10.0a4.tar.xz
  /nix/store/9kwzs3pplms8sijf55sdryypzvic4x1s-python-3.x-distutils-C++.patch
  /nix/store/cxnlnh40dvw6ipfq4f9sdba0z2l291iz-python-setup-hook.sh
  /nix/store/glhx09dkz4h6b6gvs42j7nsn2jv7bn3m-readline-6.3p08-dev
  /nix/store/l2gg10pmpcj7awz93yk4q2n5is5y9kjl-nuke-references

Compare that to:

$ brew outdated
node-build (4.9.28) < 4.9.31
openssl@1.1 (1.1.1i) < 1.1.1j
pyenv (1.2.22) < 1.2.23
python@3.9 (3.9.1_8) < 3.9.2_1

Homebrew wins this one by… a lot. Maybe there’s a way to get better output out of nix, but I can’t really figure anything out myself.

I think that’s all the like… commands that I use on the regular. We’re into weirder territory now:

138 ✓ info
135 ✓ uninstall
108 ✓ install
 63 ✓ search
 41 ✓ upgrade
 22 ✓ list
 19 ✓ update
 15 ✓ outdated
  7   link
  7   doctor
  5   leaves
  5   cleanup
  4   untap
  3   tap
  3   reinstall

I don’t know what brew link does. I think it’s a command that I run when Homebrew tells me to and usually means that something has gone very wrong. Maybe? I don’t know.

I love brew doctor. It’s always spewing dozens of problems for me to ignore. I think it’s useful when you’re installing Homebrew for the first time and getting it set up, but I don’t think it’s ever useful ever again. I love warnings like this:

Warning: Unbrewed static libraries were found in /usr/local/lib.
If you didn't put them there on purpose they could cause problems when
building Homebrew formulae, and may need to be deleted.

Unexpected static libraries:
  /usr/local/lib/libcord.a
  /usr/local/lib/libgc.a

man wouldn’t it be neat if there were a package manager that could isolate builds such that

I am surprised to see that there is actually nix doctor:

$ nix doctor
Store uri: local

error: not an absolute path: './node_modules'

Ooookay. So just as useful, I guess?

I think it’s trying to complain that I have ./node_modules/.bin in my PATH? Which I do so I can invoke my devDependencies when I use npm. It’s never caused a problem before. I don’t know if this is a problem that nix doctor is trying to report or if it’s an error that nix doctor encountered that broke it. So I temporarily remove that from my PATH to re-run it:

$ nix doctor
Store uri: local

Okay? Still no idea. It would be nice if it told me what it was doing – what checks I had passed – so that I could feel good about myself. Like when you run that SSL test thing and it gives you an A+. There’s nothing for you to do, but it feels good to hear, you know?

brew cleanup is basically nix-collect-garbage -d. Did you know you needed to manually collect garbage in Homebrew? I remember the first time I ever ran that and it freed up like 8 GB of old cached files or whatever. Good times.

brew tap and brew untap are basically nix-channel --add and nix-channel --remove, if I understand them correctly.

And then brew reinstall, which in Nix is just nix-env -i again. I think. Or just uninstall/reinstall. Honestly: not a very important command. I probably have only ever run it out of desperation, and it probably has never fixed the problem I was having.

Okay! And that’s how to use Nix instead of Homebrew.

It’s not as nice! In a lot of ways! And you can’t install hg!

I might make more helpers as time goes on so that I can just type sd nix install or sd nix outdated or whatever, and paper over some of the major UI problems that way. sd nix info has already been pretty nice to have, in the time between writing this post and publishing it.

Maybe every Nix user ever ends up doing this, and that’s why the UI is, you know, the way that it is.

Anyway, I’m going to try using Nix as my package manager for a little while and see what happens. I will report back if anything exciting happens.


  • Is there a better way to see outdated packages?
  • How should I install my login shell with Nix?
  • What happens if two packages produce outputs with the same name? Like what if I install /nix/store/foo-1.0/bin/foo and /nix/store/bar-1.0/bin/foo – what ends up in ~/.nix-profile/bin/foo?

  1. Of course, I’ve never needed to know anything about Homebrew to use Homebrew, because Homebrew just kinda works most of the time. ↩︎

  2. I also use lots of Homebrew casks, and I don’t bother trying to replace those in Nix – that’s why you don’t see emacs in that list. So I wouldn’t get to uninstall Homebrew anyway. how dare you suggest that i don’t use emacs ↩︎

  3. No way did I only think of this after uninstalling my current login shell and having to scramble to get a working terminal again so I could reinstall it. I definitely thought of it ahead of time because I’m very conscientious. ↩︎

  4. Yes, I used -e instead of --uninstall. I came up with a mnemonic to remember the short version: “-i before -e.” Cute, right? ↩︎

  5. How naïve I was. Yes, nix-env -u exists. And it might even work, for some packages. But if you’re using the Nixpkgs repository, nix-env -u is basically useless. See My first package upgrade, in which I first encounter its failings, How to install Python, in which I learn why it doesn’t work, and Ambiguous packages, in which I discover just how deeply broken it really is. Or see Setting up a declarative user environment for a simple alternative that works the way you want it to, and that I have been using without issue since writing that post. ↩︎