March 7, 2022·4 min read·engineeringnixflakescompatibility

Nix puts default flake outputs inside their named collections

Nix 2.7 standardizes default packages and development shells as entries named default. Compatibility fallbacks keep older output names usable while flake check reports the deprecated forms.

A flake that exports several packages already needs names for them. Giving its default package a separate top-level convention makes consumers learn another structure for a choice within the same collection. Nix 2.7 changes that convention so the default package lives at packages.<system>.default. The March 7 release notes describe the corresponding changes for development shells, applications, and other outputs.

The default becomes a named entry in the collection it belongs to. Existing output names continue to work, but nix flake check warns about them. This is a convention change with a compatibility period, not an immediate requirement to rewrite every flake before using the new client.

Package selection becomes structurally consistent

The 2.7 installable-selection implementation shows how a command finds its default package. It tries packages.<system>.default before the older defaultPackage.<system>. The current system determines the platform-specific part of that path.

That ordering has a practical consequence if a flake exports both names with different values. The new entry wins for this default lookup. A transition alias should therefore refer to the same derivation when the intention is to offer one package under both conventions. Two definitions with different results would encode a behavioral choice in the client's fallback order.

Here is a structural example for a flake that already has Nixpkgs as an input. It exposes a normal named package and selects the same derivation as its default.

outputs = { self, nixpkgs }: let
  system = "x86_64-linux";
  pkgs = nixpkgs.legacyPackages.${system};
in {
  packages.${system} = {
    hello = pkgs.hello;
    default = pkgs.hello;
  };
};

This fragment belongs inside flake.nix, with the input declaration supplied by the surrounding file. It intentionally supports one stated system. The example demonstrates output naming without implying that declaring one platform automatically builds or exports every other platform.

I find this arrangement easier to inspect because the default and the alternatives occupy the same collection. Declarative configuration is most useful to me when its structure reveals which values can be selected. A special default elsewhere in the output tree makes that relationship less immediate.

Development shells follow the same pattern

The older devShell.<system> output becomes devShells.<system>.default. This also creates a natural location for multiple named environments. The development-command implementation in 2.7 tries the new default shell path first, then the old shell path, and then the ordinary default-package paths.

outputs = { self, nixpkgs }: let
  system = "x86_64-linux";
  pkgs = nixpkgs.legacyPackages.${system};
in {
  devShells.${system} = {
    default = pkgs.mkShell {
      buildInputs = [ pkgs.hello ];
    };
    minimal = pkgs.mkShell { };
  };
};

The relevant change is the shape of the exported attributes. The shell definitions themselves still describe their environments. A consumer can select the default or request a named alternative without the flake inventing a separate naming scheme for additional shells.

The mkShell definition in Nixpkgs 21.11 supplies the environment constructor used here. It accepts the input lists and constructs a derivation intended for shell use. Exporting that result under devShells identifies its role to the flake consumer. Renaming the containing attribute does not turn the environment into an ordinary application package or change which input list the constructor receives.

The fallback to package outputs also explains why a successful nix develop invocation does not by itself prove that the flake contains an explicitly defined development shell. The command has other candidate outputs. Inspecting the exported attributes establishes which definition the command is selecting.

That is useful when diagnosing an unexpected environment. A missing shell output and a shell with missing dependencies are different problems. The selection order determines whether a command can still resolve another installable after the intended shell path is absent.

A warning marks the migration without removing the fallback

The flake-check implementation reports deprecated output names and recommends their replacements. The command's documentation defines its broader job as checking that outputs evaluate and that derivations in checks build successfully.

That distinction matters when reading a check result. The naming warning reports a deprecated convention. It does not establish that the underlying package is incorrect. A build failure and a convention warning describe different conditions even when both appear during the same command.

Compatibility also has two directions. The new client retains lookups for old output names, which helps existing flakes. A flake that publishes only the new structure is a separate compatibility question for older consumers. The release note's statement that old names still work should not be expanded into a promise about every older client understanding every new default convention.

My preference is to make a transition explicit in the exported definitions when older consumers remain a requirement. Keeping two names tied to one value avoids accidental disagreement while those consumers are still supported. The long-term interface can then use the collection's default entry consistently.

Other defaults lose their special top-level forms

The release notes also move the default template into templates.default and the default overlay into overlays.default. Bundlers receive a platform-specific collection with a default member. These changes apply the same structural idea to different kinds of output, although the values themselves have different required types.

Applications use apps.<system>.default. The run-command implementation confirms that the old application fallback is spelled defaultApp.<system>, in the singular. The release note's list uses defaultApps, so the implementation is the more precise source for that old name.

Nix 2.7 also changes the bundler function interface, which the release notes identify separately as a breaking API change. That should not be confused with renaming a default output. A bundler's new callable shape concerns what its function accepts and returns, while the collection convention concerns how a consumer finds that function.

For package and shell authors, the immediate improvement is concrete. The value selected when no name is supplied can live next to the named alternatives. Commands prefer that location, and the checker can identify older declarations that still depend on the compatibility fallback.