Build NixOS config without environment dependencies and have `nixos-option`/`nixos-rebuild` support?

I am still a relative beginner in all of this, so excuse me if I say wrong things, but please do correct me so I can learn and improve.

I find the normal way of building a NixOS configuration somewhat odd (considering Nix encourages not relying on environment state). I bet I am misunderstanding this. Please help me fix my mistakes.

The method I currently use is dependent only on explicit inputs, i.e. nix-build <nix-files-root>, and I can switch to it and so on if I want.

By <nix-files-root> I do NOT mean something defined in NIX_PATH, depending on NIX_PATH is exactly the state I am trying to avoid when building the system (note that on random nix-builds I do not mind the extra convenience). This is just syntax to mean some path on my system, for this thread.

By “explicit inputs” I mean things which are not:

  1. nixpkgs/nixos in /etc/nixos (or is it nix-channels directory?) or somewhere pointed to by NIX_PATH, at some revision that is not specified in the system’s nix expression
  2. A nixpkgs config file you might or might not have in your home directory
  3. Overlays you may or may not have in your home directory
  4. nix channels
  5. ? I don’t know what else as I am not familiar with this way of building

Each of these things are outside of <nix-files-root> and yet affect the system output and so, hurt reproducibility. You might say that I can have those files in <nix-files-root> and symlink but then I have to do the symlink to get to that state, If I forget and there happens to be different files there, I get a different system.

The method I use works for me. The reasons I made this post are the following:

  1. I am curious as to why NixOS with its emphasis on avoiding state and being declarative and reproducible has these stateful defaults. Is it to be user-friendly? If so, that’s appreciated. But I am not sure this is easier to grasp for beginners than having everything derived from your nix expressions without dependencies on stuff in the environment.
  2. The method I use breaks when using tools like nixos-rebuild, nixos-option unless I jump through a couple of hoops, that start to make the system building more stateful. I am probably approaching this in the wrong manner so I would be very appreciative if people share their ways of doing this. I don’t really know what I am doing.

The method I use is instead of having a configuration.nix like so:

{ config, lib, pkgs, ... }:
{
  ...config...
}

I have a default.nix in some <nix-files-root>/<host-name> path that looks like so:

let
  defaultPkgs = import ../nixpkgs {};

in { pkgs ? defaultPkgs } :
  import "${pkgs.pkgsSrc}/nixos" {
    system = "x86_64-linux";

    configuration = {
      ...config...
    };
  }

I think I got this idea from Gabriel Gonzalez blog post, but it was a long time ago so excuse me for not remembering exactly where.

As you can see from defaultPkgs nixpkgs itself ALSO comes from some nix exprssion, that fetches the repo at the needed revision and applies config and overlays that are all defined as nix expressions inside this same <nix-files-root> without depending on files in /etc/nixos, /home/<user>/.config, nix-channels directory, and without depending on having nixpkgs/nixos as it is fetched along with the nixpkgs repo for the build. There is also the added advantage that the system can now be built just like every
other thing is built with Nix using nix-build. Yes you have to invoke a second command to choose if you want to test, switch or none at all. I just have a bash function to do that along with adding to the system profile so it is selectable from the boot menu.

In order to be able to use nixos-option I had to have a configuration.nix in the format shown above with an extra nixpkgs.config = import ... to get the same config I use for my regular setup. This is not so bad. But I am also forced to clone the nixpkgs repo somewhere, and set NIX_PATH nixpkgs/nixos to point to it, as well as nixos-config to point to the configuration.nix I just made. The nix files are in a git repo so now I need to have the nixpkgs repo as a submodule correct? And when I want to use a different revision, instead of changing a revision inside a nix expression, I now have to change the revision of the submodule instead?

Is there an easy way to have <nixpkgs/nixos> being fetched during the nix-build from the nixpkgs repo (and have config/overlays applied as defined by what I am building rather than environmental files)? Rather than using nix-channels or some other place pointed to by NIX_PATH? And still have support for nixos-option/nixos-rebuild/who-knows-what-other-commands-in-the-future?

In other words, is it possible to adjust the current scheme I use to support these tools?

They fail because <nixpkgs/nixos> is not found. But I don’t know the path in the nix store it’s going to have and it’s going to change every couple of builds, so I can’t possibly set it in NIX_PATH.

BTW I am using nix-eval --instantiate instead at the moment as an alternative to nixos-option but it’s not as good.

Any intention to help in fixing my understanding or methods is greatly appreciated!

