We open on a chat conversation that I had with my good friend and former colleague “Doug,” in which we were discussing my experience reading through the Nix manual.

 ian: there were certain things that i wanted to learn how to do
 ian: and i still don't really know how to do them
 ian: like
 ian: it seems like
 ian: you should just have a file
 ian: called "ian.nix"
 ian: where you write down the things you want
 ian: and then there should be a way to say "install exactly this set
      of things"
 ian: but there isn't
 ian: so i still want to figure out how to write that
 ian: anyway

At this point I intended to change the topic, because I wanted to talk to Doug about videogames or something. But the topic was not to be changed:

doug: isn't that command nix-env -irf ian.nix? or am i missing something
 ian: wut
 ian: oh
 ian: i have no idea
 ian: maybe!
doug: that was roughly my takeaway from my adventures

I pause, and think about it. I remember nix-env --set, and how I think I can use that if I can figure out how to write an expression that is a derivation for a user environment – one of those good old /nix/store/xxx-user-environment directories that we’ve seen many times.

I had spent a little time thinking about this already, and my intention was to read through the Nixpkgs manual, as I suspected that somewhere in that massive repo I would find some mkUserEnvironmentDerivation function.1 I could imagine how I would write this derivation myself – it’s basically just find $targets -mindepth 1 -exec ln -s {} $out \; (yes and a separate find to set up the directory structure and you’d need to get the relative path for the whatever I’m not getting sucked into this rabbit hole with you right now) – but there’s gotta be some built-in “official” way to do that. And if I could find that official way, I could probably learn a lot about how the environment actually gets set up.

Anyway. These thoughts pass through my head, and I reply succinctly:

 ian: i don't know how to write ian.nix
 ian: i guess
 ian: i will try things
 ian: and see what happens
 ian: no spoilers

But Doug is determined to spoil:

doug: have you considered a record? or a list? or maybe it doesn't matter
      and it's unclear why?

At this point I’m thinking “Oh, Doug doesn’t get it. See, nix-env -i requires a single derivation; I read the whole manual so I know that I need to give it an expression that resolves to a derivation.” I’m sure if I tried to “install” a list, that command invocation would just fail.

Or would it? I become unsure. There are no type signatures here. man nix-env’s section on --install seems to support my position: the documentation talks only about single derivations. But still: maybe? Dynamically typed languages love implicit overloading, right?

I politely disagree.

 ian: yeah that's i guess
 ian: i don't know WHY that would work
 ian: given my understanding of nix-env -i
doug: everything is lazy except for the thing where shit gets installed

Doug said a swear, and I apologize for repeating it. He didn’t know this was going to end up in a blog post.

 ian: but like i have always specified a single package
 ian: there's nothing about it being able to work on lists or sets or something
 ian: like is it going to recursively evaluate every derivation in
      any expression i give it?
 ian: that's insane and also probably exactly how it works
doug: my understanding is that it doesn't "work" on a list, it just evaluates
      the expression and the side-effect of doing that installs the packages
doug: and then discards the result

I meditate on this. It makes some sense, and actually tickles a bit of intuition that I remember from long ago: I remember thinking that Nix worked by evaluating expressions, and somehow collecting a list of derivations encountered during that evaluation – like a writer monad – and then installing them as a second pass once it had evaluated everything.

I had completely forgotten that I had this mental model before Doug said that, but it comes back to me. This was my intuition for how NixOS decided what packages it needed to install when I would change configuration.nix. I don’t think I ever thought to apply the intuition to nix-env, but maybe – it’s been a long time.

Still, that can’t be right. Because:

 ian: but that would mean that if you type nix-env -i it would install the
      whole world
 ian: which like

You know that scene at the end of Oldboy where you’re slowly realizing what’s happening right as the protagonist is slowly realizing what’s happening?

doug: yes
 ian: surely not
doug: oh yes
 ian: surely not

The curtain has been drawn back. I can see the entire scheme laid out before me. I am faced with a horror too great for me to comprehend. The mind rejects it.

 ian: oh man this is going in the post
 ian: this conversation

I respond flippantly, as I am wont to do.

doug: i mean my state was kind of fucked up the whole time
doug: so i might be remembering wrong or i typed something that i thought
      was right
 ian: i do remember you doing something and it was trying to install the
      whole world
 ian: but surely it wasn't THAT

