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.