Looking forward to using NixOS the way it was meant to be used so that I can get support from NixOS tools.

1 Like

You may be interested in my suggestion in this GitHub issue on pinning configuration.nix, as it solves your main question of using nixos-rebuild transparently with pinned nixpkgs/nixos.

These days I updated the solution slightly and export $NIX_PATH to match the pinned version, such that home-manager picks up the system’s nixpkgs without additional setup. You can deduce from the way nixos-config is defined that in this case per-machine configuration is organized in $PREFIX/nixos/machines/<machine>/.

It gets easier if you have only one machine, then you can just fix configuration to a file name instead of my somewhat confusing round-tripping with environment variables.

$PREFIX/nixos/default.nix:

# ideally this would be the *only* entry point to each machine, but then
# 1) this file is the same everywhere, as it imports the same relative paths
# 2) since `nixos` lives in the same repository as `nixpkgs`, and
#    `nixos-rebuild` searches `<nixpkgs/nixos>`, we have to point it at
#    `machines/$machine/nixos`, which is annoying. that is also why this file
#    lives at the toplevel and the repository around it is conveniently named
#    `nixos` (only slightly less annoying).

{
  system ? builtins.currentSystem,
  configuration ? <nixos-config>,
  ...
}:
import "${import ./common/nixpkgs.nix}/nixos" { inherit system configuration; }

$PREFIX/nixos/common/nixpkgs.nix:

fetchGit {
  name = "nixpkgs-channels";
  url = "https://github.com/NixOS/nixpkgs-channels";
  ref = "nixos-19.09";
  rev = "dae3575cee5b88de966d06b11861c602975cb23a";
}

$PREFIX/nixos/common/default.nix:

{ config, pkgs, ...}:
{
  nix.nixPath = [
    # NOTE: updated values are only available on a fresh user session
    "nixpkgs=${(import ./nixpkgs.nix)}"
    # XXX: spell out the filename for `nixos-rebuild edit` to work
    "nixos-config=${toString ../machines}/${config.networking.hostName}/default.nix"
  ];
  config.environment.systemPackages =
  let
    nixos-rebuild = pkgs.writeShellScriptBin "nixos-rebuild" ''
      exec ${config.system.build.nixos-rebuild}/bin/nixos-rebuild -I ${toString ../../.} $@
    '';
  in [ nixos-rebuild ];
}
2 Likes

nixos-rebuild secretly is only a very thin wrapper around nix-build -A config.system.build.toplevel + ./result/bin/switch-to-configuration

I use NixOS without nixos-rebuild

I haven’t gotten nixos-option to work yet though

FWIW if Flakes ever lands the whole NIX_PATH approach that nixos-rebuild and nixos-option currently use will probably be deprecated or removed I think; and using a nixos configuration file regardless of filesystem location is going to be easier

2 Likes

Thank you for suggesting this method.

It sounds more robust than mine and supports nixos-rebuild as well.

It is pretty comprehensive, so I will need to invest some time in order to fully grok what is going on however :slight_smile:

Indeed I noticed that nixos-rebuild is just a bash script that runs some nix commands along with depending on that dreaded <nixpkgs/nixos>

Your method avoids nix-rebuild, like mine but I am a bit confused about the environment setting that you do. Are you avoiding use systemPackages ? Why would you need to set your environment like that unless of course you are on a non-NixOS distro?

I avoided reading about flakes until now, as I still feel like I’m not on top of the current Nix situation, so I was not compelled to already look into future Nix stuff. But now that you mention it in relation to this problem, I will see the NixCon talk now

Both @fricklerhandwerk and @arianvp suggested alternative methods to the current status-quo which I shall look into and I’m grateful for, but that still leaves the first question open:
Why was the default method chosen to be as it is?

For me it feels like I am missing something. The people behind this whole solution are much smarter than I am, so rather than thinking I know better, my first instinct is to think I am misunderstanding. And I still think I am.

On NixOS I use

systemPackages = [ my-environment ];

But I also have some non-nixos boxes where I install it with nix-env instead.

Hence all the systemPackages in a separate file

1 Like

I think honestly some choices where made that where thought of as convenient back then but Eelco now realises those choices weren’t always great. And being explicit about inputs can actually be useful.

He wants to get rid of channels, NIX_PATH (and thus global paths) completely with flakes exactly for the downsides of unclear reproducibility guarantees that you brought up. I advice having a look at his talk and the RFC. I think it resonates well with your observations and opinions.

1 Like