Okay. In the last post I got sort of an introduction to flakes, and I even wrote a flake, but I didn’t learn how to… do anything with flakes. My flake provided an overlay, but I have no idea how to make my Nixpkgs actually use that overlay.

I have no idea what “my Nixpkgs” even means anymore. My channel? My flake? Everything I’ve learned about Nix has been turned upside down.

But presumably if we keep reading, these blog posts will explain how to actually put them to use.

Nix Flakes, Part 2: Evaluation Caching

Nix evaluation is often quite slow. In this blog post, we’ll have a look at a nice advantage of the hermetic evaluation model enforced by flakes: the ability to cache evaluation results reliably.

Neat. That was something that I observed happening in the last post, when I was trying to use builtins.trace to debug things. I’m excited about the idea of evaluation caching for two reasons:

  1. nix-shell takes like two seconds before it actually enters a shell.
  2. Scripts written with nix-shell -i take like two seconds before they actually start running.

I’m aware of a third-party thing that tries to solve the latter problem, by caching evaluation results, but I haven’t actually used it. I have tried to stick as close as possible to “vanilla Nix,” so I’m excited to hear that this is going to be a first-party feature.

Let’s see. The blog post starts by saying that Nix is slow, and explaining that you can’t cache things safely, because pre-flakes evaluation is not “hermetic.” I’m with you so far.

(As an aside: for a while, Nix has had an experimental replacement for nix-env -qa called nix search, which used an ad hoc cache for package metadata. It had exactly this cache invalidation problem: it wasn’t smart enough to figure out whether its cache was up to date with whatever revision of Nixpkgs you were using. So it had a manual flag --update-cache to allow the user to force cache invalidation.)

I’m personally kind of happy to see the end of --update-cache, even if it means nix search is slower, but that’s sort of irrelevant.

Oh look! The blog post actually goes into the implementation:

The cache is implemented using a simple SQLite database that stores the values of flake output attributes. After the first command above, the cache looks like this:

$ sqlite3 ~/.cache/nix/eval-cache-v1/302043eedfbce13ecd8169612849f6ce789c26365c9aa0e6cfd3a772d746e3ba.sqlite .dump
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE Attributes (
    parent      integer not null,
    name        text,
    type        integer not null,
    value       text,
    primary key (parent, name)
);
INSERT INTO Attributes VALUES(0,'',0,NULL);
INSERT INTO Attributes VALUES(1,'packages',3,NULL);
INSERT INTO Attributes VALUES(1,'legacyPackages',0,NULL);
INSERT INTO Attributes VALUES(3,'x86_64-linux',0,NULL);
INSERT INTO Attributes VALUES(4,'firefox',0,NULL);
INSERT INTO Attributes VALUES(5,'type',2,'derivation');
INSERT INTO Attributes VALUES(5,'drvPath',2,'/nix/store/7mz8pkgpl24wyab8nny0zclvca7ki2m8-firefox-75.0.drv');
INSERT INTO Attributes VALUES(5,'outPath',2,'/nix/store/5x1i2gp8k95f2mihd6aj61b5lydpz5dy-firefox-75.0');
INSERT INTO Attributes VALUES(5,'outputName',2,'out');
COMMIT;

Neat. Okay.

The name of the SQLite database, 302043eedf….sqlite in this example, is derived from the contents of the top-level flake. Since the flake’s lock file contains content hashes of all dependencies, this is enough to efficiently and completely capture all files that might influence the evaluation result. (In the future, we’ll optimise this a bit more: for example, if the flake is a Git repository, we can simply use the Git revision as the cache name.)

Sure. Fair enough.

Oh, I learn an interesting thing here.

I might have be looking at the wrong performance numbers when I timed nix search in the last post.

$ time nix search nixpkgs git >/dev/null
[5.6/0.0 MiB DL] downloading 'https://api.github.com/repos/NixOS/nixpkgs/tarbal

IT DID THE DOWNLOAD THING AGAIN. What the heck? What is it downloading? Why is it downloading anything? I happened to copy it quickly enough that time, so I can see that it’s downloading Nixpkgs… but why? I haven’t collected garbage. I haven’t done anything weird on this computer since the last time I ran Nix search. What is Nix doing?

