commit draft

This commit is contained in:
Artemis Tosini 2024-04-12 00:41:01 +00:00
parent c2cb440c9a
commit 5984c44f9f
Signed by: artemist
GPG key ID: ADFFE553DCBB831E

View file

@ -0,0 +1,86 @@
---
layout: post
title: What is nixos-rebuild anyway?
date: 2024-01-29
---
If you've used NixOS before, you've almost certainly used the `nixos-rebuild` program before.
With one `nixos-rebuild switch` command you can build your updated system configuration,
add it to your bootloader as the default entry, stop all old services, and start any new services.
What you may not know is that `nixos-rebuild` is a bash script and you can do everything (relatively) easily without it.
The [full source code](https://github.com/NixOS/nixpkgs/blob/c074160dcfa338f8424c440ccb0f0a5412de0dbf/pkgs/os-specific/linux/nixos-rebuild/nixos-rebuild.sh) is quite long and includes many special cases, but most of these aren't necessary if you're building manually.
Some of this will make more sense if you know about two options `--build-host` and `--target-host`.
With these you can build in one computer, copy it to the destination, and install there.
Unforutnately, there's one important question we have to answer first:
# What is a NixOS?
NixOS is a very complicated way of defining options and setting them to a value.
For example, your configuration you could set:
```
environment.systemPackages = [ pkgs.git ];
```
The value is matched by an "option" of the same name which describes the default value and the type [^type].
This is how NixOS describes `environment.systemPackages`:
```
options.environment.systemPackages = mkOption {
type = types.listOf types.package;
default = [];
example = literalExpression "[ pkgs.firefox pkgs.thunderbird ]";
description = lib.mdDoc ''
<removed for brevity>
'';
};
```
The idea that makes this useful is that you can set values based off of each other.
For example, the gamescope service sets;
```
environment.systemPackages = mkIf (!cfg.capSysNice) [ gamescope ];
```
meaning that `gamescope` is only added to `systemPackages` if the option `programs.gamescope.capSysNice` is disabled.
So we have a big set of options with values, but that doesn't make it an operating system.
The piece that ties this all together is the "toplevel derivation", at `system.build.toplevel`.
The toplevel derivation is a package that takes all of your settings and stuffs them into
one directory. For example, when you set a kernel, the toplevel derivation sees that and puts it in `kernel`.
When a module wants to put a configuration file in `/etc` it creates a file in `etc`.
And all the packages in `environment.systemPackages` get linked together and put into `sw`.
If you want to see yours, then go to `/run/current-system` on a NixOS machine.
This will always have the version you're currently running [^booted-system].
# Step 0: Build nix
The script tries not to make too many assumptions about the build host. It must have a nix store,
but that doesn't necessariy mean it has a new enough nix to build your configuration, or that your configuration
is defined using only settings that your build nix can understand. Therefore, it downloads a newer nix if possible,
or builds one using your configuration. I'm not _entirely_ sure why this happens, but it does.
# Step 1: Building your system
Now that you have a nix to use, it's time to build your toplevel derivation. If you're using [flakes](https://zero-to-nix.com/concepts/flakes) that means tunning `nix build /etc/nixos#nixosConfigurations.$(hostname).config.system.build.toplevel`, which
## Wait what if you're building remotely?
Building a NixOS configuration is secretly two steps: evaluation and realisation.
Evaluation turns your nix code into a derivation: a file with build instructions
(environment variables, a build script, and arguments), a list of other derivations its needs before it can build,
and the outputs it will create when you run it.
Evaluation is always done on the local machine (where you run `nixos-rebuild`), because the build host might have different channels (for non-flake builds) or no access to the source of some inputs (for flake builds)[^flake-eval].
Realisation (you can think of it as building) takes a derivation and its tree of dependencies then creates the output paths by running each
build script in its own sandbox (or downloading the result from a trusted substituter, often [cache.nixos.org](https://cache.nixos.org/)). Most of the hard work happens here.
In order to get the derivations from the local machine to the build host,
`nixos-rebuild` uses `nix copy --derivation --to`,
which works just like `nix-copy-closure` but copies derivations instead of the entire closure.
Thn the hard work can happen on the destination without needing to copy all the nix source code and dependencies.
# Step 2: Add a profile
# Step 3: Activate
[^booted-system]: ... for a certain definition of "running". Software is loaded from here but the kernel and modules will be in the version in `/run/booted-system` because Linux can't load modules from other kernel versions. This is only setup at boot and won't be changed by a `nixos-rebuild switch`.
[^flake-eval]: It is actually possible to evaluate flakes on the remote machine, but this isn't supported. The `nix flake archive` command, which copies a flake and all of its inputs to the nix store, can copy to another machine with the `--to` argument. Building this way works but I haven't bothered writing a patch.
[^type]: In NixOS `type` defines not just "can I set this to a string or only a list" but also what happens when multiple conflicting options are set. If you set `environment.systemPackages = [ pkgs.git ];` in one file and `environment.systemPackages = [ pkgs.mercurial ];` then the result will be `[ pkgs.git pkgs.mercurial ]` because `listOf` says to merge them.