Nix 2.28 reorganizes the headers used by downstream libraries
Nix 2.28 branches from 2.27 to deliver interface changes needed by dependents. Explicit component paths and configuration headers complete infrastructure work around the Meson transition.
Changing a project's build system can expose assumptions in the headers other projects compile against. Nix 2.28 is an unusual release created to address that problem before the planned NixOS 25.05 integration. It branches directly from 2.27, carrying interface-breaking header changes that the maintainers considered inappropriate for a patch release.
The April 2 release notes explicitly call it a continuation of 2.27 rather than an ordinary feature release. They describe the work as completing infrastructure changes for the Meson switchover. The versioned notes preserve the same explanation alongside the released source.
I value Nix's declarative model because it makes dependencies part of the configuration. Header lookup is another dependency relationship that benefits from explicit names. A build that succeeds because one directory happens to be searched first contains an assumption that its source has not fully expressed.
Header names now identify their component
Installed C++ headers now begin with nix/, followed by the component and the remaining header path. The release notes use a store header to illustrate the change. A downstream include that previously named only derived-path.hh now names the owning component too.
#include "nix/store/derived-path.hh"
The additional path is meaningful. It identifies the store component instead of relying on a build system to add a component's private include directory to the lookup path. This makes the source declaration more specific about the header it needs.
The associated pkg-config files now provide the general include directory, rather than the directory with nix already appended. That agrees with source files including the nix/ prefix themselves. Include spelling and compiler search paths have to change together, otherwise a downstream can accidentally search a duplicated or incomplete path.
The notes explain why older abbreviated includes sometimes worked. A C preprocessor can search the directory containing the current file as well as configured include paths. A header could therefore find another header incidentally through its own location. Success under that arrangement did not establish that the installed interface was consistently described.
Public headers include the configuration they require
Nix also removes the requirement to force configuration headers into compilation through -include. Regular headers now explicitly include the configuration headers they need. A consumer no longer has to recreate that hidden precondition merely to use the installed declarations.
This addresses a different dependency from the filesystem path. A header may have the correct name but still rely on macros introduced elsewhere. If the necessary configuration is only present because every compilation command injects another file, the header's own dependencies remain incomplete.
The new arrangement puts that dependency near the declarations using it. It can also distinguish configuration for different Nix components. The release-note example names store and expression configuration headers separately, which reflects the components exposed by the reorganized include tree.
Installed configuration headers are smaller in scope too. Implementation-specific definitions move to headers used only while compiling Nix itself. Remaining public macro names acquire a NIX_ prefix. This reduces the information downstreams receive to the configuration relevant to the interface they consume.
The renamed macros are a real source compatibility change. The notes show HAVE_ACL_SUPPORT becoming NIX_SUPPORT_ACL, along with other replacements. A downstream using the old names must follow the new public definitions, rather than assuming the build-system migration only changes how Nix's own binary is produced.
The example also renames the general config.hh include to nix/util/configuration.hh. The notes explain that this distinguishes it from component configuration headers. Similar filenames can carry different responsibilities, so the more specific path communicates both which component owns a declaration and whether the file describes configuration values or the configuration interface.
The release boundary communicates a migration
The maintainers had intended 2.27 to become the Nixpkgs default for NixOS 25.05. Dependents linking against Nix needed the revised interface, which prompted this separate release. Branching from 2.27 limits the change set while providing a version boundary for those dependents.
That decision acknowledges a difference between command users and library consumers. A shell script calling a Nix command does not include these C++ headers. A downstream library compiling against Nix has a source dependency on them, including their macro and path conventions. An interface change can therefore matter substantially without adding a user-visible language feature.
The notes label the C++ API unstable, but still explain why the changes do not belong in a patch release. Unstable status does not make version communication useless. A distinct release gives downstreams a recognizable point at which to adapt their includes and build configuration.
This is the part of the release I find most relevant to maintaining infrastructure tooling. The packaging boundary needs to communicate when another component's source must change. A version can carry that information even when most ordinary command usage behaves like the preceding release.
The C API removes one global initialization step
The release also removes nix_flake_init_global. Its replacement adds flake settings to an evaluation-state builder through nix_flake_settings_add_to_eval_state_builder. The notes describe this as removing global state to improve modularity.
The order shown in the migration is concrete. A caller creates the evaluation-state builder, then adds the flake settings to that builder. That associates configuration with the builder involved in creating the evaluation state, rather than invoking a separate global initializer first.
The maintainers say they generally try to preserve C API compatibility. They make an exception here because the removed function is young and believed to have limited use. That is a narrower claim than saying the C API has no compatibility intent, and it explains why this change accompanies the header migration.
Nix 2.28 therefore completes infrastructure work at the points where dependents compile and initialize against Nix. Explicit component names identify installed headers, those headers declare their configuration dependencies, and flake settings move into the evaluation-state construction path. The release boundary gives downstreams a specific version against which to make those source changes.