Anyway, here’s the damage:

$ time nix search nixpkgs git >/dev/null
24.43s user 37.05s system 89% cpu 1:08.43 total

Yeah, it took a full minute because it had to download and decompress all of Nixpkgs. Even though I didn’t ask it to. Man, that’s really bad. I sort of… hate Nix 2.4 so far, just because of this one issue.

Nix 2.3 had a lot of UX problems, but at least the problems were predictable. But with Nix 2.4, I don’t know when a command is going to randomly take 34 times longer than I expect it to, because Nix decided that the one moment I asked it to do something was the right time to perform some very slow housekeeping.

Oof. There had better be an option to disable this. This is horrible.

Anyway.

As you may recall, I was in the middle of trying to do something, before Nix decided that actually it was time to do something else.

Ugh.

Anyway: subsequent invocations of the same command are quick:

$ time nix search nixpkgs git >/dev/null
1.90s user 0.08s system 97% cpu 2.039 total

But this blog post has made me suspect that it’s because I searched for the same package. If I try:

$ time nix search nixpkgs mercurial >/dev/null
1.72s user 0.05s system 98% cpu 1.792 total

Okay, no, it’s still very fast. Even faster, actually, probably because I searched for a longer string and that’s how computers work. Or because there were fewer results. Or, well, probably both of those things, really.

I tried a couple other packages, and I’ll stand by my statement in the previous blog post that 2.4’s nix search takes about twice as long as 2.3’s nix search, and that that’s an acceptable sacrifice for the lack of manual cache invalidation. But I will add a strong caveat that the new auto-updating-Nixpkgs behavior is so completely user-hostile that I’m going to have to go back to Homebrew if I can’t disable that.1

Okay. Deep breaths. It’s going to be okay.

Oh, look! The blog post says something about cache invalidation:

There is only one way in which cached results can become “stale”, in a way. Nix evaluation produces store derivations such as /nix/store/7mz8pkgpl24wyab8nny0zclvca7ki2m8-firefox-75.0.drv as a side effect. (.drv files are essentially a serialization of the dependency graph of a package.) These store derivations may be garbage-collected. In that case, the evaluation cache points to a path that no longer exists. Thus, Nix checks whether the .drv file still exist, and if not, falls back to evaluating normally.

Well, okay sure, but this does nothing to explain why you choose to just randomly refresh the Nixpkgs repo while I’m in the middle of doing something.

The blog post goes on to talk about the idea of sharing evaluation caches and downloading them from the internet. Which is funny, to me. We sort of… already do that, right? Like, you can download a .drv file from a substituter, right? This is just… because flakes are a new concept, you need a new substitution mechanism. Hmm.

Well, that’s all for this blog post. Still no idea how to use flakes. Let’s try the next one.

Nix Flakes, Part 3: Managing NixOS systems

Oh. Oh no. I don’t care about that. Nor do I have a way to follow along at home. It does seem to describe how to use flakes, sort of, but it involves editing your NixOS configuration files, which obviously… I do not have.

It describes how to apply overlays:

  outputs = { self, nixpkgs, nix }: {
    nixosConfigurations.container = nixpkgs.lib.nixosSystem {
      ...
      modules =
        [
          ({ pkgs, ... }: {
            nixpkgs.overlays = [ nix.overlay ];
            ...
          })
        ];
    };
  };
}

But only when defining a “NixOS container” flake. Nothing about how to use overlays myself.

Hmm. It goes on to say:

It’s often convenient to pin the nixpkgs flake to the exact version of nixpkgs used to build the system. This ensures that commands like nix shell nixpkgs#<package> work more efficiently since many or all of the dependencies of <package> will already be present. Here is a bit of NixOS configuration that pins nixpkgs in the system-wide flake registry:

nix.registry.nixpkgs.flake = nixpkgs;

Note that this only affects commands that reference nixpkgs without further qualifiers; more specific flake references like nixpkgs/nixos-20.03 or nixpkgs/348503b6345947082ff8be933dda7ebeddbb2762 are unaffected.

But again, no hint of how to do this outside of NixOS.

And that’s the end! That’s it. That’s the introduction and tutorial.

