It's not about “Flakes vs. Channels”

I have seen a couple of times people conflate that “not using Flakes” is synonymous to “using ‘channels’”.

In reality, things are murkier than that, and anyway this simplification misses the big picture.

What even are channels??

Exactly.

The term channel in the NixOS ecosystem is severely overloaded.

It may mean any or many of:

Let's dive into how this all matches-up.

Channel (advances), release (bumps), and git branches

This definition of channel is more of an organizational and conceptual one. It is used to refer to a specific version of (usually) Nixpkgs that has been built and tested on the organization's Hydra instance.

So, for example, at any point in time, the nixos-23.11 channel conceptually refers to the latest Nixpkgs commit where the tested job has been built successfully, and the whole package set has been (tried) to be built on Hydra.

As a concept, “channels” is either used to talk about the release one uses (e.g. 23.11 vs. unstable) or to talk about how the current release has been bumped, or that the channels advanced to a given revision.

The closer to the core of Nix, Nixpkgs and NixOS developers you find yourself, the more likely it is that channels refer to this definition.

In practice, end-users will observe and interact with these in two places:

The former is what the legacy-but-still-default Nixpkgs and NixOS installs refer to with nix-channel, and the latter is how other schemes (including flakes) will refer to them.

These are what could be referred to as inputs to a build.

The channel:... syntax

This is a quick one.

Paraphrasing from the Nix manual:

The URLs of the tarballs from the official nixos.org channels can be abbreviated as channel:<channel-name> [when used in NIX_PATH or with fetchTarball]. For instance, the following two values are equivalent:

Though, I've rarely seen someone use the bare “channel” word to refer to this shortcut.

In the end, it's just another way to refer to the previous concept of channels.

Hydra channels?

Forget this was even mentioned…

They are a deprecated concept.

In the before-before-times, hydra itself was able to serve a nixexprs for usage with nix-channel.

Mis-quoting another contributor:

Hydra channels are just wildly broken.

And anyways, not used anymore in the NixOS organization.

What's in a NIX_PATH

Not to be confused with Nix store paths.

NIX_PATH is the overall concept within Nix, analogous to the PATH environment variable, where a colon-separated list of entries is used during evaluation to look up some values.

NIX_PATH and -I arguments are what makes the impure angled-brackets syntax work to refer to paths.

$ NIX_PATH="foobar=/etc" nix repl

nix-repl> <foobar>
/etc

One reason this is impure is because the values are not declared within the expressions, but come from an external soupy ambient mess (the environment). In more precise words, things looked-up in NIX_PATH are not declarative.

In addition to being an anti-pattern due to evaluation impurities, it is also confusing. What is the value of NIX_PATH on a given system? Which of the many values will it use? (See root vs. user environments.)

Its value and semantics also vary greatly depending on how Nix is managed, whether it is running on NixOS or not, and will be affected by some factors like the management of the nix-channel command and its own semantics.

The reason NIX_PATH is described separately here is because some people conflate NIX_PATH with the overall concept of channels since nix-channel relies on NIX_PATH.

The nix-channel command

Not to be confused with channels.

The nix-channel command is the main thing people conflate as being the opposite of Flakes. It is not.

It is, however, the most likely method people used or use to manage inputs system-wide when not using Flakes.

The nix-channel semantics, quickly explained, use nix-env's profile semantics to build a directory that can be used as an entry for NIX_PATH semantics. The names given to a nix-channel entry are arbitrary, but some concepts built on top of nix-channel may give a particular importance to some names (like nixpkgs or nixos).

Maybe showing what it does can help:

$ nix-channel --add https://stuff.samueldr.com/foobar-channel/nixexprs.tar.bz2 foobar

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

$ NIX_PATH=~/.nix-defexpr/channels/ nix repl
Welcome to Nix 2.18.2. Type :? for help.

nix-repl> :p <foobar>
/Users/samuel/.nix-defexpr/channels/foobar

nix-repl> :p import <foobar>
"Oh, hi there!"

nix-repl> :q

$ ls -lA .nix-defexpr/channels
lrwxrwxrwx 1 samuel users 48 May  7 17:05 .nix-defexpr/channels -> /Users/samuel/.local/state/nix/profiles/channels