At this point I have to apologize again for Doug’s language but also clarify something.

I had told Doug that I was writing this series before I had finished reading the Nix manual, and before I had published any of these blog posts. And he had pretty much the optimal response: “Oh, yeah, I did that once. Lemme send it to you.” He then proceeds to pull out his own Nix diary, a chronicle of his attempt to set up a brand new laptop using Nix as his primary package manager. (Sans NixOS, because he’s brave but not crazy.)

I had read through his diary when I was getting pretty close to the end of the Nix manual and thought it wouldn’t corrupt me too much. But I was sort of skimming it, and obviously overlooked this most fascinating of experiences.

Fortunately, he proceeded to quote from his own Nix diary:

doug: > But I don't know how to say "just go back to whatever the config is".
      > I thought nix-env -ir would be it (which is install and remove all,
      > equivalent to uninstalling everything first), but that seemed to...
      > want to install everything?

I trust him to remember this. But I also cannot accept it. I apply a heuristic to try to decide if this is true: technical Darwinism. If it were the case that typing nix-env -i would try to install every package in Nixpkgs, would Nix still exist? No: even if it ever worked that way, that is such an obvious bug that it would have been fixed years ago. Nix would not exist with that bug. And Nix still exists, so that bug must not exist. Right?

 ian: that's like
 ian: that's so
 ian: easy
 ian: to shoot yourself in the foot with

But the sad truth comes to light.

doug: https://github.com/NixOS/nix/issues/1334
doug: hahaha
doug: oh here's the original one https://github.com/NixOS/nix/issues/308
doug: 2014
doug: open
doug: > I've come to like this feature

And here we are.

I rush to get my thoughts down as quickly as I can, as I know that I will not remember the details of the conversational flow and my own thought processes come tomorrow. The date: March 4, 2021. The day that I published the first 4 posts in this series – which is why we were having this conversation in the first place.

A week later I pick this back up, and decide to actually get around to trying this. Because this conversation definitely shortened the time it would take me to figure this out, I included it above. Now we’re on the same page again. You are once again in sync with my Nix journey.

Because I don’t really want to mess up my own profile, I make a new one:

$ nix-env -iA nixpkgs.hello --profile ~/scratch/profile
installing 'hello-2.10'
these paths will be fetched (0.02 MiB download, 0.07 MiB unpacked):
  /nix/store/70pxcwpdiq7ddrk4w8axfl51s9xh9ahn-hello-2.10
copying path '/nix/store/70pxcwpdiq7ddrk4w8axfl51s9xh9ahn-hello-2.10' from 'https://cache.nixos.org'...
building '/nix/store/14p1q2z1shvjw6jkk5wd63l0bia0wx6v-user-environment.drv'...
created 2 symlinks in user environment
$ ll ~/scratch/profile*
/Users/ian/scratch/profile@ -> profile-1-link
/Users/ian/scratch/profile-1-link@ -> /nix/store/l8h2bfp2grx7c0ia3c4awmsf2m3666a0-user-environment
$ tree -l ~/scratch/profile
/Users/ian/scratch/profile/
├── bin -> /nix/store/70pxcwpdiq7ddrk4w8axfl51s9xh9ahn-hello-2.10/bin
│   └── hello
├── manifest.nix -> /nix/store/nhh2gf0cpiaw5rsnr4g3xnai1jflxmgx-env-manifest.nix
└── share -> /nix/store/70pxcwpdiq7ddrk4w8axfl51s9xh9ahn-hello-2.10/share
    ├── info
    │   └── hello.info
    └── man
        └── man1
            └── hello.1.gz

Neat. I wasn’t really sure if that would work.2

Now let’s try to make a “declarative” profile:

$ cat ian.nix
with import <nixpkgs> {}; [ hello git ]
$ nix-env -p ~/scratch/profile --set ian.nix
error: selector 'ian.nix' matches no derivations

Er, right. Uhh…

$ nix-env -p ~/scratch/profile --set 'import ./ian.nix'
error: getting status of '/Users/ian/scratch/import ./ian.nix': No such file or directory

???

