I'm not mad, I'm disappointed

A short rant about the state of your nix code

Introduction

When I see another friend of mine start using nix my eyes glisten with the knowledge there is still hope for their lost soul yet. But then I observe their copy-pasted code and see all the bad practices everywhere. Sighhhh. Time to explain to you everything wrong about what you’re doing in your nix code. Make sure to pay attention and make some notes because I don’t want to explain it again.

system and lib.nixosSystem

Often I see people passing the system argument to lib.nixosSystem. However, you should not do this! This is because “legacy input” and will straight up be ignored because of your autogenerated hardware-configuration.nix file, which will set system for you. So to rectify this we can instead set nixpkgs.hostPlatform to the desired system.

{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = inputs: {
nixosConfigurations = {
# this host contains our `system` antipattern, which we want to avoid
myBadHost = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./hardware-configuration.nix ];
};
# this is our fixed host, notice how it uses `nixpkgs.hostPlatform`
myGoodHost = inputs.nixpkgs.lib.nixosSystem {
modules = [
./hardware-configuration.nix
{ nixpkgs.hostPlatform = "x86_64-linux"; }
];
};
};
};
}

For further reading about this topic please consult nixpkgs@332e603/flake.nix#L54.

To such I am usually asked “But I want to be able to track other nixpkgs revisions or inputs, then don’t I have to? Or set a specialArg or something?”. NO! Almost never do you want to use specialArgs. specialArgs are great but often overused for things that the module system was designed to do better. Now to answer the main question, you can still do that. Allow me to introduce you to pkgs.stdenv.hostPlatform.system and its alias pkgs.system. So now we can do something more like the following:

{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# notice our new nixpkgs input that is pinned to a stable version
nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-24.05";
};
# v this here is destructuring the inputs, and the `@` will allow us to refer to `inputs` as a whole
outputs = { nixpkgs, ... } @ inputs: {
nixosConfigurations.mySystem = inputs.nixpkgs.lib.nixosSystem {
# notice we are now we are now passing the inputs as a specialArg
specialArgs = {
inherit inputs;
# do NOT add `system` as a specialArg
system = "x86_64-linux";
};
modules = [
# instead notice how we can use `pkgs.system` to refer to the system output from nixpkgs 24.05
# and we didn't need to add `system` as a specialArg
({ pkgs, inputs, ... }: {
environment.systemPackages = [
inputs.nixpkgs-stable.legacyPackages.${pkgs.system}.nil
];
# this didn't change from our previous example
nixpkgs.hostPlatform = "x86_64-linux";
})
];
};
};
}

Overall, system being an input to lib.nixosSystem was so irritating me so much that I made several bluesky posts complaining about it. And went on to make easy-hosts as an abstraction around making systems for your systems that will prevent you from making these mistakes, and will force you to use the module system more.

system dependent overlays

I sadly am not kidding. These are real. I have seen people do this.

First off, overlays should not be consumed with a system like overlays.x86_64-linux or overlays.x86_64-linux.default. Please stop yourself if you ever get here. Instead, overlays should be consumed like source.overlays.name, to improve upon this consider the following template:

{
# inputs if you need them
outputs = inputs: {
overlays = let
pkgs = inputs.nixpkgs.legacyPackages.x86_64-linux;
in {
# these are all incorrect variations of the same code, that i have seen
# or would be generate by some kind of code
x86_64-linux = final: prev: {
hello = pkgs.callPackage ./default.nix { };
};
# this is bad
x86_64-linux.default = final: prev: {
hello = pkgs.callPackage ./default.nix { };
};
# this is still bad
default.x86_64-linux = final: prev: {
hello = pkgs.callPackage ./default.nix { };
};
# notice in this example we removed any reference to `pkgs` and instead we use `prev`
# this means that the overlay is not system dependent
default = final: prev: {
hello = prev.callPackage ./default.nix { };
};
};
# any additional outputs go here
};
}

Notice how here I used prev instead of pkgs or anything alike because we do not need to know what the system is here. And if you do wish to refer to the system recall how earlier I spoke of pkgs.system well prev like pkgs will also expose system, so you can use prev.system.

Stop importing weirdly

You do not need this needless import. This is a waste of nix eval time. Since users will place inputs.<source>.nixosModules.default in their flake imports it will load those files for them, so you don’t need to import unless you need to pass args.

{
inputs = { };
outputs = inputs: {
nixosModules.default = import ./module.nix;
};
}

Here is a more acceptable flake, where we only import because we are passing our inputs to our other module.

{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = inputs: {
nixosModules = {
noImport = ./module.nix;
importWithArgs = import ./module.nix { inherit inputs; };
};
};
}

Stop importing nixpkgs

This one is somewhat of a subset of the previous problem, where people import needlessly. nixpkgs already provides legacyPackages which is a pre-constructed set of packages for you. So you don’t need to import nixpkgs.

{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { nixpkgs, ... }: let
pkgs = import nixpkgs { };
# or
pkgs = import nixpkgs { system = "aarch64-linux"; };
in {
# imagine the rest of the file
};
}

Importing nixpkgs without a system will use builtins.currentSystem which is impure, this means that it needs to use information that may not be reproducible like your system type. Instead, consider writing:

{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { nixpkgs, ... }: let
system = "aarch64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
packages.${system}.default = pkgs.callPackage ./default.nix {};
};
}

You may also recall my earlier posts titled Experimenting with nix and or Dev dependencies, where I refer to a function called forAllSystems to improve upon this pattern further. For the sake of completion I will bring one of those examples back and use it to generate the system based outputs of packages.

{
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
outputs = { nixpkgs, ... }: let
forAllSystems =
function:
nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (
system: function nixpkgs.legacyPackages.${system}
);
in {
packages = forAllSystems (pkgs: {
default = pkgs.callPackage ./default.nix { };
});
};
}

You may however have noticed this won’t work if you need to access unfree or broken packages. In which case I would recommend modifying the function to align with the following:

forAllSystems =
function:
nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (
system: function (
import nixpkgs {
inherit system;
config.allowUnfree = true;
}
)
);

The key difference in the above example is that we now are importing nixpkgs and as such we now have access to nixpkgs.config and nixpkgs.overlays if you so wish to use those.

This brings me onto my final point,

Stop using flake-utils

flake-utils is often an extra dependency added for no good reason. The main purpose for using it more modernly is its overridable systems, they do this through an input from the nix-systems GitHub org. But more typically no one uses it for that, in fact most people use flake-utils for its ability to create system dependent outputs. This is what leads to the issues with system dependent overlays. But recall, I’ve already covered all of what you need to make your own flake-utils, through the usage of the forAllSystems function.

However, some of you may still want those overridable systems. In which case please consider writing this flake like this instead:

{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
systems.url = "github:nix-systems/default";
};
outputs = { nixpkgs, systems, ... }: let
forAllSystems =
function:
nixpkgs.lib.genAttrs (import systems) (
system: function nixpkgs.legacyPackages.${system}
);
in {
packages = forAllSystems (pkgs: {
default = pkgs.callPackage ./default.nix { };
});
overlays.default = final: prev: {
hello = prev.callPackage ./default.nix { };
};
};
}

Conclusion

I was never mad at you, I was just disappointed. But now you know better, so go forth and write better nix code.

© Blog post licensed under CC BY-NC-SA 4.0