$ ls -l ~/.local/state/nix/profiles/
total 12
lrwxrwxrwx 1 samuel users 15 May  7 17:05 channels -> channels-3-link
lrwxrwxrwx 1 samuel users 60 May  7 17:01 channels-1-link -> /nix/store/1bfrwc8kaq2ncy25nn4jiwklx0sqlfid-user-environment
lrwxrwxrwx 1 samuel users 60 May  7 17:03 channels-2-link -> /nix/store/6mc6isrpx6zc8wxd8qlmiw1z6znyw7dk-user-environment
lrwxrwxrwx 1 samuel users 60 May  7 17:05 channels-3-link -> /nix/store/s4ivcl9jv7gxf14081r26p89hxw8l26a-user-environment

$ ls -l ~/.local/state/nix/profiles/channels/
total 4
lrwxrwxrwx 2 root root 57 Dec 31  1969 foobar -> /nix/store/y64v3wal14x3rj5phkqgy16kpmaqa3sm-foobar/foobar
lrwxrwxrwx 2 root root 60 Dec 31  1969 manifest.nix -> /nix/store/mq250jdhynq8wvmvfcnvd6smp16znlkq-env-manifest.nix

$ cat ~/.local/state/nix/profiles/channels/foobar/default.nix 
"Oh, hi there!"

Note that this is simplified and showing user channels only. The root user also can have channels, and whether or not they are used for the user depends on the exact setup. Though they will generally be used.

There are a couple issues with using nix-channel for managing inputs.

First, these are global values. While you can override them with NIX_PATH or by using -I, the default is to have a single set of values globally. Since they rely on NIX_PATH semantics, they get confusing quickly. Quick: what's the difference between nixos-rebuild --fast dry-build and sudo nixos-rebuild --fast dry=build? It depends! If the user's channel don't declare another input for nixpkgs, they should be equivalent. Otherwise, the user's own channels are prefixed in NIX_PATH, which can make the outputs different. Having the user accidentally manage two nixpkgs inputs, one for root and one for the user, is a common pitfall.

Second, they are extremely non-declarative. Given enough luck, a release bump to the expression could happen between the moment you run nix-channel --update on two different machines, leading to different inputs even though one would morally feel the update happened at the same moment. This is exacerbated by the fact that referring to a given precise input in nix-channel is pretty much impossible out of the box. You can't reliably use a pool of machines with a common coordinated single revision of Nixpkgs while using nix-channel.

Because of these reasons, nix-channel should not be used. It's a tool from another era.

Then Flakes are the alternative to channels?

Yes, but also no.

Pedantically, when using Flakes, you (likely) end-up using the concept of channels through referring to a Nixpkgs branch name. So in this way, they are not an alternative to channels, since they use channels.

What Flakes are, among myriad things, is a way to manage inputs. In that sense Flakes can and do replace nix-channel semantics.

Though they are not the alternative, only one of many.

Other ways to manage inputs

This is not a comprehensive list. There are other ways to handle this.

With bare fetchTarball

This is a cumbersome way to manage inputs, and has drawbacks, but it works and relies on nothing external.

$ cat test.nix 
let
  sha256 = "sha256:0xxbzvrr7kylvjn2yj0nnrm1qndx8rq99mlkk0ghvy364y5c5pv0";
  rev = "44d0940ea560dee511026a53f0e2e2cde489b4d4";
  pkgs = import (fetchTarball {
    url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz";
    inherit sha256;
  }) {};
in
  pkgs.hello

$ $(nix-build --no-out-link ./test.nix)/bin/hello
Hello, world!

For single-file reproducers, this is really useful.

When working with multiple inputs, in a complicated project, it has its issues. Mainly, updating is a chore (need to get a proper revision manually). Since it's bespoke, every project could be using this pattern differently.

With that said, using this pattern as a default value for an argument in the entry-point of an evaluation can work well enough, and is declarative.

Using npins

The npins project is a single-purpose tool to manage inputs for Nix-based projects.

There is no goal to lock all transitive dependencies from the whole evaluation like there is with Flakes. No way to inherit inputs from another input (e.g. follow with Flakes). It only handles declaring inputs for your expression.

This is what I am currently using to manage my NixOS system inputs. I value the simplicity and straight-forwardness here.

Unless a project needs the complexity of Flakes, I now prefer using npins to declare inputs.

Closing thoughts

Just remember, when talking with other Nix users, to make sure you're using the same definition when talking about channels, and that Flakes use channels, and are only one way to not use nix-channel.