I read the man nix-env and learn that --set only works if I provide the name of a package. So… I have no idea what to do about that. Not use --set, I guess. And here I thought… that it was anything? Nope. Let’s try it Doug’s way:

$ nix-env -p ~/scratch/profile -irf ian.nix
building '/nix/store/6mlp94f12lh71knbvki2qk4g8qrk8ala-user-environment.drv'...
created 186 symlinks in user environment

$ ls ~/scratch/profile/bin
git@                git-credential-netrc@       git-credential-osxkeychain@
git-cvsserver@      git-http-backend@           git-receive-pack@
git-shell@          git-upload-archive@         git-upload-pack@
hello@

Okay! Lotta symlinks. Mostly man pages for git. But… that was it! That was super easy.

Now I have no idea why that was super easy. I mean, I sort of do – some sort of mental image is forming here.

$ nix-env -qaf ian.nix
git-2.30.1
hello-2.10

So what it seems like nix-env is doing is recursively evaluating whatever expression you give it. Instead of just evaluating the argument you give it, it is “deeply” evaluating everything, and installing every derivation it can get its hands on – or so it seems.

Let’s test a few different things:

$ cat ian.nix
with import <nixpkgs> {}; [ [[hello] hello] { attribute = git; } ]
$ nix-env -qaf ian.nix
git-2.30.1
hello-2.10

$ nix-env -p ~/scratch/profile -irf ian.nix

Doesn’t seem to mind arbitrary nesting. Seems to deduplicate derivations. Evaluates to the exact same set, apparently, such that nix-env -irf gave me no output.

Let’s try something weirder:

with import <nixpkgs> {}; [ ({}: hello) { attribute = git; } ]

No problems with that. But if I try:

with import <nixpkgs> {}; [ (x: hello) { attribute = git; } ]
$ nix-env -qaf ian.nix
error: expression does not evaluate to a derivation (or a set or list of those)

Aha. Okay. And indeed, tossing other values in there – numbers, strings, nulls, etc – gives me the same error.

So okay – at least it’s like… at least it’s not just cherry-picking derivations out of an arbitrary tree. I can feel okay about this. Minus the part where it’s completely undocumented.3

Now I can write my own “real” ian.nix by basically copying the output of nix-env -q

I remember, though, that nix-env -q reports derivation names, but I need attribute names to put in my file. But these are almost always the same thing, which is super confusing – you don’t really need to think about this distinction 90% of the time but then sometimes something goes wrong and suddenly you do.

Anyway, this works great except for nss-cacert, which is apparently just called nixpkgs.cacert.

I try to test this out, but I’m getting errors about an excellent package called ngrok, which I’ve added to my environment since last we spoke.

$ nix-env -p ~/scratch/profile -irf ian.nix
error: Package ‘ngrok-2.3.35’ in /nix/store/mi0xpwzl81c7dgpr09qd67knbc24xab5-nixpkgs-21.05pre274251.f5f6dc053b1/nixpkgs/pkgs/tools/networking/ngrok-2/default.nix:38
has an unfree license (‘unfree’), refusing to evaluate.

The rest of the error message basically tells me to do this:

$ mkdir -p ~/.config/nixpkgs

$ echo '{ allowUnfree = true; }' > ~/.config/nixpkgs/config.nix

And now it works just fine.

Neat! Okay.

This seems like a pretty good solution to the declarative user environment problem? That file is pretty simple:

$ cat ian.nix
with import <nixpkgs> {}; [
  cabal-install
  curl
  fzf
  git
  # ...
  xz
  yarn
  yasm
  zsh
]

Now, what else do I want to do?

Well, over time I expect that I will nix-env -iA things, and will want to see how my environment has diverged from my declarative environment so that I can bring them into sync.

But that’s easy to do:

$ nix-env -p ~/scratch/profile -iA nixpkgs.hello
installing 'hello-2.10'
these paths will be fetched (0.02 MiB download, 0.07 MiB unpacked):
  /nix/store/70pxcwpdiq7ddrk4w8axfl51s9xh9ahn-hello-2.10