Nothing about how to use flakes outside of NixOS.

So I suppose I’ll have to look for myself.

Let’s try reading the man pages.

Er, well, there are no man pages for the new nix commands. But the --help text is very long, and formatted sort of like a man page. So we’ll start there.

nix flake --help

I learn a bit of terminology, that I have seen before:

A flake is a filesystem tree (typically fetched from a Git repository or a tarball) that contains a file named flake.nix in the root directory. flake.nix specifies some metadata about the flake such as dependencies (called inputs), as well as its outputs (the Nix values such as packages or NixOS modules provided by the flake).

And, more importantly:

Flake references (flakerefs) are a way to specify the location of a flake. These have two different forms:

  • An attribute set representation, e.g.
{
type = "github";
owner = "NixOS";
repo = "nixpkgs";
}

The only required attribute is type. The supported types are listed below.

  • A URL-like syntax, e.g.
github:NixOS/nixpkgs

These are used on the command line as a more convenient alternative to the attribute set representation. For instance, in the command

# nix build github:NixOS/nixpkgs#hello

I’m going to stop right here and say that it’s using # as a prompt character…? Instead of $? Which I understand as convention for “run this command as root.” But… I have no idea why the manual used that here. It doesn’t seem to do it anywhere else, so maybe it was just a typo. Anyway.

github:NixOS/nixpkgs is a flake reference (while hello is an output attribute). They are also allowed in the inputs attribute of a flake, e.g.

inputs.nixpkgs.url = github:NixOS/nixpkgs;

is equivalent to

inputs.nixpkgs = {
type = "github";
owner = "NixOS";
repo = "nixpkgs";
};

Okaaaaay. Does that mean that, like, that’s new Nix syntax?

nix-repl> github:NixOS/nixpkgs
"github:NixOS/nixpkgs"

No. Okay. That’s still parsing as a string, using the weird unquoted URL syntax. Does it parse differently during flake evaluation? Surely not, right? Surely not.

I don’t know how to test that, because I don’t know how to evaluate flakes. Flakes are still weird black boxes to me. But I’m going to assume that the manual is speaking loosely, when it calls those equivalent.

It then gives lots of examples of different syntax for flakerefs. Files, github: paths, git+https:// URIs, compressed tarballs over https://. Okay neat. Apparently .tar.gz files can be “flakes” as well.

Flake reference attributes

The following generic flake reference attributes are supported:

  • dir: The subdirectory of the flake in which flake.nix is located. This parameter enables having multiple flakes in a repository or tarball. The default is the root directory of the flake.

  • narHash: The hash of the NAR serialisation (in SRI format) of the contents of the flake. This is useful for flake types such as tarballs that lack a unique content identifier such as a Git commit hash.

This is the next section of the manual. I think that this is documenting the special magic attributes that Nix implicitly attaches to the attribute-set-not-attribute-set value that flakes have in the Nix expression language, though it doesn’t say that explicitly. It lists:

  • dir
  • narHash
  • rev (if applicable)
  • ref (if applicable)
  • revCount (if applicable)
  • lastModified

Is that all of them? Is this list exhaustive? There’s no way to confirm, because I don’t know how to observe a flake in motion.

It then lists all the possible types, as it promised it would.

The first is path, which is also the most complicated. I learn that if you supply a path to a local Git repo it’s smart enough to treat it like a Git thing, and not a regular directory.

path generally must be an absolute path. However, on the command line, it can be a relative path (e.g. . or ./foo) which is interpreted as relative to the current directory. In this case, it must start with . to avoid ambiguity with registry lookups (e.g. nixpkgs is a registry lookup; ./nixpkgs is a relative path).

Good to note; nice UI touch. The other types are simpler:

  • git
  • mercurial (!)
  • tarball
  • github
  • indirect

Where indirect means “something in the registry.” I can apparently type such “flakerefs” as nixpkgs or as flake:nixpkgs.

Aha. And then it describes flakes, and here it lists the attributes that flakes have:

In addition to the outputs of each input, each input in inputs also contains some metadata about the inputs. These are:

  • outPath: The path in the Nix store of the flake’s source tree.
  • rev: The commit hash of the flake’s repository, if applicable.
  • revCount: The number of ancestors of the revision rev. This is not available for github repositories, since they’re fetched as tarballs rather than as Git repositories.
  • lastModifiedDate: The commit time of the revision rev, in the format %Y%m%d%H%M%S (e.g. 20181231100934). Unlike revCount, this is available for both Git and GitHub repositories, so it’s useful for generating (hopefully) monotonically increasing version strings.
  • lastModified: The commit time of the revision rev as an integer denoting the number of seconds since 1970.
  • narHash: The SHA-256 (in SRI format) of the NAR serialization of the flake’s source tree.

Note that this is a different list than the list it gave earlier about flake attributes. What’s… what? What was that first list? What did that mean, then?

Let’s see. It talks about specifying inputs. We sort of saw this already, but it goes into more detail.

you can use the URL-like syntax:

inputs.import-cargo.url = github:edolstra/import-cargo;
inputs.nixpkgs.url = "nixpkgs";

Oh. I see that I misread this before, when I was asking about these values parsing differently. I didn’t notice the .url part. Okay. It seems a little surprising to me that you don’t just write inputs.import-cargo = github:edolstra/import-cargo;; that’s what I read it is as before. But okay. I will try to remember the extra url bit.

But that’s super weird, because then you could say something contradictory:

inputs.nixpkgs = {
  type = "github";
  owner = "NixOS";
  repo = "nixpkgs";
  url = "github:ianthehenry/nothing";
}

And like… do you ignore the url, in that case? I guess so? It feels weird. Maybe there’s a case when you would specify url and some other attributes…? I don’t know.

It is also possible to omit an input entirely and only list it as expected function argument to outputs. Thus,

outputs = { self, nixpkgs }: ...;

without an inputs.nixpkgs attribute is equivalent to

inputs.nixpkgs = {
  type = "indirect";
  id = "nixpkgs";
};

Ah, okay. This explains the sort-of-weird implicit nixpkgs dependency I encountered before. It wasn’t magically hardcoded, it was just inferred from the inputs.

Which means that any typo is going to implicitly add a new input? I guess that’s not a big deal. You’d notice pretty easily.

Repositories that don’t contain a flake.nix can also be used as inputs, by setting the input’s flake attribute to false:

inputs.grcov = {
  type = "github";
  owner = "mozilla";
  repo = "grcov";
  flake = false;
};

It doesn’t say anything about what that… means? But it gives an example that uses grcov as if it’s a path, so it like… I still don’t understand what a flake is in the Nix expression language, but it seems like these flake inputs that are not flakes are also flakes, in the expression language?

I dunno, y’all.

Transitive inputs can be overridden from a flake.nix file. For example, the following overrides the nixpkgs input of the nixops input:

inputs.nixops.inputs.nixpkgs = {
  type = "github";
  owner = "my-org";
  repo = "nixpkgs";
};

That’s neat. I like that; I could see that being useful.

It is also possible to “inherit” an input from another input. This is useful to minimize flake dependencies. For example, the following sets the nixpkgs input of the top-level flake to be equal to the nixpkgs input of the dwarffs input of the top-level flake:

inputs.nixpkgs.follows = "dwarffs/nixpkgs";

Neat, okay. I saw that in the last post and correctly figured out what it meant.

It’s sort of weird that this is… slash-separated? Like, it’s not:

inputs.nixpkgs = inputs.dwarffs.inputs.nixpkgs;

Or whatever the correct recursive expression is. Instead it’s… a string that sort of looks like a path. But whatever.

Overrides and follows can be combined, e.g.

inputs.nixops.inputs.nixpkgs.follows = "dwarffs/nixpkgs";

sets the nixpkgs input of nixops to be the same as the nixpkgs input of dwarffs. It is worth noting, however, that it is generally not useful to eliminate transitive nixpkgs flake inputs in this way. Most flakes provide their functionality through Nixpkgs overlays or NixOS modules, which are composed into the top-level flake’s nixpkgs input; so their own nixpkgs input is usually irrelevant.

Aha!

This is the first hint we get of how we might actually use flakes. If I write a flake that has an input that contains an overlay, and also has an input that is nixpkgs, then my nixpkgs input is going to contain those overlays applied?

