May 30, 2022·6 min read·engineeringnixnixospackagingconfiguration

NixOS 22.05 makes package overrides refer to final values

Quokka updates the desktop and ships Nix 2.8, but its package-construction changes deserve attention too. finalAttrs lets derived settings follow overrides instead of retaining values from an earlier recursive attribute set.

A package override can change one attribute while leaving another attribute computed from its old value. That is a problem for declarative packaging because the source appears to express a relationship that the override no longer preserves. NixOS 22.05 adds a package-construction interface that makes those relationships refer to the final attribute set. The Quokka release notes describe this change alongside its desktop and system updates.

The May 30 release announcement identifies Quokka as the new stable release. GNOME moves to 42, and the release notes identify profile-guided optimization for Firefox on x86_64-linux. The change to stdenv.mkDerivation is less visible on a desktop, but it affects the way package authors can make a customized system internally consistent.

I value declarative systems because the relationships between inputs should be inspectable in source. An override that changes the setting but misses a dependent value weakens that property. Giving a definition access to its final attributes addresses the relationship directly.

Recursive syntax does not know about overrides

Nix permits a recursive attribute set to refer to its own definitions. That syntax is useful for constructing related values. However, the 22.05 standard-environment manual explains that rec operates at the syntax level and is unaware of later overriding.

Consider a package with a boolean controlling a configure flag. If the flag refers to a binding in the original recursive set, changing the boolean through an override does not automatically rewrite that lexical reference. The final package can consequently expose a different switch value without deriving every dependent setting from it.

The new interface accepts a function as the argument to mkDerivation. Nixpkgs invokes that function with the final arguments, including overrides. The manual uses the name finalAttrs for this parameter. Package authors can then express derived values through that parameter explicitly.

stdenv.mkDerivation (finalAttrs: {
  pname = "example";
  version = "1.0";
  withFeature = true;
  configureFlags =
    lib.optionals finalAttrs.withFeature [ "--with-feature" ];
})

This fragment demonstrates the relationship and assumes the surrounding package definition supplies stdenv and lib. A real package also needs its source and other build details. The relevant difference is that configureFlags reads finalAttrs.withFeature, so its input is the final setting seen by package construction.

That is stronger than asking every downstream override author to remember all derived attributes. The package author records the dependency once. A consumer changing the boolean can preserve the relationship already declared by the package, instead of duplicating the expression for its configure flags.

The override can see both the final and previous sets

The release also extends overrideAttrs to accept two parameters, conventionally finalAttrs and previousAttrs. The constructor implementation in this release retains support for the older one-parameter function form and supplies the two-set form when the function requests it.

The distinction between the sets is useful when adding to an existing list. An override can obtain the earlier list from the previous attributes while referring to another final setting for a related value. It need not confuse preserving an earlier contribution with freezing every dependency to its original value.

For the boolean example, the override itself can remain small. If package is the derivation defined with a final-attribute reference, the following selects the disabled setting.

package.overrideAttrs (finalAttrs: previousAttrs: {
  withFeature = false;
})

The meaningful effect depends on the original package using the new relationship correctly. Applying this form to an unrelated definition does not automatically repair lexical references inside that definition. The constructor provides the interface, and the package author decides which derived values should use it.

The implementation documents a limit on self-reference. Attribute names returned by the override function must not depend on the final set, although attribute values can. Depending on the completed set to decide which names it contains can create an infinite recursion. The final-attribute interface makes certain dependencies expressible without removing Nix's evaluation requirements.

Passthru can refer to the final package too

The manual adds finalAttrs.finalPackage for references to the resulting package, including its output paths and override interface. This matters for metadata such as package tests. A test associated with an overridden package should often receive that overridden result, not an earlier package bound outside the constructor.

The manual's longer example distinguishes those references explicitly. A binding outside the constructor still denotes the original package. A reference through finalAttrs.finalPackage denotes the package after the relevant overrides. That makes the identity of the object under test part of the declared relationship.

For an author building language-specific package helpers, the release notes describe a related simplification. A prototype package can contain common language logic and then be overridden. This reduces the need for an additional override layer around generic-builder arguments. The benefit is a more consistent override interface, not a promise that every existing helper has already adopted it.

My preference is for customization that retains the original package's declared relationships. The new constructor supports that by making final values available where those relationships are defined. It also makes a review question concrete: whether a derived field should refer to the previous contribution or the completed package.

Nix configuration becomes structured system configuration

Quokka also exposes the reworked daemon configuration through nix.settings. The release notes describe mappings from older options to their corresponding settings. The daemon module defines the settings as structured NixOS configuration and selects pkgs.nix as the default Nix package.

The Nix package definitions at the release tag select the stable 2.8 series, with version 2.8.1. Shipping that implementation does not make its experimental interfaces stable. Nix's feature declarations still govern whether the new CLI and flakes are enabled.

This is another example of separating a relationship from an incidental representation. A NixOS option can describe daemon configuration as a value in the system definition. The module then produces the configuration needed by the daemon. An operator can inspect the declared setting in the same source that describes the rest of the system.

The release notes also add more precise reload triggers for systemd services. A service can list reloadTriggers instead of relying only on the broader reloadIfChanged behavior. That gives the system definition another place to state which change requires which operation, although the appropriate reload behavior remains specific to the service.

Certificate configuration gains security.acme.defaults for settings shared by multiple certificates. The notes explain that this also permits DNS-01 validation when a web-server virtual host requests ACME through its enable option. Centralizing the shared declaration reduces repetition between certificates. It does not make the validation method implicit knowledge outside the system definition, since the defaults themselves remain part of that declaration. This is useful for configurations where several virtual hosts need the same certificate-management policy.

Stateful defaults remain a separate compatibility decision

A new system release does not imply that every stateful default should immediately change. The version module in 22.05 explains system.stateVersion as compatibility for defaults that affect persistent data. It explicitly says changing this value does not upgrade the system and that it should normally remain constant during a system upgrade.

The release notes supply concrete examples. New installations select Podman as the OCI-container backend, while systems with an older state version retain Docker as their default. The directories for NixOS containers also change, with migration responsibilities attached to manually advancing the state version on an existing system.

Those are different operations from replacing the system's package set. A declaration can select new software while retaining a stateful default appropriate for existing data. Making the release number and the state-compatibility declaration identical without examining their purposes would discard that distinction.

The same release defaults PostgreSQL to major version 14, which makes the version module's explanation particularly concrete. The module identifies database data compatibility as a reason to retain older stateful defaults. Declarative configuration can describe the selected package and service settings, but an existing database still has a format whose migration needs its own account.

Quokka's package-construction work applies the same care at a different level. A final-attribute reference tells the constructor how one setting depends on another after customization. The release gives authors a way to retain that relationship instead of requiring each override to reconstruct it manually.