copying path '/nix/store/70pxcwpdiq7ddrk4w8axfl51s9xh9ahn-hello-2.10' from 'https://cache.nixos.org'...
building '/nix/store/687kj91sv4cdvhf2z9rkrdaj2g7gb6g2-user-environment.drv'...
created 767 symlinks in user environment
$ diff -u --label ian.nix <(nix-env -qaf ian.nix) --label current <(nix-env -p ~/scratch/profile -q)
--- ian.nix
+++ current
@@ -3,6 +3,7 @@
 fzf-0.25.1
 git-2.30.1
 graphviz-2.42.2
+hello-2.10
 hexedit-1.2.13
 htop-3.0.5
 hugo-0.81.0

Handy command, maybe? It occurs to me that this is also a better version of nix-env -u --dry-run for seeing outdated packages – I tweak it a little bit to show me “the diff that will be applied to my environment if I resync,” which is the opposite of what I had above, and save it in my script directory.

Now it looks like this (after a nix-channel --update):

$ sd nix diff
--- current
+++ user.nix
-git-2.30.0
+git-2.30.1
-hello-2.10
-python3-3.8.7
+python3-3.8.8

For the record, in the “full version,” that’s:

$ sd cat nix diff
#!/usr/bin/env bash

set -euo pipefail

diff -U 0 \
  --label current <(nix-env -q) \
  --label user.nix <(nix-env -qaf ~/dotfiles/user.nix) \
| grep -v '^@'

Neat. Now any packages I manually install will show up as - lines to tell me that “re-syncing” with nix-env -irf ~/dotfiles/user.nix will uninstall them. I think that’s clear; I think I can remember that. I might end up hating it, or making the output fancier. For now? Seems great.

This is also much faster than nix-env -u --dry-run, because it’s not doing anything with package names, just attributes. So that’s pretty nice too. And it’s not going to end up trying to get me to install some weird alpha version of Python. Which is a bonus.

Alright! I feel pretty good about this. Thanks, Doug. I think it would have taken me much longer to get here without your help.

But before I go, I just have to see for myself…

$ nix-env -p ~/scratch/profile -i

Oh. Oh yeah. Oh yeah it does.

It blew out my scrollback, but I can show you the very end:

...
installing 'zxing-3.1.0'
installing 'zydis-3.1.0'
installing 'zynaddsubfx-3.0.5'
installing 'zz-unstable-2021-01-26'
installing 'zziplib-0.13.71'
installing 'zzuf-0.15'
error: Package ‘1oom-1.0’ in /nix/store/v43dzqk60bmd4rpksq5ix9gibcj18as9-nixpkgs-21.05pre273941.a2b0ea6865b/nixpkgs/pkgs/games/1oom/default.nix:27 is not supported on ‘x86_64-darwin’, refusing to evaluate.

a) To temporarily allow packages that are unsupported for this system, you can 
   use an environment variable for a single invocation of the nix tools.

     $ export NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM=1

b) For `nixos-rebuild` you can set
  { nixpkgs.config.allowUnsupportedSystem = true; }
in configuration.nix to override this.

c) For `nix-env`, `nix-build`, `nix-shell` or any other Nix command you can add
  { allowUnsupportedSystem = true; }
to ~/.config/nixpkgs/config.nix.

(use '--show-trace' to show detailed location information)

Phew. Thanks, 1oom-1.0. Saved me there.

I do realize that I’m relying on exactly this same behavior whenever I run nix-env -irf ian.nix. And I like that I can do that. But I would really like it much more if it went something like this:

$ nix-env -irf ian.nix
error: no selector provided
If you want to install the entire package universe, re-run this
command with --yes-i-really-want-to-install-every-package

$ nix-env -irf ian.nix --yes-i-really-want-to-install-every-package

But oh well. There’s no way I’ll ever type nix-env -i by itself by accident, so what are the odds that I’ll ever have to think about this again he said his voice trailing off into a mumble.


  1. If you are paying way too much attention you might remember that I learned in in part 20 that this is not a Nix function, it’s a C++ thing. But part 20 took so long to finish that the timeline is a bit muddied: I started writing this post after I started writing part 20, but finished it before I got to that part, so just… you don’t care anyway. ↩︎

  2. Yes, I already did this in part 20, but due to the timeline multiverse I did actually learn how to do it here first. ↩︎

  3. In part 26, I will learn that this is a little more complicated than I thought. ↩︎