Skip to content

Latest commit

 

History

History
910 lines (728 loc) · 32.3 KB

README.md

File metadata and controls

910 lines (728 loc) · 32.3 KB

nixos-pxe-installer

A set of modules to perform a fully automated installation of a customised NixOS system over the network via PXE using an NFS-mounted root file system.

Overview

The system is composed of two components: the module modules/install-image.nix, which creates an install image of a NixOS system and the module modules/installer-nfsroot.nix, which creates a generic installer for such an image.

An install image is a tar archive of a (almost) complete NixOS system created from a given nixpkgs source tree in the form of a NixOS channel and an arbitrary NixOS configuration. The image contains the closure of the corresponding top-level system configuration as well as the channel from which it was created. This is essentially the result of an invocation of the nixos-install command.

The installer provides the files required for a PXE-based network boot of the client system on which the install image is going to be installed. It mounts a generic root file system over NFS which contains a copy of the install image and some client-specific configuration parameters. The installer then partitions and formats the local disk, unpacks the install image onto it, performs the final activation of the NixOS configuration and reboots the client into the new system.

The install image is completely self-contained in the following sense: no contents is transferred from the installer and all components that are being created during the final acitvation are derived from the install image within a chroot environment. In particular, the NixOS versions from which the installer and install image are derived are completely independent.

Because the install image contains the closure of the system configuration, all components needed to activate the final configuration on the installed system should already be present and there should not be any need to fetch source packages or substitutes at that point. However, the final configuration necessarily differs from the configuration that was used to create the system closure due to hardware-dependent features that are not known ahead of the actual installation procedure.

The hardware-dependent configuration is generated by executing the nixos-generate-config command, which creates the file /etc/nixos/hardware-configuration.nix, overwriting the genereic hardware configuration used for the creation of the install image. The resulting differences may lead to the building of store paths which are not part of the original system closure.

Some of those store paths can be created locally (e.g. the file system table for the root device) but others may require fetching of sources or subsitutes, like the derivation of the initrd RAM disk used to bootstrap the system due to the inclusion of kernel modules that were not required by the original configuration.

For this reason, a number of additional store paths that are not part of the initial system closure are included in the install image. The list of these paths has been determined heuristically and is not guaranteed to be sufficient for all possible installation targets.

In any case, as long as the system being installed has network connectivity to the binary cache of the NixOS channel provided by the install image and/or the systems that provide the sources for all derivations that need to be built, the installation process will always succeed.

HowTo

This section provides a walk-through of the steps needed to install a system using default settings for the creation of the installer and install image. The documentation section describes the installer and install image modules in detail.

Setting up the installer

Building

To create an installer, clone into the nixos-pxe-installer repository and create the file installer.nix containing the Nix expression for the desired configuration of the installer as described in the section about installer examples, e.g.

{ system ? "x86_64-linux" }:

with import <nixpkgs> { inherit system; };
with lib;

let

  nfsroot = (import <nixpkgs/nixos/lib/eval-config.nix> {
    inherit system;
    modules = [ modules/installer-nfsroot.nix ];
  }).config.system.build.nfsroot;

in
  with nfsroot;
  [ nfsRootTarball bootLoader ]

Then execute nix-build installer.nix in the top-level directory. This will create two derivations containing the components of the installer (in this example, all derivations have already been built in a previous invocation)

$ nix-build installer.nix
/nix/store/p89a676j5gv6bmwwqy0ar67p6y8csyrd-nfsroot
/nix/store/6bxm87wczxgvm9bah6sy8gxlr5w4vpm6-grub-efi-bootloader

$ ls -l result*
lrwxrwxrwx 1 gall users 51 Sep  6 10:33 result -> /nix/store/p89a676j5gv6bmwwqy0ar67p6y8csyrd-nfsroot
lrwxrwxrwx 1 gall users 63 Sep  6 10:33 result-2 -> /nix/store/6bxm87wczxgvm9bah6sy8gxlr5w4vpm6-grub-efi-bootloader

Make a copy of the files

  • result/nfsroot.tar.xz
  • result-2/boot-loader.tar.xz

Configuring