Is that… is that statement saying that this is something Nix does automatically, or that this is conventially going to be the way that overlays will be used by “the top-level flake?”

I don’t know.

Next up we learn about lock files… I am familiar with the general idea from all other package managers; if I ever have to understand the details at the level that the help text is explaining right now then something has gone horribly wrong.

And that’s it! That’s the whole thing.

Okay.

We didn’t really learn… how to use flakes, still. How to apply overlays into “my” copy of Nixpkgs.

Like, say I want to do something very simple:

$ nix build nixpkgs#git
zsh: no matches found: nixpkgs#git

Oh, have mercy.

Yes, I have EXTENDED_GLOB enabled in my zsh, which means that my shell treats nixpkgs#git as a glob, roughly equivalent to the following regular expression:

nixpkgs+git

Of course, this is not what I want; this is never what I want. I use EXTENDED_GLOB so I can write ^negated globs, and I don’t think I have ever tried to type a repeated glob like that.

But I can’t disable only that. So in order to type a “flakeref” at the command line, I have to quote it:

$ nix build 'nixpkgs#git'
[0.0 MiB DL] downloading 'https://api.github.com/repos/NixOS/nixpkgs/tarball/18e6b518427

And it’s just hanging again. Because it needs to download Nixpkgs – again – for some reason. Honestly, what is even happening here; how is it even possible that it doesn’t cache the “flakes” in my registry? Isn’t it… what is Nix thinking here?

I don’t know if I want to keep learning about Nix 2.4. This is… this is just horrible to use.

So we’re going to take a brief digression into man 5 nix.conf to try to figure out how to disable… whatever the hell this is.

I grep for flake, but there’s nothing promising. I try update, then cache… nothing there. I read through every option individually, but can’t find anything that sounds relevant.

I search google for “nix flake downloading constantly” and find this Discourse thread.

The answer says you can “pin” nixpkgs to prevent re-downloading it constantly. I see that there is nix registry pin, and the help shows me another weird superuser prompt example:

# nix registry pin nixpkgs

I ran that – or rather, I ran:

$ nix registry pin nixpkgs

And it didn’t… give me any output. It didn’t tell me what it pinned it to. But I can look at the flake:

$ nix flake metadata nixpkgs
Resolved URL:  github:NixOS/nixpkgs/18e6b5184274305f2c9dc36141003473375a5df9
Locked URL:    github:NixOS/nixpkgs/18e6b5184274305f2c9dc36141003473375a5df9
Description:   A collection of packages for the Nix package manager
Path:          /nix/store/f7pd39anz0rmv3jv3yk07a8m74xn1dqj-source
Revision:      18e6b5184274305f2c9dc36141003473375a5df9
Last modified: 2021-12-07 19:28:59
Inputs:

Okay. So like… before I pinned it, it was literally making an HTTP request to GitHub every time I ran search? Is that… that’s insane, right?

Let’s find out.

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

Sigh of course. Though note that nix --help has absolutely no help; you need to run nix registry --help instead.

Er, sorry, that doesn’t have any information about unpinning either. You have to look in nix registry pin --help, of course. I do that, and learn that I need to nix registry remove…? But I don’t want to remove nixpkgs. I just want to unpin it.

So it seems like what’s actually happening is:

nix registry pin didn’t “pin” anything. It added a new entry to my “user” registry that shadows the nixpkgs entry in the “global” registry.

$ nix registry list | grep nixpkgs
user   flake:nixpkgs github:NixOS/nixpkgs/18e6b5184274305f2c9dc36141003473375a5df9
global flake:nixpkgs github:NixOS/nixpkgs

Then I can run nix registry remove nixpkgs to remove the entry from my user registry – but not the global registry.

$ nix registry remove nixpkgs

$ nix registry list | grep nixpkgs
global flake:nixpkgs github:NixOS/nixpkgs

Okay. And now I can disable my Wi-Fi™ and try again…

$ nix search nixpkgs git
warning: you don't have Internet access; disabling some network-dependent features
...

Ha! So.. it really was checking with GitHub to see if I had the latest version of the repo? It was able to search offline just fine, but the message implies that it wanted to transfer some hypertext anyway.

