NixOS

The canonical source of truth is the Nix code.

There is a search engine for options.

testing configuration

I couldn't get nix-instantiate --eval to play well with flakes, so I use the following setup.

Although I don't usually use a REPL for most languages (python, julia, e.g.), preferring file-based development, unfortunately I couldn't find a more ergonomic setup than using the REPL.

I wrap nix repl with a shell script like

#!/run/current-system/sw/bin/sh

nix repl \
  --file "$(dirname "$0")/nixos-repl.nix" \
  --argstr username "$(whoami)" \
  --argstr hostname "$(hostname)" \
  --argstr path "/keep$HOME/.config/home-manager"

that loads nixos-repl.nix placed in the same directory.

{ username, hostname, path }:

let
  self = builtins.getFlake path;
  nixos = self.nixosConfigurations.${hostname};
in
{
  inherit self;
  ${hostname} = nixos;
  ${username} = nixos.config.home-manager.users.${username};
  inherit (nixos) config pkgs options;
  inherit (nixos.pkgs) lib;
}

This can be then used like

nix-repl> <hostname>.config.

and press <tab> to see completions. :p can be used to force evaluation.

If I have something I want to inspect over and over again, I use a wrapper of nix eval

nix eval ".#nixosConfigurations.$(hostname)" --apply "$(hostname): $1"

like so.

nixos-eval "<hostname>.config.system.activationScripts.usrbinenv"

updating

Simple script to make sure /boot is mounted before updating.

#!/run/current-system/sw/bin/sh

if [ ! "$(findmnt /boot)" ]; then
    sudo mkdir --parents /boot
    sudo mount --onlyonce /dev/nvme0n1p1 /boot
fi
sudo nixos-rebuild switch

nixos-rebuild has a few possible commands.

  • switch: activate and make boot default
  • boot: make boot default but don't activate
  • test: activate but don't make boot default
  • build: neither activate nor make boot default
    • the result is a symlink placed in ./result

removing channels and flake registries

Channels and flake registries are unnecessary and a source of impurity as they are unpinned.

In NixOS the following configuration disables channels.

{
  nix.channel.enable = false;
}

In Home Manager the following configuration disables flake registries.

{
  nix.settings = {
    experimental-features = [ "nix-command" "flakes" "repl-flake" ];
    flake-registry = "";
    use-registries = false;
  };
}

Note flake-registry controls the global registry while use-registries controls user registries.

It is convenient to replace the nixpkgs reference with the shell script nixpkgs.

#!/run/current-system/sw/bin/sh

flake="/keep$HOME/.config/home-manager"
# `--impure` as the flake may be dirty and considered unlocked
# error: cannot call 'getFlake' on unlocked flake reference
nix eval --impure --raw \
  --expr "(builtins.getFlake \"$flake\").inputs.nixpkgs.outPath"

Then commands like

nix-shell -I nixpkgs=flake:nixpkgs -p python3
nix shell nixpkgs#python3

can be replaced by

nix-shell -I nixpkgs=$(nixpkgs) -p python3
nix shell $(nixpkgs)#python3

which has the advantage of not requiring internet as it uses the NixOS configuration's nixpkgs.

In addition, a dependency lookup like

nix why-depends /nix/var/nix/profiles/system $(nixpkgs)#nss

is more accurate as it uses the same version of the package as the system.

miscellaneous

installing from arch

One can switch from Arch Linux completely "in-place", i.e. without re-partitioning any drives. This can be done by prototyping with kexec to get a working configuration and then using NIXOS_LUSTRATE through the lustrate mechanism (which will move the old root partition to /old-root).

After this, it's still possible to get into arch with chroot, e.g.

sudo chroot /old-root /bin/bash
/bin/pacman -Q

Note that commands need to be fully qualified as $PATH is still from NixOS.

live boot

Live boot can be done from an Arch live boot (see NixOS Wiki - Change root).

cryptsetup open /dev/nvme0n1p3 cryptlvm
mount /dev/VolumeGroup/root /mnt
mount -o bind /dev /mnt/dev
mount -o bind /proc /mnt/proc
mount -o bind /sys /mnt/sys
chroot /mnt /nix/var/nix/profiles/system/activate
chroot /mnt /run/current-system/sw/bin/bash

/bin/sh

Having /bin/sh is technically an impurity as applications can reference it without knowing the exact version. However, it's required to do system() calls in libc, so it can't be easily disabled entirely. This can cause issues with reproducibility but progress in fixing this seems to have stalled.

(see also: NixOS Discourse - Add /bin/bash to avoid unnecessary pain, NixOS Wiki - Command Shell)

I think the cleanest thing to do is to use the default sandbox shell provided in the default stdenv, currently a statically linked ash shell from busybox. The reasoning being that if /bin/sh matches the one used at build-time, there's less chance of a runtime error due to possible incompatibility.

environment.binsh = "${pkgs.busybox-sandbox-shell}/bin/busybox";

It does warn about changing from bash, but considering it's over 10 years old, it's probably fine now.

/usr/bin/env

Like /bin/sh, /usr/bin/env is an impurity that does not exist at build time.

Unlike sh, it can be disabled relatively easily.

environment.usrbinenv = null;

This can cause issues for unpatched software that rely on env, e.g. prettier installed with npm.

There are (currently) also a few minor spurious errors that should be fixed, see this issue.

system.activationScripts.usrbinenv =
  lib.mkIf (config.environment.usrbinenv == null) (
    lib.mkForce ''
      rm -f /usr/bin/env
      mkdir -p /usr/bin
      rmdir --ignore-fail-on-non-empty /usr/bin /usr
    ''
  );
systemd.services.systemd-update-done.serviceConfig.ExecStart = [
  "" # clear
  (
    pkgs.writeShellScript "systemd-update-done-wrapper" ''
      mkdir -p /usr
      ${pkgs.systemd}/lib/systemd/systemd-update-done
      rmdir --ignore-fail-on-non-empty /usr
    ''
  )
];

vulnix

vulnix scans the dependencies of the entire system for CVEs.

vulnix --system