May 31, 2024·6 min read·engineeringnixnixosreproducibilitypackaging

NixOS 24.05 makes package sources and foreign binary failures clearer

Uakari connects interactive package lookups to the system's Nixpkgs source and installs a diagnostic ELF loader. Both changes clarify assumptions that previously surprised users.

A system built from a pinned Nixpkgs source can still leave an interactive command asking a different source for packages. NixOS 24.05 makes that less surprising for flake-based configurations. When a system uses nixpkgs.lib.nixosSystem, its default package lookup and system-wide registry now refer to the Nixpkgs version that built the system.

The 24.05 release notes date Uakari to 31 May. Alongside the kernel update to Linux 6.6, the release introduces another small but useful clarification. A stub ELF loader explains why a dynamically linked executable intended for a generic Linux environment cannot start on NixOS out of the box.

I find these changes relevant to the practical appeal of a declarative system. A declaration is easier to understand when nearby interactive commands resolve the same package source. A failure is easier to diagnose when it names the missing assumption instead of leaving the user to infer which part of execution failed.

Interactive package names follow the system source

The release-tagged notes give two examples of the new default. nix run nixpkgs#hello and nix-build '<nixpkgs>' -A hello now work without extra lookup configuration and can reuse dependencies already present on the system. The two commands use different lookup conventions, but the module connects both to the system's source.

The Nixpkgs flake module explains the mechanism. It records the Nixpkgs source path used to build the system. The system-wide flake registry maps nixpkgs to that store path, and the configured NIX_PATH supplies nixpkgs=flake:nixpkgs for angle-bracket lookup.

The result is an indirection from the older lookup form through the registry entry. That is more precise than saying that every use of the word Nixpkgs is globally pinned. The module establishes particular defaults for the system's interactive lookup paths. A command that explicitly chooses another source is still expressing another choice.

The module also makes the dependency between the options explicit. Setting nixpkgs.flake.setNixPath requires nixpkgs.flake.setFlakeRegistry, because the former's implementation depends on the latter. They are related settings, not independent switches that happen to have similar effects.

This helps explain an otherwise confusing situation. The system can be fully described by one input while an unqualified interactive package request consults another input. Neither request has to be invalid for them to differ. The new default connects them, reducing the number of implicit source choices a user has to keep in mind.

Keeping the source has a closure cost

The release notes identify a cost for systems that will not run Nix commands interactively. Retaining the source makes the system closure depend on Nixpkgs. A closure-size-constrained machine may not need that source merely to run the software already selected for it.

The options allow that system to decline the defaults explicitly. The following module fragment selects the smaller-source-retention policy described by the release notes.

{
  nixpkgs.flake.setNixPath = false;
  nixpkgs.flake.setFlakeRegistry = false;
}

This choice does not describe a different operating-system configuration language. It changes whether the deployed system carries and exposes the source for those lookup mechanisms. A workstation that runs package commands frequently has a different reason to retain it from an appliance whose package choices are all made elsewhere.

The source option can also be set for a non-flake configuration that uses pinned sources. The module documentation explicitly describes that possibility. Automatic setup through nixosSystem is the convenient default, but the connection between lookup and a recorded source is not conceptually limited to one way of writing the system configuration.

I like the explicit cost here. Keeping interactive commands consistent with the deployed system is useful, and the configuration names the source retained to provide that consistency. A machine that does not need the capability can make a different declaration without pretending it has the same interactive environment.

The registry module uses default definitions for the registry target and Nix path. That implementation choice matters because an explicitly supplied system policy can take precedence. The module supplies a useful starting point instead of treating a generated value as an unchangeable global rule. Its assertion checks the dependency between the two exposed switches, which prevents enabling the indirection while disabling the registry it needs.

There is also a distinction between the source used to build the system and the source of the calling configuration itself. The module comments explain that it does not add a nixos-config entry to NIX_PATH. Recording the Nixpkgs input does not establish how to invoke the entire system configuration. The new defaults connect package lookup to the package source, while leaving the broader system entry point to the workflow that selected it. That is a narrower and more useful statement than saying every interactive command now knows the whole configuration.

A foreign executable now gets a specific explanation

The new stub loader concerns a different boundary. The module implementing it builds a small static program that prints an explanation when it is invoked as the loader. Its message says NixOS cannot run dynamically linked executables intended for generic Linux environments out of the box.

The implementation also prints the executable name and exits with status 127. It does not attempt to locate and load the foreign program's dependencies. Its job is diagnostic. That distinction matters because installing the stub does not establish compatibility with the executable the user downloaded.

The release notes describe it as an informative error for binaries not made for NixOS. The actual message narrows the explanation to dynamically linked executables intended for generic environments. That is the useful case to keep in view when explaining why a file can be present and executable while still failing to start as expected.

The command's failure is not evidence that its own application code ran and rejected the machine. The loader is an earlier stage of execution. By supplying a targeted explanation there, the system directs the user toward the environment assumption that prevented the application from starting.

Diagnostic and compatibility loaders have different jobs

The stub is enabled by default through environment.stub-ld.enable. The release notes say that enabling programs.nix-ld.enable automatically disables it. Users of that configuration do not need an additional change merely to avoid the diagnostic loader replacing their chosen arrangement.

This is a useful example of making defaults cooperate with an explicit alternative. A diagnostic stub helps when no compatibility arrangement has been selected. Once the configuration requests a different loader path through nix-ld, the stub's role would no longer be appropriate. The release states that interaction directly.

Disabling the stub alone removes the diagnostic facility. It does not promise that the foreign executable will then work. That follows from what the module actually supplies, which is a static explanation program. The requested runtime environment still has to exist in whatever form the system configuration chooses.

The implementation provides a corresponding 32-bit stub on x86-64 systems. That is another detail of the diagnostic coverage, not a claim that every architecture-specific executable can run. The module chooses the stub through the existing loader configuration points and leaves compatibility behaviour to the explicitly selected alternative.

The kernel default remains a separate choice

Uakari changes the default kernel package from 6.1 to 6.6. Its notes state that all supported kernels remain available. A default update therefore changes the ordinary selection while preserving the configuration's ability to name another supported choice.

The release also makes an optional system without Perl possible through the perlless profile. Two activation scripts previously required Perl, and the profile selects replacements. That is relevant to closure composition, but it does not mean the ordinary system silently removes Perl or that applications can no longer depend on it.

These details reinforce the distinction between a distribution default and a universal restriction. The flake lookup behaviour is a default with explicit size-related opt-outs. A stub loader supplies an explanation and yields to a selected compatibility mechanism. The kernel update changes the normal package while retaining supported alternatives.

For a user maintaining declarative machines, the package-source connection is especially useful. An interactive request can now resolve through the same source that defines the deployed system, making its result easier to relate to the configuration. The loader diagnostic provides a corresponding explanation when an executable assumes an environment that configuration has not supplied.