Oh my goodness.

Wow.

That’s… that’s really insane default behavior.

But I don’t think it’s checking on every command. It has some kind of local cache, apparently, because I can still run nix registry pin without internet access, and it does work.

But there’s no --verbose or anything to explain what’s actually going on here or when it does decide to check. Nix is just… randomly deciding to make HTTP requests to GitHub, when it feels like its registry needs updating?

Ugh. It used to be so easy to just dtruss this and find out. Now I need to like… disable various macOS features just to be able to instrument system calls. Why am I using a mac, again? Oh yeah iOS. Stupid iOS.

Well, anyway, I have no idea. Nix’s behavior around registry updates feels like a crazy black box. I find this PR which implies that the “re-use most recently downloaded thing” behavior is only enabled when you’re offline, so maybe it really is checking in with GitHub every time? Oof. I dunno. This seems crazy, but that PR is more than two years old, so maybe things have changed.

Anyway…

So now if I want to update my Nix things… instead of typing nix-channel --update, I’m supposed to type:

$ nix registry remove nixpkgs

$ nix registry pin nixpkgs

To update things? Is that… is that what I’m hearing here? Or can I just run pin again, and have it update? That would be surprising. I don’t know how to test it. While poking around, I find that my “user registry” seems to live in ~/.config/nix/registry.json:

$ cat ~/.config/nix/registry.json
{
  "flakes": [
    {
      "from": {
        "id": "sd",
        "type": "indirect"
      },
      "to": {
        "owner": "ianthehenry",
        "ref": "add-flakes",
        "repo": "sd",
        "type": "github"
      }
    },
    {
      "from": {
        "id": "nixpkgs",
        "type": "indirect"
      },
      "to": {
        "lastModified": 1638934139,
        "narHash": "sha256-uV77+UK9rfcvCHM6eLYYNFp9iLhIJ6WUAPv4QcZ1pV0=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "18e6b5184274305f2c9dc36141003473375a5df9",
        "type": "github"
      }
    }
  ],
  "version": 2
}

That’s good to know, I guess.

Hmm.

How do I roll back these “pin” updates? nix-channel --rollback has saved my skin many times. Do I like… save the lock file that it creates, before I unpin? Do I just write down the rev that it was pinned to? Where does the lock file live?

Also… am I really getting the latest revision of the Nixpkgs repo? And if so, is that what I want?

I remember learning, while reading the Nix manual, that the unstable channel existed to lag the Nixpkgs repo so that tests could pass and binaries could be built and put in the cache, so that if you used the unstable channel you could trust that you wouldn’t have to compile everything from source.

I also remember learning that this didn’t really seem to be the case, and there were lots of derivations without substitutes, despite whatever the manual had to say about the purpose of the unstable channel.

But now it seems… Nix is abandoning that pretense, and is always giving me the tip of the repo? Like, constantly, against my will, whenever I try to run any command?

Hmm.

Very strange.

Well, I was trying to see if the overlays in my sd flake are available when I reference my nixpkgs flake. But it seems that they aren’t.

How do I “apply” the overlay in my sd flake to my nixpkgs flake?

I have no idea. My mental model is telling me that I can’t, and I would have to write my own nixpkgs flake and add it to my registry separately if I actually wanted to do this. And then I have no idea how I would update that. Hmm.

But maybe I don’t want to do that? Maybe there’s no point?

I thought it made sense to provide sd as an overlay, when I was thinking about channels. I wanted to be able to type nixpkgs.sd. But now… now I’m thinking that that’s something I need to give up? I’m not sure why I would ever refer to just nixpkgs, in this day and age. It would be nice if my overlayed software showed up when I, like, nix search nixpkgs, but… it’s not really vital for me.

Maybe I need to take the next step, and figure out how to actually start using flakes. How to upgrade my shell.nix files; how to replace nix-env. And then maybe it will be clear how to do this, or it will be clear why I don’t need to do this after all.


  • When does Nix decide to update unpinned flakes?
  • How do I roll back a Nix… flake… pin operation?

  1. Yes, I know that Homebrew does the same thing, and yes, it’s horrible there too, and the fact that Nix didn’t do this was a big selling point for me. ↩︎