I start reading through the Nix manual. I won’t quote it in its entirety like I did in the glossary. But I will any time I want to highlight something or I have a question or I don’t understand something.

Chapter 1. About Nix

This is mostly a feature tour – sort of explaining what Nix is and why you should use it. There’s very little actual information about how Nix works, but it does prime the mind with a few questions.

The first I have is about rollbacks:

$ nix-env --upgrade some-packages
$ nix-env --rollback

What exactly does it mean to roll back a package?

I remember from the glossary that the user environment is a bunch of symlinks to executables. Presumably rolling back packages means changing the symlinks back. But how does it know what they were before? I suspect the “Nix database” keeps track of this, but I will have to wait to see if I am right.

I then notice that everything in the manual seems to use the “old” style of commands. One of the reasons I was excited to return to Nix was the prospect of a more user-friendly command-line interface – the nix executable. So I’m surprised that the manual, at least in the introduction, doesn’t use it. All the examples are of the “old style” commands, like nix-env and nix-shell.

I suspect this is just documentation rot, but Nix 2.0 came out in the beginning of 2018, and I’m writing this in 2021, so if I got the math right that’s not super reassuring. Either the rest of the manual is going to be quite outdated as well, or the nix interface is still not considered stable enough for real world use. I suppose I will find out in the rest of the manual.

Moving on, we get to “transparent source/binary deployment,” which basically says that prebuilt binaries will be fetched from https://cache.nixos.org/. It doesn’t say why or how to configure that – I assume I can set up my own custom cache? That domain is not magical, I hope? This is of course fine for an introduction, but something that I will need to follow up on for my own understanding.

We see a brief introduction to nix-shell, which includes our first truly confounding command, which is not explained at all:

$ nix-shell '<nixpkgs>' -A pan

(pan, in case you aren’t following along with the manual at home, is an example package – a Gnome Usenet client, I guess.)

What is -A? Elsewhere in the manual there they use long flags, but here we get an abbreviation, which is a pet peeve of mine to see in documentation, so I will expand it for you:

$ nix-shell '<nixpkgs>' --attr pan

And it’s no more clear. We want to start a shell with pan installed, but we specify it as --attr? Huh?

