November 30, 2021·6 min read·engineeringnixnixosreleasescompatibility

NixOS 21.11 keeps Nix 2.3 as its default

Porcupine retains Nix 2.3.16 while offering an explicit route to 2.4. Its iptables backend change shows that retaining one default does not make the whole release conservative.

NixOS 21.11 does not make Nix 2.4 its default package manager. The Porcupine release notes explicitly retain 2.3.16 because of regressions in non-experimental behaviour. Version 2.4 remains available through an explicit package selection. That distinction is central to understanding the release, since the availability of experimental flakes and a new command interface does not imply their adoption as the distribution's default workflow.

The decision separates the operating-system release from the package manager's release schedule. Nix 2.4 was announced on November 1, after more than two years of development since 2.3. NixOS 21.11 arrives on November 30 with a different judgement about its default deployment. A component can be available and useful for explicit adoption while the distribution retains an older version for existing users.

I am interested in Nix because declarative infrastructure makes selected state reviewable. That includes the version of the tool interpreting and building the configuration. Keeping the default explicit is useful when a release introduces new workflows whose appeal can otherwise obscure what a normal installation actually runs.

Selecting Nix 2.4 is a separate decision

The NixOS notes identify nixFlakes and nix_2_4 as package choices for the nix.package option. A configuration can select 2.4 directly through the latter.

{ pkgs, ... }:
{
  nix.package = pkgs.nix_2_4;
}

This changes the chosen package. It does not by itself establish every experimental-feature setting needed for a flake-based workflow. The Nix 2.4 release notes explain selective experimental-feature enablement and mark the new nix command as experimental. The package version and the selected experimental behaviours remain related but separate configuration questions.

The distribution notes also identify nixUnstable as a prerelease of Nix 2.5. It is therefore inaccurate to use that attribute name as if it meant precisely the same version as nix_2_4. A package attribute is an operational selection, and its actual version matters when explaining behaviour. An intention to try the newer release should name the attribute that provides that release.

The retained default concerns regressions outside experimental behaviour. That wording is more specific than a general claim that the project rejected flakes. Existing package-manager operations can be affected independently of whether a user enables a new feature. Holding the default protects an established workflow while leaving an explicit route for users willing to adopt the alternative.

The notes do not identify every regression or promise a date for changing the decision. A release account should not invent those details. The supported conclusion is that NixOS maintainers judged 2.3.16 appropriate as the default for this release and documented how to select 2.4 separately.

Flakes describe projects through declared inputs and outputs

The Nix 2.4 notes define a flake as a repository or tarball containing flake.nix. That file specifies dependencies on other flakes and returns outputs such as packages or NixOS modules. The new command interface uses those outputs in references such as nixpkgs#hello. This is a project format and command convention, not a new operating-system distribution.

The release adds operations for querying flakes and updating lock files. It also adds an evaluation cache for flake outputs, which the notes attribute to the hermetic evaluation model. These are concrete mechanisms behind the attraction of the workflow. A project can give tools a predictable place to find its inputs and outputs instead of requiring unrelated conventions for each repository.

They do not imply that every existing Nix command line remains unchanged. The 2.4 notes explicitly describe incompatible changes. The older nix run becomes nix shell, while a new nix run executes an application or a package's corresponding program. Scripts relying on an earlier command's name and meaning need to distinguish those operations.

Profile handling changes too. nix profile records package provenance and exposes history and closure differences. The release warns that a profile updated through it is no longer usable with nix-env. Trying a new interface can consequently affect a persistent environment that another tool expects to manage. That is a compatibility property of the workflow, not only a matter of learning new subcommand names.

I find provenance and closure comparisons useful because they explain what an environment changed. They still need to be adopted with an account of which command owns the profile. A distribution that exposes the new tools while retaining its established package-manager default gives that choice a visible boundary.

The firewall command name keeps a new backend

Porcupine changes iptables to use the nf_tables backend. The command family remains familiar, but the selected implementation behind it changes. The notes explicitly identify ipset as lacking support for this backend and name ferm, shorewall, and firehol as applications that may have limited functionality as a result.

This is a useful counterexample to assuming that the retained Nix default makes every part of the release conservative. Compatibility is decided component by component. A package-manager hold and a firewall-backend transition can coexist in the same distribution. Each has a different reason and a different set of affected users.

The practical question is which component manages the firewall rules and which interfaces it requires. A configuration using a higher-level tool with an ipset dependency has a specific compatibility concern. The presence of an iptables executable alone cannot establish that every older integration receives identical semantics. The release notes identify the dependency boundary that makes that inference unsafe.

The announcement does not provide a universal migration command for every firewall manager. It gives the backend change and known application limitations. An accurate account should preserve that level of specificity instead of treating the change as a blanket claim that packet filtering is either fully compatible or broken for every installation.

Dry activation gains participation from scripts

Activation scripts can now optionally run during nixos-rebuild dry-activate. They detect that mode by reading $NIXOS_ACTION. The release describes this as a way to print changes that would occur during real activation. The users and modules activation script participates and reports some actions.

The word optionally is significant. Dry activation is not a complete simulation of every arbitrary script. A script has to support the mode and provide meaningful reporting. The feature gives module authors a mechanism to describe effects before executing them, while leaving the quality and coverage of that description dependent on implementation.

This is useful for declarative systems because evaluation and activation answer different questions. Evaluation constructs a system configuration. Activation may need to change state on the target machine. Reporting those proposed actions can reveal an effect not apparent from the package closure alone. The new mechanism makes some of that operational work inspectable ahead of time.

It also makes a clear contract for custom activation logic possible. A script can branch on the action and print intended effects in the dry mode. The source does not promise that a script unaware of this convention becomes safe to execute without effects. Participation and correct implementation remain necessary.

Identity defaults become more explicit

The release removes the default nogroup group from user definitions. Its notes explain that the default was insecure and show explicitly creating a corresponding group for a system user. An out-of-tree service module that previously relied on the implicit group may therefore need an additional declaration.

{
  users.users.worker = {
    isSystemUser = true;
    group = "worker";
  };
  users.groups.worker = {};
}

This example makes the user/group association part of the configuration. It supplies the information the older default could conceal. The release similarly requires security wrappers to specify owner and group together with the setuid or setgid choice. It explains that older partial declarations could produce wrappers owned by nobody and nogroup.

Both changes reject ambiguity in privileged configuration. They can cause an older declaration to need revision, but the resulting error points to information the module now requires. Declarative syntax does not remove the need to define ownership. It gives the module a place to demand that definition before generating the resulting system.

Porcupine also changes package defaults including Python 3.9 and PostgreSQL 13, while updating systemd to 249. These are independently documented component changes. The release's defining operational pattern is selective adoption with explicit migration boundaries. Nix remains at 2.3.16 by default, the newer package manager is available by selection, and several system modules change contracts that existing configurations need to describe more fully.