---
title: I'm not mad, I'm disappointed
description: A short rant about the state of your nix code
date: 2025-01-09
updated: 2025-02-07
tags:
  - nix
image: im-not-mad-im-disappointed.png
atUri: "at://did:plc:qxichs7jsycphrsmbujwqbfb/site.standard.document/3mn5hpcdx3l22"
---

## 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.

```nix
{
  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](https://github.com/NixOS/nixpkgs/blob/332e6030634ce7701496564d4c70ae8209919931/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:

```nix
{
  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](https://github.com/isabelroses/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:

```nix
{
  # 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.

```nix
{
  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.

```nix
{
  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`.

```nix
{
  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:

```nix
{
  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](/blog/experimenting-with-nix-7) and or [Dev
dependencies](https://isabelroses.com/blog/nix-shells-8), 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.

```nix
{
  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:

```nix
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](https://github.com/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:

```nix
{
  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.