I think this is a great example where there is a disconnect between the way that I think about things (“I want a shell with this package in it”) and what Nix needs me to say (“I want a shell with this particular named symbol, or maybe record element or something, which I call an attribute, to be available, and because that attribute refers to a derivation or something that package will ultimately be installed.") I am, obviously, guessing here, helped only by a very shaky memory. Maybe that’s not what it means.

More confusing is the <nixpkgs> line. What do those angle brackets mean?1 Why is that an anonymous argument, when --attr was named?

I vaguely recall that this is some sort of import or open statement or something, which includes the definition of the symbol (attribute?) pan. But I have no idea what the nixpkgs string actually refers to: is that a filename somewhere? Is it the name of a channel? I don’t think I’ve forgotten this: I think I never really understood what that meant.

Chapter 2. Quick Start

This chapter begins with a great line:

This chapter is for impatient people who don’t like reading documentation.

Being the exact opposite of that person, I wondered if I should skip it, but decided that would be foolish. This is probably the most commonly read section in the manual. If anything should be improved to make Nix adoption easier, it’s this.

Installation

First we see the install instructions:

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

I think I speak for all of us when I say: really? Yeah, this is what Homebrew does, but isn’t Nix like… hardcore? Shouldn’t we be looking down our noses at people who suggest we run code that we found on the internet? It’s not like we’re about to give this software total control over our computer to do whatever it okay fine.

I am pleased to see that the main install page at least gives instructions for how to verify that the installation script has not been tampered with:

$ curl -o install-nix-2.3.10 https://releases.nixos.org/nix/nix-2.3.10/install
$ curl -o install-nix-2.3.10.asc https://releases.nixos.org/nix/nix-2.3.10/install.asc
$ gpg2 --recv-keys B541D55301270E0BCF15CA5D8170B4726D7198DE
$ gpg2 --verify ./install-nix-2.3.10.asc
$ sh ./install-nix-2.3.10

Probably. I mean, who knows? Has anyone ever actually done that? Have you really? Do you even have gpg2 installed? Don’t you lie to me.

So it’s probably fine. You can see that this script just downloads another script and executes that, and like, I’m not gonna read that script too. I’ve got docs to read.

So let’s try it out:

$ bash <(curl -L https://nixos.org/nix/install)
(some curl progress)
Note: a multi-user installation is possible. See https://nixos.org/nix/manual/#sect-multi-user-installation

Installing on macOS >=10.15 requires relocating the store to an apfs volume.
Use sh <(curl -L https://nixos.org/nix/install) --darwin-use-unencrypted-nix-store-volume or run the preparation steps manually.
See https://nixos.org/nix/manual/#sect-macos-installation

Okay. That did not work, and redirected me to a couple of different manual sections. I don’t particularly care about a multi-user installation on my laptop – I am the only user – so I skipped that and went on to read about the macOS problem.

And I learn that apparently I cannot just make /nix anymore.

$ mkdir /nix
mkdir: /nix: Read-only file system

They ain’t lyin'.

So that’s not great. The manual provides a few options to get around this:

  1. Change the Nix store path prefix

Which means forgoing binary caches, which makes this an absolute nonstarter for me. Ain’t nobody got time for that.

  1. Use a separate encrypted volume

This sounds fine, but annoying, as certain things won’t be available immediately after booting, which could cause weird failures if I have software configured to run immediately. But I hardly ever reboot, so this doesn’t sound that bad.

  1. Symlink the Nix store to a custom location

The manual basically just says “this don’t work.”

  1. Use a separate unencrypted volume

This is given as the recommended command above, but the manual explains what it is and how to do it.

I have to say, I’m incredibly impressed by this manual section. This is great. It makes me feel good; it puts me at ease; it gives me reassurance that the people behind Nix spent time thinking about this problem and the best ways to solve it.

And having read their recommendation, I concur with their conclusion, and will run the recommended command:

$ sh <(curl -L https://nixos.org/nix/install) --darwin-use-unencrypted-nix-store-volume

Uninstallation is now more complicated than rm -r /nix, but the script tells me what I need to do. Because I will definitely forget and am not sure if these steps will be documented anywhere else, I write them down in my blog:

Creating volume and mountpoint /nix.

     ------------------------------------------------------------------
    | This installer will create a volume for the nix store and        |
    | configure it to mount at /nix.  Follow these steps to uninstall. |
     ------------------------------------------------------------------

  1. Remove the entry from fstab using 'sudo vifs'
  2. Destroy the data volume using 'diskutil apfs deleteVolume'
  3. Remove the 'nix' line from /etc/synthetic.conf or the file

Unfortunately, the script fails shortly after that.

error: refusing to create Nix store volume because the boot volume is
       FileVault encrypted, but encryption-at-rest is not available.
       Manually create a volume for the store and re-run this script.
       See https://nixos.org/nix/manual/#sect-macos-installation

I don’t know what that means. What is encryption-at-rest? I do not know. The manual mentioned something about “T2 chips,” but I don’t know what that means either. It sure sounds futuristic, though, and my laptop is still powered by steam, so I’d be very surprised if I have one of those.

I try to create the volume manually, following the instruction in the manual, but that too gives me an error:

$ sudo diskutil apfs addVolume diskX APFS 'Nix Store' -mountpoint /nix
Could not find APFS Container Reference diskX

Boo. What looked like a command to run was in fact the template for a command to run, which I did not know because I don’t know the first thing about the diskutil CLI.

diskutil list tells me I have disk0 and disk1. Which one is the elusive diskX? I don’t know.

$ diskutil list
/dev/disk0 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *121.3 GB   disk0
   1:                        EFI EFI                     209.7 MB   disk0s1
   2:                 Apple_APFS Container disk1         121.1 GB   disk0s2

/dev/disk1 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +121.1 GB   disk1
                                 Physical Store disk0s2
   1:                APFS Volume Macintosh HD - Data     68.8 GB    disk1s1
   2:                APFS Volume Preboot                 365.1 MB   disk1s2
   3:                APFS Volume Recovery                613.7 MB   disk1s3
   4:                APFS Volume VM                      3.2 GB     disk1s4
   5:                APFS Volume Macintosh HD            19.5 GB    disk1s5
   6:              APFS Snapshot com.apple.os.update-... 19.5 GB    disk1s5s1

man diskutil tells me that the first argument to addVolume should be the containerReferenceDevice. I make a weak pattern-matching guess that that should be disk1, and hope that I’m not about to brick my computer:

$ sudo diskutil apfs addVolume disk1 APFS 'Nix Store' -mountpoint /nix
Password: hunter2
Will export new APFS Volume "Nix Store" from APFS Container Reference disk1
Started APFS operation on disk1
Preparing to add APFS Volume to APFS Container disk1
Creating APFS Volume
Created new APFS Volume disk1s7
Mounting disk
Setting volume permissions
Disk from APFS operation: disk1s7
Finished APFS operation on disk1

That looks… good? I guess? diskutil list now includes my Nix Store volume. My computer did not start emitting smoke or noxious fumes. Let’s re-run the install command:

$ sh <(curl -L https://nixos.org/nix/install) --darwin-use-unencrypted-nix-store-volume
(buncha curl stuff)
Using existing 'Nix Store' volume
Configuring /etc/fstab...
123
164
performing a single-user installation of Nix...
copying Nix to /nix/store.............................................
installing 'nix-2.3.10'
building '/nix/store/h37hlzjs8grkr2d6zzl8yv9j459k0zy2-user-environment.drv'...
created 7 symlinks in user environment
installing 'nss-cacert-3.49.2'
building '/nix/store/z1mna9c241jqw3raldq0cpqi6z9f9zr9-user-environment.drv'...
created 9 symlinks in user environment
unpacking channels...
created 1 symlinks in user environment
modifying /Users/ian/.zshenv...

Installation finished!  To ensure that the necessary environment
variables are set, either log in again, or type

  . /Users/ian/.nix-profile/etc/profile.d/nix.sh

in your shell.

And it worked! 123 164!

I am now the proud owner of nix-2.3.10.

I don’t love that it modified .zshenv for me, though my inner pedant is reluctantly pleased that it didn’t modify .zshrc instead. The only modification is to source that script it mentions, so let’s go through that and see if we understand what it’s doing.

if [ -n "$HOME" ] && [ -n "$USER" ]; then

    # Set up the per-user profile.
    # This part should be kept in sync with nixpkgs:nixos/modules/programs/shell.nix

    NIX_LINK=$HOME/.nix-profile

    # Append ~/.nix-defexpr/channels to $NIX_PATH so that <nixpkgs>
    # paths work when the user has fetched the Nixpkgs channel.
    export NIX_PATH=${NIX_PATH:+$NIX_PATH:}$HOME/.nix-defexpr/channels

    # Set up environment.
    # This part should be kept in sync with nixpkgs:nixos/modules/programs/environment.nix
    export NIX_PROFILES="/nix/var/nix/profiles/default $HOME/.nix-profile"

    # Set $NIX_SSL_CERT_FILE so that Nixpkgs applications like curl work.
    if [ -e /etc/ssl/certs/ca-certificates.crt ]; then # NixOS, Ubuntu, Debian, Gentoo, Arch
        export NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
    elif [ -e /etc/ssl/ca-bundle.pem ]; then # openSUSE Tumbleweed
        export NIX_SSL_CERT_FILE=/etc/ssl/ca-bundle.pem
    elif [ -e /etc/ssl/certs/ca-bundle.crt ]; then # Old NixOS
        export NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt
    elif [ -e /etc/pki/tls/certs/ca-bundle.crt ]; then # Fedora, CentOS
        export NIX_SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt
    elif [ -e "$NIX_LINK/etc/ssl/certs/ca-bundle.crt" ]; then # fall back to cacert in Nix profile
        export NIX_SSL_CERT_FILE="$NIX_LINK/etc/ssl/certs/ca-bundle.crt"
    elif [ -e "$NIX_LINK/etc/ca-bundle.crt" ]; then # old cacert in Nix profile
        export NIX_SSL_CERT_FILE="$NIX_LINK/etc/ca-bundle.crt"
    fi

    if [ -n "${MANPATH-}" ]; then
        export MANPATH="$NIX_LINK/share/man:$MANPATH"
    fi

    export PATH="$NIX_LINK/bin:$PATH"
    unset NIX_LINK
fi

Well, that’s a lot of things. Let’s try to go through the important bits.

# Append ~/.nix-defexpr/channels to $NIX_PATH so that <nixpkgs>
# paths work when the user has fetched the Nixpkgs channel.
export NIX_PATH=${NIX_PATH:+$NIX_PATH:}$HOME/.nix-defexpr/channels

No idea what this means. No idea what NIX_PATH is or why it’s being appended to when, as far as I know, it does not currently exist. Why not set it to that? I guess to not be destructive if the user configures something in their own .zshenv. But what would I configure it to? What is NIX_PATH?

Beyond that, what is $HOME/.nix-defexpr? It just contains a couple symlinks:

$ tree ~/.nix-defexpr
/Users/ian/.nix-defexpr
├── channels -> /nix/var/nix/profiles/per-user/ian/channels
└── channels_root -> /nix/var/nix/profiles/per-user/root/channels

No idea what the purpose of those are. Apparently this has something to do with the mysterious <nixpkgs> syntax we saw in the nix-shell example, but I can’t begin to guess what. I wonder what defexpr means: “def” means “definition” to me, but together I’m leaning towards “default expression.” If so, I wish that this were called ~/.nix-default-expression. I wonder what that means, so I will add it as an open question.

Next:

# Set up environment.
# This part should be kept in sync with nixpkgs:nixos/modules/programs/environment.nix
export NIX_PROFILES="/nix/var/nix/profiles/default $HOME/.nix-profile"

Okay. Neat. I don’t know what it means about keeping this in sync with something, nor do I really recognize nixpkgs:nixos/modules/programs/environment.nix as, like, a path to something. Is nixpkgs: a namespace here? Is this a file? It reads like a protocol to me. But it’s gotta be a file, right? I don’t know.

Next there’s a long section around configuring SSL certificates.

I have a strong recollection of SSL certs not working when I used Nix before, which made every nix-env invocation fail, I think – or maybe just nix-channel commands? – I don’t remember. I had to do some nonsense to set some environment variable manually (presumably NIX_SSL_CERT_FILE, but I don’t actually remember). So maybe these lines are new since I last tried Nix? Or they weren’t working on OS X? I don’t know. We shall see if they work on macOS now.

Then the script sets up PATH and MANPATH. No objection to any of this. Let’s source it.

$ source ~/.nix-profile/etc/profile.d/nix.sh

Isn’t that nicer to read than the . syntax? I think so. There’s probably some ancient reason to prefer . over source if you’re running Plan 9 on a Commodore 64 or whatever people used to do before Steve Jobs came along and saved us all from having to know anything about computers.

Anyway, everything is working great now. Nix is installed!

First steps

The quick start goes on to describe some simple commands to try. Let’s start with the first one, to “see what installable packages are currently available in the channel”:

$ nix-env -qa

This command appears to just hang. No output at all. Is it working? Does it do anything? Ah, after thirty seconds or so it gives me a big list of words in a pager. How many words? Countless thousands. Actually 32,544. That’s too many!

As my first ever real live Nix command, I gotta say… this is not a very good showcase. I do not care to see the 32,544 packages that make up the Nix ecosystem. I care to see, like, a couple dozen of them, tops.

But still, sure, okay. The manual authors wanted to highlight this command; I should at least understand it.

$ nix-env -qa

The -q flag is for “query” and not “quiet” as I would assume from every other CLI I have ever used. Similarly a does not stand for --all but --available. The man page explains:

OPERATION --QUERY
   Synopsis
       nix-env {--query | -q} [--installed | --available | -a]
               [{--status | -s}] [{--attr-path | -P}] [--no-name] [{--compare-versions | -c}] [--system] [--drv-path] [--out-path]
               [--description] [--meta]
               [--xml] [--json] [{--prebuilt-only | -b}] [{--attr | -A} attribute-path]
               names...

   Description
       The query operation displays information about either the store paths that are installed in the current generation of the active
       profile (--installed), or the derivations that are available for installation in the active Nix expression (--available). It only
       prints information about derivations whose symbolic name matches one of names.

       The derivations are sorted by their name attributes.

   Source selection
       The following flags specify the set of things on which the query operates.

       --installed
           The query operates on the store paths that are installed in the current generation of the active profile. This is the default.

       --available, -a
           The query operates on the derivations that are available in the active Nix expression.

So my query was for “derivations that are available in the active Nix expression” – whatever that means – and I didn’t have an actual query, so I got back everything. Okay. Let’s try it with a string:

$ nix-env -qa git
git-2.30.0
git-2.30.0
git-2.30.0

After thirty more seconds, I get the same package, three times. All the same version. Huh. Okay. That’s weird.

In my head and from my memory, this command is analogous to brew search. I can run brew search git and get back not just git, but lots of packages with the string “git” in the name, like git-annex and something called gitmoji. Are these packages not available in Nix?

$ nix-env -qa git-annex
git-annex-8.20210127

Huh? Okay. So I guess… it’s not really searching? The string I’m giving it is something else?

Well. Weird. But we all know that Nix traditionally has a pretty confusing – if not outright hostile – user interface. But that’s one of the big reasons we’re returning to it now, with Nix 2! We have new commands! So let’s try one now.

$ nix search git
warning: using cached results; pass '-u' to update the cache
error: no results for the given search term(s)!

Um. Okay. I suppose that since I’ve never run this before, my cache is empty? And it searched it anyway, instead of auto-updating it? Sure.

$ nix search git -u
* nixpkgs.act (act-0.2.20)
  Run your GitHub Actions locally

* nixpkgs.argocd (argocd-1.8.4)
  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.extraRule
s</literal>: <literal>SUBSYSTEM=="usb", ATTR{idVendor}=="16c0", ATTR{idProduct}=="0442", OWNER="someuser", GROUP="somegroup"</literal>

(much more output elided)

It took the same amount of time, but the output I got was leaps and bounds better than with nix-env -qa. Not only did it search package names, but also searched and displayed package descriptions – although with some apparent escaping issues.

It’s also very nicely colorized in ways that don’t show up here – my search term is highlighted wherever it appears, the scary computer version of the package (axoloti-1.0.12-2) is nicely grayed out so that it doesn’t distract my eye. It looks great. And I love that they’re all namespaced with nixpkgs – that makes me feel good; that makes things feel less magical. It makes me feel heard.

Scrolling through the output I see all kinds of git-related packages, including git-annex – although, tragically, there is no gitmoji.

So, okay. It might be good if nix search was the first command you run, instead of nix-env -qa. It’s familiar. It does the thing you expect. It leaves a good first impression – apart from having to re-run it with -u. But it told me that! It didn’t just silently give me nothing! The fact that I was able to use it successfully without googling anything means that the UI is working, even if my experience could have been a little smoother.

But doing this highlighted a weird and unexpected thing. I can run man nix-env and learn that the expanded version of that command is nix-env --query --available, which is easier to read. But there is no man nix. Weird. I want to learn more about nix search -u, so I run:

$ nix help search
error: 'help' is not a recognised command
Try 'nix --help' for more information.

Because that’s how Homebrew works. And I am very pleased that it told me the correct command. So I run:

$ nix search --help

And see that -u stands for --update-cache. Perfect. Love it. Super explicit. I was worried it would just stand for --update, and I would have something to grumble about.

All-in, nix search: five stars. nix-env -qa: three wats.

Let’s keep going.

The next command is listed as “install some packages from the channel.” I still don’t know what a “channel” is, but okay.

$ nix-env -i hello

Is -i short for --interactive? --ignore? No: it’s --install, another “subcommand” of nix-env. I’ve gotta say, I really like the convention of subcommands not being flags. I’m glad we landed there. The world is a much better place now than it once was.

Anyway, let’s run it.

It… is slow. Probably another 30 seconds before it starts printing anything. I’m starting to sense a pattern.

$ nix-env -i hello
installing 'hello-2.10'
these paths will be fetched (14.84 MiB download, 74.75 MiB unpacked):
  /nix/store/2s8x59z3cly97fyc3hvlngl637snpwcq-swift-corefoundation
  /nix/store/541lzmhppr82600vz7ap54n22dz7gydf-ICU-66108
  /nix/store/5y6lcfjghk5kbv4782vi7w79vz60gsyn-bash-4.4-p23
  /nix/store/aqf78c2x1kwv9i123p55m3f4iyx9fz3c-openssl-1.1.1i
  /nix/store/c1ln5y1p009wn44j03z1r7rdhmjx3k2j-curl-7.74.0
  /nix/store/mw590ax8x2n9161hwvnl9j5156di6d47-libc++abi-7.1.0
  /nix/store/pakmb65sf3g2hkbm1fdgk2fh6hiij720-hello-2.10
  /nix/store/pqajcmw6jmq2i8ka001z53r1a09w4y67-libssh2-1.9.0
  /nix/store/q1rhw9f30fm0yzx1hic1hgj31sfc6k4p-libkrb5-1.18
  /nix/store/s5rd3hgdirrdwdfbn7m4hd0i3d2zqpz5-libxml2-2.9.10
  /nix/store/vrmgqjl51gwf47i5i1rbs7dnkb1g1pvf-libc++-7.1.0
  /nix/store/wphpzw2swy36pm4ph3r1zfvwfj2njxjf-zlib-1.2.11
  /nix/store/yi9klhxd1243l7inrkn12f6lwzp9bki4-Libsystem-1238.60.2
  /nix/store/zxya6i6ncqs8q6fq3mcl0igflmy2219n-nghttp2-1.41.0-lib
copying path '/nix/store/yi9klhxd1243l7inrkn12f6lwzp9bki4-Libsystem-1238.60.2' from 'https://cache.nixos.org'...
copying path '/nix/store/5y6lcfjghk5kbv4782vi7w79vz60gsyn-bash-4.4-p23' from 'https://cache.nixos.org'...
copying path '/nix/store/mw590ax8x2n9161hwvnl9j5156di6d47-libc++abi-7.1.0' from 'https://cache.nixos.org'...
copying path '/nix/store/q1rhw9f30fm0yzx1hic1hgj31sfc6k4p-libkrb5-1.18' from 'https://cache.nixos.org'...
copying path '/nix/store/vrmgqjl51gwf47i5i1rbs7dnkb1g1pvf-libc++-7.1.0' from 'https://cache.nixos.org'...
copying path '/nix/store/zxya6i6ncqs8q6fq3mcl0igflmy2219n-nghttp2-1.41.0-lib' from 'https://cache.nixos.org'...
copying path '/nix/store/541lzmhppr82600vz7ap54n22dz7gydf-ICU-66108' from 'https://cache.nixos.org'...
copying path '/nix/store/aqf78c2x1kwv9i123p55m3f4iyx9fz3c-openssl-1.1.1i' from 'https://cache.nixos.org'...
copying path '/nix/store/wphpzw2swy36pm4ph3r1zfvwfj2njxjf-zlib-1.2.11' from 'https://cache.nixos.org'...
copying path '/nix/store/pqajcmw6jmq2i8ka001z53r1a09w4y67-libssh2-1.9.0' from 'https://cache.nixos.org'...
copying path '/nix/store/s5rd3hgdirrdwdfbn7m4hd0i3d2zqpz5-libxml2-2.9.10' from 'https://cache.nixos.org'...
copying path '/nix/store/c1ln5y1p009wn44j03z1r7rdhmjx3k2j-curl-7.74.0' from 'https://cache.nixos.org'...
copying path '/nix/store/2s8x59z3cly97fyc3hvlngl637snpwcq-swift-corefoundation' from 'https://cache.nixos.org'...
copying path '/nix/store/pakmb65sf3g2hkbm1fdgk2fh6hiij720-hello-2.10' from 'https://cache.nixos.org'...
building '/nix/store/dgm6dvfqzvk0jf4r0qn3sbrfp8nn00w3-user-environment.drv'...
created 40 symlinks in user environment

Neat! 40 symlinks! For one command! Why!

So I look in ~/.nix-profile/bin/ – the only Nix-flavored directory on my PATH – and find just one new symlink: hello. So that’s good. Not sure where the other 39 symlinks got off to. Kind of a weird upsetting message, to be honest. But at least I now have hello.

$ hello
Hello, world!

Excellent.

It then tells me how to uninstall a package, with – you know what? I’m not going to tell you.

I want you to guess. nix-env -u, for uninstall? nix-env -r, for remove? nix-env -d, for delete?

No. None of this. It’s nix-env -e. The -e is short for, according to the manual, --uninstall. I am not making this up.

I sort of refuse to use nix-env -e on principal, and instead run it like this:

$ nix-env --uninstall hello
uninstalling 'hello-2.10'
building '/nix/store/29hrprfz6bn7qnn39a5xjw3mm563xbxs-user-environment.drv'...
created 9 symlinks in user environment

What? You already created 40 symlinks to install it, you need 9 more just to uninstall it? This seems… it seems like this message is telling me something very different from what I assume it is telling me.

But it worked!

$ hello
zsh: command not found: hello

Rest in peace, friend.

Fortunately, the next example is to “test out” a command by opening a shell that has the command available, without actually installing it (er, creating symlinks to it, I guess).

That looks like this (quite different from the pan example we saw before):

$ nix-shell -p hello

And this installed way more than when I just ran nix-env -i. Like, way more. I won’t even include the output of the command because there was so much of it. For scale, this told me (138.31 MiB download, 695.33 MiB unpacked) vs (14.84 MiB download, 74.75 MiB unpacked) for nix-env -i. And I assume that hello itself was still around – that uninstalling it didn’t actually delete the files, because I remember that much from years ago.

So that’s all… overhead? Shell overhead? It’s not just bash? I do not know. The shell does seem to work, though:

[nix-shell:~]$ hello
Hello, world!

Excellent.

Subsequent invocations of nix-shell -p hello don’t download anything, but the command still has to think for a solid 2 or 3 seconds before I get into my shell. Which is annoying.

So, okay. These seem like pretty important, common commands. Let’s see how they behave with the nix command line.

$ nix install hello
error: 'install' is not a recognised command
Try 'nix --help' for more information.

Huh.

I look through nix --help, but nothing sounds like install to me. There’s build? add-to-store? These don’t really sound like install. So I guess… nix-env is still the only way to do this? Look for yourself:

$ nix --help
Usage: nix <COMMAND> <FLAGS>... <ARGS>...

Common flags:
      --debug                  enable debug output
      --help                   show usage information
      --help-config            show configuration options
      --no-net                 disable substituters and consider all previously downloaded files up-to-date
      --option <NAME> <VALUE>  set a Nix configuration option (overriding nix.conf)
  -L, --print-build-logs       print full build logs on stderr
      --quiet                  decrease verbosity level
  -v, --verbose                increase verbosity level
      --version                show version information

In addition, most configuration settings can be overriden using '--<name> <value>'.
Boolean settings can be overriden using '--<name>' or '--no-<name>'. See 'nix
--help-config' for a list of configuration settings.

Available commands:
  add-to-store     add a path to the Nix store
  build            build a derivation or fetch a store path
  cat-nar          print the contents of a file inside a NAR file
  cat-store        print the contents of a store file on stdout
  copy             copy paths between Nix stores
  copy-sigs        copy path signatures from substituters (like binary caches)
  doctor           check your system for potential problems
  dump-path        dump a store path to stdout (in NAR format)
  edit             open the Nix expression of a Nix package in $EDITOR
  eval             evaluate a Nix expression
  hash-file        print cryptographic hash of a regular file
  hash-path        print cryptographic hash of the NAR serialisation of a path
  log              show the build log of the specified packages or paths, if available
  ls-nar           show information about the contents of a NAR file
  ls-store         show information about a store path
  optimise-store   replace identical files in the store by hard links
  path-info        query information about store paths
  ping-store       test whether a store can be opened
  repl             start an interactive environment for evaluating Nix expressions
  run              run a shell in which the specified packages are available
  search           query available packages
  show-config      show the Nix configuration
  show-derivation  show the contents of a store derivation
  sign-paths       sign the specified paths
  to-base16        convert a hash to base-16 representation
  to-base32        convert a hash to base-32 representation
  to-base64        convert a hash to base-64 representation
  to-sri           convert a hash to SRI representation
  upgrade-nix      upgrade Nix to the latest stable version
  verify           verify the integrity of store paths
  why-depends      show why a package has another package in its closure

Note: this program is EXPERIMENTAL and subject to change.

Boo. I’m actually pretty surprised by this. Nix 2 brings this new fancy interface, but it doesn’t bother to include a command to install packages? In a package manager? This is… this does not bode well. I’m not sure how much Nix I can Nix if I have to type nix-env -e ever again. I realize there are no laws around user ergonomics for command line tools, but, like, doesn’t that feel a little bit like a violation of some kind of inalienable right?

I do see that there’s a nix run command. Let’s try that, as the description implies it’s like nix-shell:

$ nix run hello
error: attribute 'hello' in selection path 'hello' not found

I don’t know what that means but it’s vaguely frightening. Ah, but the --help text has an example that gets me there:

$ nix run nixpkgs.hello

The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
bash-3.2$ hello
Hello, world!

Wha? This is a very different shell than nix-shell. And for some reason tells me something about zsh. zsh has been my default interactive shell for… ever. The link to an Apple support article makes me think this invoked the system bash? But that doesn’t sound like something nix would do. That’d be crazy. I guess all macOS builds link to an Apple support article? Weird. It prints it every time I use nix run, too! It’s not just a first-run message. So… gross. This command seems worse than nix-shell.

Let’s move on. Next we learn how “to keep up-to-date with the channel.” Still no idea what a channel actually is.

$ nix-channel --update nixpkgs
$ nix-env -u '*'

The manual clarifies:

The latter command will upgrade each installed package for which there is a “newer” version (as determined by comparing the version numbers).

Okay. So I guess nix-env supports globs. Maybe that would make my nix-env -qa git search work.

$ nix-env -qa '*git*'
error: An empty regex is not allowed in the POSIX grammar.

Amazingly, this still took 30 seconds to print that error. Which is a syntax error! What is that command doing?

I dunno. But let’s try updating my channel:

$ nix-channel --update nixpkgs
unpacking channels...

Pretty quick! No idea if it… did anything. I only installed Nix like an hour ago, so I assume there’s nothing to update yet.

This is analogous to brew update, if I recall correctly: it refreshes the local cache of what packages are available. Meanwhile nix-env -u (or nix-env --upgrade) is analogous to brew upgrade.

For fun, I run:

$ nix-env --install hello
installing 'hello-2.10'
$ nix-env --upgrade '*'
upgrading 'nss-cacert-3.49.2' to 'nss-cacert-3.60'
these paths will be fetched (0.22 MiB download, 0.41 MiB unpacked):
  /nix/store/b7ykp2bibwrhgf67ql9dq1yfyy8a8h3a-nss-cacert-3.60-unbundled
  /nix/store/ki7gssifc0xracrah8ygm63xj23wkjdz-nss-cacert-3.60
copying path '/nix/store/ki7gssifc0xracrah8ygm63xj23wkjdz-nss-cacert-3.60' from 'https://cache.nixos.org'...
copying path '/nix/store/b7ykp2bibwrhgf67ql9dq1yfyy8a8h3a-nss-cacert-3.60-unbundled' from 'https://cache.nixos.org'...
building '/nix/store/gmh97cqlq90cn7j7jsacsaaram1g7pxf-user-environment.drv'...
created 40 symlinks in user environment

Each of those commands still takes 30 seconds every time.

Impressively, I did have something to upgrade, even in so short a time. So I got to see that the command did, in fact, work. I now suspect that the “created 40 symlinks in user environment” is like… the total number? Not 40 new symlinks, but it’s like building a whole new user environment every time?

Let’s find out: the next example command is a rollback.

$ nix-env --rollback
switching from generation 6 to 5

That happened instantly. How? What did it actually do? How did it know what to restore? Is anything actually different? Let’s find out.

$ nix-env --upgrade --dry-run '*'
(dry run; not doing anything)
upgrading 'nss-cacert-3.49.2' to 'nss-cacert-3.60'

30 seconds later… alright! Okay. So that worked.

I wonder how a numbered generation works if I do something now – would that put me on generation 7? Or overwrite generation 6? Can I undo a rollback?

Let’s find out.

$ nix-env --uninstall hello
uninstalling 'hello-2.10'
$ nix-env --list-generations
   1   2021-02-20 11:00:07
   2   2021-02-20 11:00:07
   3   2021-02-20 12:44:56
   4   2021-02-20 12:51:54
   5   2021-02-20 13:32:32
   6   2021-02-20 13:33:58
   7   2021-02-20 13:39:57   (current)
$ nix-env --switch-generation 6
switching from generation 7 to 6
$ nix-env --upgrade --dry-run '*'
(dry run; not doing anything)
$ hello
Hello, world!

Cool! Okay. Very cool. I got those commands from man nix-env under the --rollback command – they are not listed in the quick start guide.

Lastly, we come to:

$ nix-collect-garbage -d

-d is short for --delete-old, which deletes packages that are no longer needed. Is the default garbage collection to… keep packages that are no longer needed? I expect it will delete the old nss-cacert-3.49.2 when I run it. Let’s find out.

Holy gosh. I’m not sure what all it printed because it blew out my scrollback printing so much. It ended with:

1788 store paths deleted, 700.10 MiB freed

Wow. That’s quite a lot of garbage. Considering, you know, I just got here. I just installed hello. I don’t know what all that mess is. I certainly didn’t put it there.

I suspect that it was all the stuff that was installed when I ran nix-shell -p hello, so I run that again. And yep, sure enough, now it has to re-download all… whatever it was that it downloaded. So that’s… annoying. That doesn’t really seem like garbage to me, if I have to re-download it every time I want to use nix-shell.

And just for good measure: no, there is no nice nix gc version of this command.

Early impressions

That’s the end of the quick start guide. I learned a lot. I started. Not particularly quickly.

My main takeaway from this experience? nix-env invocations are too slow for anyone to reasonably use Nix as a package manager. Full stop. Every. Single. Command. 30 seconds. It was unbearable. I mean, it was nice for writing this blog post – plenty of time to get my thoughts down – but if I were an actual, normal user? Trying to use Nix? I would just stop. My Nix adventure would end before I finished running the first command. Because I would assume it was broken, ctrl-C outta there, and go back to using Homebrew (or pacman, or apt, or whatever).

I don’t remember this being such a huge issue before. I do remember using nox to actually manage packages – it offered a fuzzy search a little bit like nix search, and it provided a better interface for installing stuff. But I don’t remember it taking 30 seconds to do anything. Did I just block this out? Did I use nox for so long that I just never had to experience the joy of running bare nix-env commands? That’s crazy. This is Nix 2 land, the land of making things better. Surely we aren’t still supposed to use nox? Right? That would be crazy, right?

To test if this is some weird problem with my multi-volume setup in macOS, I got on my NixOS box and ran time nix-env -i hello. If it finishes before I publish this blog post, I’ll let you know.

Oh, there it goes:

42.03s user 22.34s system 26% cpu 3:58.80 total

Four minutes. Super.

Look, this is truly insane, and at this point I was actually mystified at how Nix is even a thing. But I’m writing here to say: it gets better. There’s a better way. We just have to get slightly outside of the quick start guide to see it.

Chapter 8. Upgrading Nix

Yes, we went from chapter 3 to chapter 8. It’s not actually a big jump: chapters 4-6 are about installation; I already read the salient bits. Chapter 7 is about environment variables, but doesn’t really explain anything about them, just tells you what to set to get things working – things that the install script already did.

Chapter 8 is about upgrading Nix, and it is two sentences long. One sentence is about single-user installs, and the other is about multi-user installs.

I did a single-user install, so to me upgrading looks like this:

nix-env -iA nixpkgs.nix

And that blew the case wide open.

It’s a little weird: it uses a qualified path, which the quick start guide didn’t. It’s weird that it’s an install, not an upgrade – I would have expected just nix-env -u nix. If --install performs an upgrade, why is there a separate --upgrade? Could nix-env drop that, and free up -u for --uninstall? I’m still not over nix-env -e.

Anyway, I read man nix-env to see what the -A means.

If --attr (-A) is specified, the arguments are attribute paths that select attributes from the top-level Nix expression. This is faster than using derivation names and unambiguous.

Faster? Faster, you say?

$ nix-env -iA nixpkgs.nix
replacing old 'nix-2.3.10'
installing 'nix-2.3.10'

Wow! Basically instant, in fact. No 30 second sleep. It just did stuff.

But this made me wonder: are there “fast” ways to install regular packages? Or is this some special thing about upgrading nix?

For science, I timed what I’ve been dealing with this whole time:

$ time nix-env --install hello
installing 'hello-2.10'
nix-env --install hello  14.08s user 5.51s system 60% cpu 32.211 total

My 30 second guesstimates have actually been pretty close. Nice.

And what about…

$ time nix-env -iA nixpkgs.hello
installing 'hello-2.10'
nix-env -iA nixpkgs.hello  0.42s user 0.10s system 69% cpu 0.759 total

What? Are you kidding me? Why – why is the other thing even a thing? Why are there two ways to install packages, when one of them is short but terrible and one of them is slightly more to type but usable? Why isn’t there a short and usable command? And isn’t that what the nix tool is supposed to be? Where is it in all of this? Is it complicit??

I’m sure there is some complicated reason for all of this – that without saying nixpkgs.hello it needs to like evaluate every single package in the entire package store – all 32,544 of them – and then do a linear scan for one called hello, or something ridiculous like that. Because how could it possibly guess that by hello I might mean nixpkgs.hello. But by using -A/--attr, it can just go to the right one? (???)

This is crazy. This is exactly the sort of thing that seems crazy to me, as a new user, but that I’m sure has some perfectly reasonable explanation to the Nix maintainers or power users who actually understand how nix-env works. “Of course you need to specify -A,” I can hear them saying. “Why would you ever use nix-env -i without -A?”

I am not being generous; I have no idea what the Nix community is actually like. I don’t think that I’ve ever been on the Nix IRC or Discord or mailing list or whatever; I’m sure they’re perfectly nice and would very patiently explain to me why things are the way that they are without judging me or making me feel inadequate for my lack of experience.

But.

I think I can say, just, like, as an objective truth: the quick start guide should not use the 30 second versions of these commands. It should use the instant versions. That’s just a bug in the documentation.

Maybe there wasn’t much of a difference between the two versions, back when the manual was written. Maybe the 30 second commands were 1 second commands, and the relative simplicity of nix-env -i hello versus nix-env -iA nixpkgs.hello outweighed that small delay. Maybe it got slower over time, and no one thought to update the guide.

I can definitely sympathize. Documentation is hard; that’s why I’m publishing this.

Anyway, I’m glad I kept reading before I gave up completely.


New open questions:

  • How do I configure the location of the Nix binary cache (or other “substituters,” if I recall the terminology correctly)?
  • What/why is nix-shell '<nixpkgs>' --attr pan?
  • What is NIX_PATH?
  • What does the weird comment mean about “appending channels” to NIX_PATH?
  • What is ~/.nix-defexpr?
  • What is ~/.nix-profile?
  • What is ~/.nix-channels?
  • What is NIX_PROFILES?
  • Why does nix-env -qa git print the same line three times?
  • Why doesn’t nix-env -qa behave like a search?
  • How do I search for packages by name using nix-env?
  • Why is there no man nix?
  • Why did nix-env -i hello tell me it “created 40 symlinks in user environment”?
  • How do I install commands using nix instead of nix-env?
  • Why does nix-shell download so many packages? What are they for?
  • Why does nix run need the nixpkgs qualifier when nix-shell doesn’t?
  • Why does every invocation of nix-env take 30 seconds before it does anything?
  • How do I collect garbage without making my next invocation of nix-shell -p hello redownload all the stuff it needs to be a shell?
  • Why is there an --upgrade command if --install will upgrade packages?
  • Why does the manual explicitly call out upgrading Nix? Will it not be upgraded through “normal” means? Do I have to upgrade it separately?

  1. This is a case where my past Nix experience may have saved me a bit of time. A friend read this blog post and made the following very good point: “surrounding a word in angle brackets is canonically the way you describe a like variable substitution in example code.” Which is true! And potentially very confusing! The fact that it’s in quotes hopefully makes this interpretation less likely, but maybe not I don’t know. It didn’t happen to trip me up, but maybe it would have five years ago. ↩︎