In this example, we use the following assignments

  • IP subnet where the install client resides
    • Subnet 192.0.2.0/24
    • Gateway 192.0.2.1
  • Install client
    • MAC address 01:02:03:04:05:06
    • Fixed IP address 192.0.2.2
  • TFTP server address 198.51.100.1
  • NFS server address 198.51.100.2
  • 1st DNS server address 198.51.100.3
  • 2nd DNS server address 198.51.100.4
  • DNS domain example.org
  • Location of the Grub boot loader and Linux kernel on the TFTP server: /srv/tftp/nixos
  • Location of the root filesystem on the NFS server: /srv/nixos/nfsroot
DHCP Server

The following configuration is for a stock ISC DHCP server. It supplies the install client with its IP configuration, the information needed to find and load the Grub boot loader and Linux kernel as well as from where to mount the root file system over NFS.

option domain-name "example.org";
option domain-name-servers 198.51.100.3, 198.51.100.4;
## Generate a hostname option (#12) from a host declaration
use-host-decl-names on;

## The current grub2 networking stack does not set the DHCP magic
## cookie vendor class, i.e. it creates a pure BOOTP requests.  It
## still expects a DHCP response with particular options, which is
## totally broken.  This option forces the server to reply with DHCP,
## also see "dhcp-parameter-request-list" below.
always-reply-rfc1048 on;

## Only the EFI x64 client system architecture is currently supported
option arch code 93 = unsigned integer 16;
if option arch = 00:07 {
   filename "nixos/bootx64.efi";
} else {
   ## This suppresses the "boot file name" BOOTP option
   ## in the reply, which causes non-EFIx64 systems to stall
   filename "";
} 

subnet 192.0.2.0 netmask 255.255.255.255 {
       next-server 198.51.100.1;
       option routers 192.0.2.1;
       option tftp-server-name "198.51.100.1"; # This could also be a domain name
       option root-path "192.51.100.2:/srv/nixos/nfsroot,vers=3,tcp,rsize=32768,wsize=32768,actimeo=600"; # Option 17

       ## Required for the borked grub2 bootp mechanism.  We need to
       ## include all required options, since options not on this list
       ## are suppressed, even when the client asks for them.
       option dhcp-parameter-request-list 1,3,6,12,15,17,66; # subnet mask, router, DNS server, hostname, domain, root-path, tftp-server
}

host install-client { hardware ethernet 01:02:03:04:05:06; 
                      fixed-address 192.0.2.2; }
TFTP Server

A TFTP server is needed to serve the boot loader and Linux kernel. In this example, we use tftp-hpa and assume that the path /srv/tftp/nixos exists and is readable by user nobody.

Unpack the archive boot-loader.tar.xz (see section Building) to /srv/tftp/nixos. It should contain the following files

  • bootx64.efi The EFI boot loader
  • bzImage The Linux kernel fetched by GRUB
  • grub.cfg The generic GRUB configuration
  • load-kernel.cfg A GRUB configuration file loaded from grub.cfg which loads bzImage with the standard kernel parameters

Then start the deamon as

in.tftpd --listen --address 198.51.100.1:69 --secure /srv/tftp

If you need to, you can modify the boot loader at a later time without going through the nix-build process (e.g. when you don't have access to a NixOS system).

NFS Server

Create the path /srv/nixos/nfsroot and unpack nfsroot.tar.xz (see section Building) in it

# mkdir -p /srv/nixos/nfsroot
# cd /srv/nixos/nfsroot
# tar xaf /path-to/nfsroot.tar.xz

Export /srv/nixos/nfsroot read-only via NFS by adding the following line to /etc/exports (or the equivalent on your system)

/srv/nixos/nfsroot 192.0.2.0/24(async,ro,no_subtree_check,no_root_squash)

Preparing an Install Image

Building

Create the file image.nix in the top-level directory containing the configuration for the install image as described in the section about install image examples, e.g.

$ cat image.nix
with import <nixpkgs> {};

let
  build = (import <nixpkgs/nixos/lib/eval-config.nix> {
    inherit system;
    modules = [ modules/install-image.nix ];
  }).config.system.build;
in
  with build.installImage;
    [ tarball config ]

then execute nix-build image.nix

$ nix-build image.nix
/nix/store/ypm486qd5pd0k7cnikc5dsslbi4ziky2-install-tarball-nixos-18.03.133154.f094fd6379b
/nix/store/26bakcd6si5fkkw249lb01w44yj88zly-install-config

$ ls result*
result:
nixos.tar.gz

result-2:
config

Staging

Copy the tarball result/nixos.tar.gz to /srv/nixos/nfsroot/installer on the TFTP server and create a symbolic link to it from /srv/nixos/nfsroot/installer/nixos-image

# cp /path-to/nixos.tar.gz /srv/nixos/nfsroot/installer
# ln -s ./nixos.tar.gz /srv/nixos/nfsroot/installer/nixos-image

Copy the configuration file result-2/config to /srv/nixos/nfsroot/installer/config.

Installing the Client

Configure the client's EFI boot loader to perform a PXE boot on the desired interface and initiate a system boot. With all previous steps completed, the client should be installed and rebooted into the new system automatically.

Documentation

Install Image

The main ingredients to the creation of an install image are a copy of a nixpkgs source tree and a NixOS configuration directory structured like /etc/nixos.

The nixpkgs source tree can either be a NixOS channel or a checkout of the nixpkgs Git repository, which will be transformed into such a channel by the module. Please refer to the appendix for details.

Module configuration

Pleas refer to the options declaration in module/install-image.nix for a full description of the available options. Alternatively, you can run the command

nix-build module-manpages.nix -A installImage && man result/share/man/man5/configuration.nix.5

in the repository to get a summary as a pseudo-manpage.

Image creation

The actual image (which really isn't a disk image but a tarball) is created by lib/make-install-image.nix. The main ingredient is the evaluation of the desired NixOS configuration in the context of the supplied channel (simplified):

config = (import (channel + "/nixos/nixos/lib/eval-config.nix") {
  modules = [ (nixosConfigDir + "/configuration.nix") ];
}).config;

The Nix store of the install image is populated with the closures of config.system.build.toplevel and channel as well as a list of additional packages to make the install image self-contained as described in the introduction.

The module produces three derivations, which are available in the attribute set config.system.build.installImage

  • channel. The NixOS channel containing the nixpkgs sources from the installImage.nixpkgs.path option

  • tarball. The tar archive containing the install image

  • config. A shell-script containing configuration parameters derived from the installImage module options that need to be propagated to the installer. Currently, this includes

    • The device on which to install the root file system of the install client (installImage.rootDevice)
    • The character used to separate the partition number from the device name (installImage.partitionSeparator, defaults to an empty string)

The tarball and config derivations each contain a single file called nixos.tar.gz and config, respectively. These files need to be copied into the installer directory of the NFS root file system used by the installer.

Examples

In the following examples, we assume that the current directory is a checkout of the nixos-pxe-installer repository.

The most basic usage is represented by the following code

$ cat default.nix
with import <nixpkgs> {};

let
  installImage = (import <nixpkgs/nixos/lib/eval-config.nix> {
    modules = [ modules/install-image.nix ];
  }).config.system.build.installImage;
in
  with installImage;
    [ tarball config ]

which can be executed by nix-build. This will build an install image from the nixos channel of the current system based on the default value <nixpkgs> of the option installImage.nixpkgs.path, as can be seen from the channel version

$ nixos-version 
18.03.133154.f094fd6379b (Impala)

[gall@nixos:~/ALX/installer]$ nix-build image.nix
/nix/store/ypm486qd5pd0k7cnikc5dsslbi4ziky2-install-tarball-nixos-18.03.133154.f094fd6379b
/nix/store/26bakcd6si5fkkw249lb01w44yj88zly-install-config

To create a clone of the current system, one could use

with import <nixpkgs> {};

let
  installImageConfig = {
    installImage = {
      nixosConfigDir = /etc/nixos;
    };
  };
  installImage = (import <nixpkgs/nixos/lib/eval-config.nix> {
    modules = [ modules/install-image.nix
                installImageConfig ];
  }).config.system.build.installImage;
in
  with installImage;
    [ tarball config ]

To create an install image for the current version of the NixOS release 18.03, one would first clone into the release-18.03 branch of the nixpkgs repository

$ git clone -b release-18.03 https://github.com/NixOS/nixpkgs.git
Cloning into 'nixpkgs'...
remote: Counting objects: 1304345, done.
remote: Compressing objects: 100% (61/61), done.
remote: Total 1304345 (delta 21), reused 20 (delta 10), pack-reused 1304274
Receiving objects: 100% (1304345/1304345), 820.53 MiB | 2.12 MiB/s, done.
Resolving deltas: 100% (883569/883569), done.
Checking out files: 100% (14885/14885), done.

Then use

with import <nixpkgs> {};

let
  installImageConfig = {
    installImage = {
      nixpkgs.path = builtins.path { path = ./nixpkgs; };
    };
  };
  installImage = (import <nixpkgs/nixos/lib/eval-config.nix> {
    modules = [ modules/install-image.nix
                installImageConfig ];
  }).config.system.build.installImage;
in
  with installImage;
    [ tarball config ]

to build the image. Note that nixpkgs.path must be an object in the Nix store. In this case, the Git repository, which resides at an arbitrary location on the local file system, is copied to the Nix store through the builtin function path. Alterantively, once could also specify the Git repository itself as the source of the nixpkgs tree. The following Nix expression is equivalent (at the time of writing) to the above

with import <nixpkgs> {};

let
  installImageConfig = {
    installImage = {
      nixpkgs.path = fetchgit {
        url = "https://github.com/NixOS/nixpkgs.git";
        deepClone = true;
        rev = "refs/heads/release-18.03";
        sha256 = "1hahhmnsyhgg20mvf7a5p0y2y5lazs7jnz2lf5gw95pdx56aa6rz";
      };
    };
  };
  installImage = (import <nixpkgs/nixos/lib/eval-config.nix> {
    modules = [ modules/install-image.nix
                installImageConfig ];
  }).config.system.build.installImage;
in
  with installImage;
    [ tarball config ]

The attribute deepClone = true is required to fetch the entire history of the repository, which is necessary to determine the version number via git rev-list as explained in the appendix. Unfortunately, this takes a very long time because fetchgit performs a git repack in an attempt to create a deterministic Git repository (i.e. keep the sha256 hash constant for each invocation of a fetchgit of the same Git revision). Also, it appears that it is not as deterministic as it should be, i.e. the hash value may depend on the Git version.

To change the serial device and baud rate to use by the kernel, use the following configuration snippet in any of the examples above

  installImageConfig = {
    installImage = {
      serial = {
        unit = 1;
        speed = 9600;
      };
    };
  };

Installer

The installer currently supports only PXE-based network boots of UEFI systems. The boot sequence is as follows.

  • Client PXE stack issues a DHCP "discover" request
  • DHCP server responds with IP configuration, IP address of a TFTP server (next server BOOTP option) from which to fetch the boot loader and the file name of the boot loader image (boot file name BOOTP option)
  • Client fetches the boot loader and executes it

The boot loader used by the installer is based on the EFI variant of Grub2. It performs essentially the same DHCP request as the PXE loader did before to discover IP parameters and the address of the TFTP server.

Next, it loads the file grub.cfg from the TFTP server in the same directory from where the boot loader was obtained. The standard grub.cfg file supplied by the installer tries to load the following files in the given order

  • load-kernel-<mac-address>.cfg
  • load-kernel-<ip-address>.cfg
  • load-kernel-<hostname>.cfg
  • load-kernel.cfg

Here, <mac-address> is the MAC address of the interface on which the DHCP information was obtained, <ip-address> is the IPv4 address configured on that interface and <hostname> is the host name supplied by the DHCP server (if no host name is supplied, that step is skipped). Each file is attempted to be executed by Grub with the configfile command.

The only file supplied by default is load-kernel.cfg, which loads the Linux kernel bzImage with a particular set of kernel parameters as explained below.

The other cfg files can be created on demand to customise the Linux image and kernel parameters depending on the client being installed. Note that control is handed back to grub.cfg after the file has been processed. This can lead to the undesired result that another kernel is loaded from one of the remaining files, which will override all previously loaded kernels. To avoid this, the custom cfg files should always terminate with the boot command (load-kernel.cfg doesn't need this because there is an implicit boot command at the end of grub.cfg).

The kernel is started with the options ip=::::::dhcp:: root=/dev/nfs to initiate yet another round of DHCP requests. On a multi-homed host, the kernel will auto-detect all operational interfaces and issue a DHCP request on each of them. It will only configure the interface on which the DHCP transaction is finished first.

The DHCP request includes option #17, which asks for the address, path and parameters of an NFS-exported root file system. This option should look like

<ip-address>:<path>,vers=3,tcp,rsize=32768,wsize=32768,actimeo=600

where <ip-address> is the literal IPv4 address of the NFS server and <path> is the file system path from which the root file system can be mounted by the kernel. The file system should be exported read-only.

Once the read-only root file system has been mounted, it is transformed into a writeable file system using unionfs/fuse and control is transferred to a shell script that performs the actual installation, which performs the following actions.

  • Source the installer configuration from the files in the given order

    • /installer/config-<mac-address
    • /installer/config-<ip-address>
    • /installer/config-<hostname>
    • /installer/config
  • Create a new partition table on the disk specified in the rootDevice configuration variable containing two partitions

    • Type FAT32, size 512MiB, used as EFI boot partition
    • Type EXT4, rest of disk except 4GiB at the end (may be used as swap later on but remains unused by the installer), used as root partition
  • Create filesystems on the partitions (vfat and ext4, respectively)

  • Label the ext4 partition as nixos

  • Mount the root partition as /mnt

  • Unpack the install image onto /mnt. The image is expected to be a tar file located at /installer/nixos-image, which is typically a symbolic link to the file nixos.tar.gz created by install-image.nix. The image is extracted with the --auto-compress option, i.e. the compression program is determined from the extension of the name of the install image (possibly after de-referencing symbolic links)

  • The hardware-specific NixOS configuration is created by executing nixos-generate-config --root=/mnt. This will generate the file /mnt/etc/nixos/hardware-configuration.nix. If /mnt/etc/nixos/configuration.nix does not exist, it will be created as well and import ./hardware-configuration.nix automatically. If /mnt/etc/nixos/configuration.nix has been pre-created when the install image was generated, it will not be touched by nixos-generate-config. In this case, it is important that it is configured to import ./hardware-configuration.nix.

  • The NixOS configuration of the final system is generated by executing nix-env -p /nix/var/nix/profiles/system -f '<nixpkgs/nixos>' --set -A system in a chroot environment on the root partition.

  • The final configuration is activated by executing /nix/var/nix/profiles/system/bin/switch-to-configuration boot in chroot on the root partition, which will generate the boot loader and configure the EFI boot loader to boot from it.

  • Reboot. The EFI boot loader will automatically prefer the fresh root partition (no need to change boot poriorities manually from the BIOS)

Module configuration

Pleas refer to the options declaration in module/install-image.nix for a full description of the available options. Alternatively, you can run the command

nix-build module-manpages.nix -A installer && man result/share/man/man5/configuration.nix.5

in the repository to get a summary as a pseudo-manpage.

Examples

In the following examples, we assume that the current directory is a checkout of the nixos-pxe-installer repository.

To create an installer with the default settings, put the following Nix expression in the file default.nix

{ system ? "x86_64-linux" }:

with import <nixpkgs> { inherit system; };
with lib;

let

  nfsroot = (import <nixpkgs/nixos/lib/eval-config.nix> {
    inherit system;
    modules = [ modules/installer-nfsroot.nix ];
  }).config.system.build.nfsroot;

in
  with nfsroot;
  [ nfsRootTarball bootLoader ]

and execute nix-build, which will create the NFS root file system tarball and boot loader tarball in the Nix store paths pointed to by the symbolic links result and result-2, respectively

$ nix-build installer.nix
/nix/store/p89a676j5gv6bmwwqy0ar67p6y8csyrd-nfsroot
/nix/store/6bxm87wczxgvm9bah6sy8gxlr5w4vpm6-grub-efi-bootloader

$ ls -l result*
lrwxrwxrwx 1 gall users 51 Sep  6 13:47 result -> /nix/store/p89a676j5gv6bmwwqy0ar67p6y8csyrd-nfsroot
lrwxrwxrwx 1 gall users 63 Sep  6 13:47 result-2 -> /nix/store/6bxm87wczxgvm9bah6sy8gxlr5w4vpm6-grub-efi-bootloader

$ ls result*
result:
nfsroot.tar.xz

result-2:
boot-loader.tar.xz

The following example sets the unit of the serial device used by the boot loader and the kernel to 1

{ system ? "x86_64-linux" }:

with import <nixpkgs> { inherit system; };
with lib;

let

  installerConfig = {
    nfsroot.serial = {
      unit = 1;
    };
  };

  nfsroot = (import <nixpkgs/nixos/lib/eval-config.nix> {
    inherit system;
    modules = [ modules/installer-nfsroot.nix
                installerConfig ];
  }).config.system.build.nfsroot;

in
  with nfsroot;
  [ nfsRootTarball bootLoader ]

Updating the Grub boot loader manually

The preferred method for generaing the boot loader is through the bootLoader derivation of the install-nfsroot.nix module. Apart from the boot loader itself (the file bootx64.efi), it also contains a bootable kernel, grub.cfg and load-kernel.cfg as described above. The cfg files can be edited and changed as desired. However, the boot loader only contains the Grub modules required by the default cfg files. The exact command line that was used to genereate the default boot loader is contained in the shell script generate, which is also part of the boot loader tarball. It looks like this:

grub-mkimage -O x86_64-efi -o ./bootx64.efi -p '(tftp)' \
  serial terminal net efinet tftp normal echo eval test linux configfile reboot

The second line lists all modules that are compiled into the boot loader. A new boot loader can be generated by modifying this script and executing it from the same directory (with the assumption that a compatible Grub2 with EFI enabled is available in the standard search path).

Caveat: the boot loader is tied to the NFS root file system through the init= kernel option in grub.cfg. It is important to keep this option unchanged and always use the NFS root file system from the same run of nix-build.

Appendix

Channels

In the context of the install-image.nix module, a channel is defined as the instantiation of a NixOS channel as the result of performing a nix-channel --upgrade operation. This is a store path that contains a reference to a binary cache URL and a copy of a nixpkgs source tree. For example, consider a system with the following channel named nixos

# nix-channel --list
nixos https://nixos.org/channels/nixos-18.03

Instantiation of the channel results in the creation of a new generation of the root user's profile called channels (the file manifest.nix is irrelevant in this context and is actually no longer used by newer releases of NixOS):

# ls -l /nix/var/nix/profiles/per-user/root/channels/
total 12
lrwxrwxrwx 1 root root 88 Jan  1  1970 binary-caches -> /nix/store/z0gsgq0309r5j3ks7aqzlsibcipa0r1z-nixos-18.03.133188.8b92a4e6004/binary-caches
lrwxrwxrwx 1 root root 60 Jan  1  1970 manifest.nix -> /nix/store/45j857v08j21ybb2hb08cky2a2qvl2a6-env-manifest.nix
lrwxrwxrwx 1 root root 80 Jan  1  1970 nixos -> /nix/store/z0gsgq0309r5j3ks7aqzlsibcipa0r1z-nixos-18.03.133188.8b92a4e6004/nixos

The symbolik links binary-caches and nixos point to the store path /nix/store/z0gsgq0309r...-nixos-18.03.133188.8b92a4e6004, which contains the URL of the binary cache associated with the channel

# cat /nix/store/z0gsgq0309r...-nixos-18.03.133188.8b92a4e6004/binary-caches/nixos
https://cache.nixos.org

and a copy of a full nixpkgs source tree

# ls -la /nix/store/z0gsgq0309r...-nixos-18.03.133188.8b92a4e6004/nixos
total 3000
dr-xr-xr-x  8 root root    4096 Jan  1  1970 .
dr-xr-xr-x  4 root root    4096 Jan  1  1970 ..
-r--r--r--  1 root root    1558 Jan  1  1970 COPYING
-r--r--r--  1 root root     557 Jan  1  1970 default.nix
dr-xr-xr-x  4 root root    4096 Jan  1  1970 doc
-r--r--r--  1 root root     677 Jan  1  1970 .editorconfig
dr-xr-xr-x  2 root root    4096 Jan  1  1970 .github
-r--r--r--  1 root root     193 Jan  1  1970 .gitignore
-r--r--r--  1 root root      40 Jan  1  1970 .git-revision
dr-xr-xr-x  4 root root    4096 Jan  1  1970 lib
dr-xr-xr-x  3 root root    4096 Jan  1  1970 maintainers
dr-xr-xr-x  7 root root    4096 Jan  1  1970 nixos
lrwxrwxrwx  1 root root       1 Jan  1  1970 nixpkgs -> .
dr-xr-xr-x 17 root root    4096 Jan  1  1970 pkgs
-r--r--r--  1 root root 3002368 Jan  1  1970 programs.sqlite
-r--r--r--  1 root root    2030 Jan  1  1970 README.md
-r--r--r--  1 root root      20 Jan  1  1970 svn-revision
-r--r--r--  1 root root       6 Jan  1  1970 .version
-r--r--r--  1 root root      19 Jan  1  1970 .version-suffix

It is this store path (/nix/store/z0gsgq0309r...-nixos-18.03.133188.8b92a4e6004 in this example) to which we refer to as channel in the context of the install-image.nix module.

It is worth noting that the string nixos in the directory names nixos and binary-caches/nixos of the store path is the name by which the channel was registered with nix-channel --add. The channel named nixos plays a special role in a NixOS system, because it provides the default Nix expression used for the configuration of the system, e.g. by nixos-rebuild. This is reflected in the fact that the standard NIX_PATH refers to that channel explicitely, e.g.

$ echo $NIX_PATH
/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix:/nix/var/nix/profiles/per-user/root/channels

The install-image.nix module takes a file system tree that holds a copy of a nixpkgs hierarchy and turns it into a channel. The hierarchy can either be the nixpkgs directory from an exsting channel or a checkout of the NixOS/nixpkgs Git repository.

The former is usually obtained from the channel of the local system through the Nix path <nixpkgs>. To construct a channel from it, one only needs to create the directory structure described above, copy the nixpkgs directory inot it and add the binary-cache given by the installImage.binaryCacheURL configuration option.

If the input is a Git repository, it must first be transformed into a proper nixpkgs tree as follows. The main difference (apart from the Git-specific files) with respect to a nixpkgs directory from a channel is the absence of the file .version-suffix and a missing symbolic in the top-level directory.

The file .version-suffix, together with .version, makes up the full version identifier of a NixOS system:

$ (cd /nix/store/z0gsgq0309r5j3ks7aqzlsibcipa0r1z-nixos-18.03.133188.8b92a4e6004/nixos && cat .version .version-suffix)
18.03.133188.8b92a4e6004

It is not part of a nixpkgs Git repository. Instead, it is constructed from a particular commit in one of the release branches by the Hydra CI system when a new release is created. The relevant code can be found in nixos/release.nix of nixpkgs:

{ nixpkgs ? { outPath = ./..; revCount = 56789; shortRev = "gfedcba"; }
, stableBranch ? false
, supportedSystems ? [ "x86_64-linux" "i686-linux" ]
}:
  .
  .
  .
  version = builtins.readFile ../.version;
  versionSuffix =
    (if stableBranch then "." else "pre") + "${toString nixpkgs.revCount}.${nixpkgs.shortRev}";
  .
  .
  .
  channel = import lib/make-channel.nix { inherit pkgs nixpkgs version versionSuffix; };
  .
  .
  .

When called from the Hydra build system, the dummy default values of the variables revCount and shortRev (56789 and gfedcba, respectively), are replaced by values obtained by executing the equivalent of the following shell code in the Git repository

revision=$(git rev-list --max-count=1 HEAD)
revCount=$(git rev-list $revision | wc -l)
shortRev=$(git rev-parse --short $revision)

Hydra also sets the variable stableBranch to the value true if it is building a release from one of the stable NixOS branches (e.g. release-18.03 or release-17.09 at the time of writing) to make the distinction to an unstable release visible in the version number.

The install-image.nix module imitates this mechanism in order to produce a NixOS channel that looks exactly as if it had been built by the Hydra system:

  • Calculate revCount and shortRev from the Git repository
  • Create a properly versioned archive by calling <nixpkgs/nixos/lib/make-channel.nix>

In any case, we'll end up with a tar archive containing a properly versioned nixpkgs hierarchy. In the final step, the channel itself is created by evaluating the Nix expression <nix/unpack-channel.nix>, which creates the nixos and binary-caches directories and populates them in a new store object.

This channel will eventually be installed as the initial channel of a system that gets installed from this install image.