Development This chapter describes how you can modify and extend NixOS.
Getting the sources By default, NixOS’s nixos-rebuild command uses the NixOS and Nixpkgs sources provided by the nixos-unstable channel (kept in /nix/var/nix/profiles/per-user/root/channels/nixos). To modify NixOS, however, you should check out the latest sources from Git. This is done using the following command: $ nixos-checkout /my/sources or $ mkdir -p /my/sources $ cd /my/sources $ nix-env -i git $ git clone git://github.com/NixOS/nixpkgs.git This will check out the latest NixOS sources to /my/sources/nixpkgs/nixos and the Nixpkgs sources to /my/sources/nixpkgs. (The NixOS source tree lives in a subdirectory of the Nixpkgs repository.) If you want to rebuild your system using your (modified) sources, you need to tell nixos-rebuild about them using the flag: $ nixos-rebuild switch -I nixpkgs=/my/sources/nixpkgs If you want nix-env to use the expressions in /my/sources, use nix-env -f /my/sources/nixpkgs, or change the default by adding a symlink in ~/.nix-defexpr: $ ln -s /my/sources/nixpkgs ~/.nix-defexpr/nixpkgs You may want to delete the symlink ~/.nix-defexpr/channels_root to prevent root’s NixOS channel from clashing with your own tree.
Writing NixOS modules NixOS has a modular system for declarative configuration. This system combines multiple modules to produce the full system configuration. One of the modules that constitute the configuration is /etc/nixos/configuration.nix. Most of the others live in the nixos/modules subdirectory of the Nixpkgs tree. Each NixOS module is a file that handles one logical aspect of the configuration, such as a specific kind of hardware, a service, or network settings. A module configuration does not have to handle everything from scratch; it can use the functionality provided by other modules for its implementation. Thus a module can declare options that can be used by other modules, and conversely can define options provided by other modules in its own implementation. For example, the module pam.nix declares the option that allows other modules (e.g. sshd.nix) to define PAM services; and it defines the option (declared by etc.nix) to cause files to be created in /etc/pam.d. In , we saw the following structure of NixOS modules: { config, pkgs, ... }: { option definitions } This is actually an abbreviated form of module that only defines options, but does not declare any. The structure of full NixOS modules is shown in . Structure of NixOS modules { config, pkgs, ... }: { imports = [ paths of other modules ]; options = { option declarations }; config = { option definitions }; } The meaning of each part is as follows. This line makes the current Nix expression a function. The variable pkgs contains Nixpkgs, while config contains the full system configuration. This line can be omitted if there is no reference to pkgs and config inside the module. This list enumerates the paths to other NixOS modules that should be included in the evaluation of the system configuration. A default set of modules is defined in the file modules/module-list.nix. These don't need to be added in the import list. The attribute options is a nested set of option declarations (described below). The attribute config is a nested set of option definitions (also described below). shows a module that handles the regular update of the “locate” database, an index of all files in the file system. This module declares two options that can be defined by other modules (typically the user’s configuration.nix): (whether the database should be updated) and (when the update should be done). It implements its functionality by defining two options declared by other modules: (the set of all systemd services) and (the list of commands to be executed periodically by cron). NixOS module for the “locate” service { config, lib, pkgs, ... }: with lib; let locatedb = "/var/cache/locatedb"; in { options = { services.locate = { enable = mkOption { type = types.bool; default = false; description = '' If enabled, NixOS will periodically update the database of files used by the locate command. ''; }; period = mkOption { type = types.str; default = "15 02 * * *"; description = '' This option defines (in the format used by cron) when the locate database is updated. The default is to update at 02:15 at night every day. ''; }; }; }; config = { systemd.services.update-locatedb = { description = "Update Locate Database"; path = [ pkgs.su ]; script = '' mkdir -m 0755 -p $(dirname ${locatedb}) exec updatedb --localuser=nobody --output=${locatedb} --prunepaths='/tmp /var/tmp /media /run' ''; }; services.cron.systemCronJobs = optional config.services.locate.enable "${config.services.locate.period} root ${config.systemd.package}/bin/systemctl start update-locatedb.service"; }; }
Option declarations An option declaration specifies the name, type and description of a NixOS configuration option. It is illegal to define an option that hasn’t been declared in any module. A option declaration generally looks like this: options = { name = mkOption { type = type specification; default = default value; example = example value; description = "Description for use in the NixOS manual."; }; }; The function mkOption accepts the following arguments. type The type of the option (see below). It may be omitted, but that’s not advisable since it may lead to errors that are hard to diagnose. default The default value used if no value is defined by any module. A default is not required; in that case, if the option value is ever used, an error will be thrown. example An example value that will be shown in the NixOS manual. description A textual description of the option, in DocBook format, that will be included in the NixOS manual. Here is a non-exhaustive list of option types: types.bool A Boolean. types.int An integer. types.str A string. types.lines A string. If there are multiple definitions, they are concatenated, with newline characters in between. types.path A path, defined as anything that, when coerced to a string, starts with a slash. This includes derivations. types.listOf t A list of elements of type t (e.g., types.listOf types.str is a list of strings). Multiple definitions are concatenated together. types.attrsOf t A set of elements of type t (e.g., types.attrsOf types.int is a set of name/value pairs, the values being integers). types.nullOr t Either the value null or something of type t. You can also create new types using the function mkOptionType. See lib/types.nix in Nixpkgs for details.
Option definitions Option definitions are generally straight-forward bindings of values to option names, like config = { services.httpd.enable = true; }; However, sometimes you need to wrap an option definition or set of option definitions in a property to achieve certain effects: Delaying conditionals If a set of option definitions is conditional on the value of another option, you may need to use mkIf. Consider, for instance: config = if config.services.httpd.enable then { environment.systemPackages = [ ... ]; ... } else {}; This definition will cause Nix to fail with an “infinite recursion” error. Why? Because the value of depends on the value being constructed here. After all, you could also write the clearly circular and contradictory: config = if config.services.httpd.enable then { services.httpd.enable = false; } else { services.httpd.enable = true; }; The solution is to write: config = mkIf config.services.httpd.enable { environment.systemPackages = [ ... ]; ... }; The special function mkIf causes the evaluation of the conditional to be “pushed down” into the individual definitions, as if you had written: config = { environment.systemPackages = if config.services.httpd.enable then [ ... ] else []; ... }; Setting priorities A module can override the definitions of an option in other modules by setting a priority. All option definitions that do not have the lowest priority value are discarded. By default, option definitions have priority 1000. You can specify an explicit priority by using mkOverride, e.g. services.openssh.enable = mkOverride 10 false; This definition causes all other definitions with priorities above 10 to be discarded. The function mkForce is equal to mkOverride 50. Merging configurations In conjunction with mkIf, it is sometimes useful for a module to return multiple sets of option definitions, to be merged together as if they were declared in separate modules. This can be done using mkMerge: config = mkMerge [ # Unconditional stuff. { environment.systemPackages = [ ... ]; } # Conditional stuff. (mkIf config.services.bla.enable { environment.systemPackages = [ ... ]; }) ];
Important options NixOS has many options, but some are of particular importance to module writers. This set defines files in /etc. A typical use is: environment.etc."os-release".text = '' NAME=NixOS ... ''; which causes a file named /etc/os-release to be created with the given contents. A set of shell script fragments that must be executed whenever the configuration is activated (i.e., at boot time, or after running nixos-rebuild switch). For instance, system.activationScripts.media = '' mkdir -m 0755 -p /media ''; causes the directory /media to be created. Activation scripts must be idempotent. They should not start background processes such as daemons; use for that. This is the set of systemd services. Example: systemd.services.dhcpcd = { description = "DHCP Client"; wantedBy = [ "multi-user.target" ]; after = [ "systemd-udev-settle.service" ]; path = [ dhcpcd pkgs.nettools pkgs.openresolv ]; serviceConfig = { Type = "forking"; PIDFile = "/run/dhcpcd.pid"; ExecStart = "${dhcpcd}/sbin/dhcpcd --config ${dhcpcdConf}"; Restart = "always"; }; }; which creates the systemd unit dhcpcd.service. The option determined which other units pull this one in; multi-user.target is the default target of the system, so dhcpcd.service will always be started. The option provides the main command for the service; it’s also possible to provide pre-start actions, stop scripts, and so on. If your service requires special UIDs or GIDs, you can define them with these options. See for details.
Building specific parts of NixOS With the command nix-build, you can build specific parts of your NixOS configuration. This is done as follows: $ cd /path/to/nixpkgs/nixos $ nix-build -A config.option where option is a NixOS option with type “derivation” (i.e. something that can be built). Attributes of interest include: system.build.toplevel The top-level option that builds the entire NixOS system. Everything else in your configuration is indirectly pulled in by this option. This is what nixos-rebuild builds and what /run/current-system points to afterwards. A shortcut to build this is: $ nix-build -A system system.build.manual.manual The NixOS manual. system.build.etc A tree of symlinks that form the static parts of /etc. system.build.initialRamdisk system.build.kernel The initial ramdisk and kernel of the system. This allows a quick way to test whether the kernel and the initial ramdisk boot correctly, by using QEMU’s and options: $ nix-build -A config.system.build.initialRamdisk -o initrd $ nix-build -A config.system.build.kernel -o kernel $ qemu-system-x86_64 -kernel ./kernel/bzImage -initrd ./initrd/initrd -hda /dev/null system.build.nixos-rebuild system.build.nixos-install system.build.nixos-generate-config These build the corresponding NixOS commands. systemd.units.unit-name.unit This builds the unit with the specified name. Note that since unit names contain dots (e.g. httpd.service), you need to put them between quotes, like this: $ nix-build -A 'config.systemd.units."httpd.service".unit' You can also test individual units, without rebuilding the whole system, by putting them in /run/systemd/system: $ cp $(nix-build -A 'config.systemd.units."httpd.service".unit')/httpd.service \ /run/systemd/system/tmp-httpd.service $ systemctl daemon-reload $ systemctl start tmp-httpd.service Note that the unit must not have the same name as any unit in /etc/systemd/system since those take precedence over /run/systemd/system. That’s why the unit is installed as tmp-httpd.service here.
Building your own NixOS CD Building a NixOS CD is as easy as configuring your own computer. The idea is to use another module which will replace your configuration.nix to configure the system that would be installed on the CD. Default CD/DVD configurations are available inside nixos/modules/installer/cd-dvd. To build them you have to set NIXOS_CONFIG before running nix-build to build the ISO. $ nix-build -A config.system.build.isoImage -I nixos-config=modules/installer/cd-dvd/installation-cd-minimal.nix Before burning your CD/DVD, you can check the content of the image by mounting anywhere like suggested by the following command: $ mount -o loop -t iso9660 ./result/iso/cd.iso /mnt/iso
Testing the installer Building, burning, and booting from an installation CD is rather tedious, so here is a quick way to see if the installer works properly: $ nix-build -A config.system.build.nixos-install $ dd if=/dev/zero of=diskimage seek=2G count=0 bs=1 $ yes | mke2fs -j diskimage $ mount -o loop diskimage /mnt $ ./result/bin/nixos-install
NixOS tests When you add some feature to NixOS, you should write a test for it. NixOS tests are kept in the directory nixos/tests, and are executed (using Nix) by a testing framework that automatically starts one or more virtual machines containing the NixOS system(s) required for the test. Writing tests A NixOS test is a Nix expression that has the following structure: import ./make-test.nix { # Either the configuration of a single machine: machine = { config, pkgs, ... }: { configuration… }; # Or a set of machines: nodes = { machine1 = { config, pkgs, ... }: { }; machine2 = { config, pkgs, ... }: { }; … }; testScript = '' Perl code… ''; } The attribute testScript is a bit of Perl code that executes the test (described below). During the test, it will start one or more virtual machines, the configuration of which is described by the attribute machine (if you need only one machine in your test) or by the attribute nodes (if you need multiple machines). For instance, login.nix only needs a single machine to test whether users can log in on the virtual console, whether device ownership is correctly maintained when switching between consoles, and so on. On the other hand, nfs.nix, which tests NFS client and server functionality in the Linux kernel (including whether locks are maintained across server crashes), requires three machines: a server and two clients. There are a few special NixOS configuration options for test VMs: The memory of the VM in megabytes. The virtual networks to which the VM is connected. See nat.nix for an example. By default, the Nix store in the VM is not writable. If you enable this option, a writable union file system is mounted on top of the Nix store to make it appear writable. This is necessary for tests that run Nix operations that modify the store. For more options, see the module qemu-vm.nix. The test script is a sequence of Perl statements that perform various actions, such as starting VMs, executing commands in the VMs, and so on. Each virtual machine is represented as an object stored in the variable $name, where name is the identifier of the machine (which is just machine if you didn’t specify multiple machines using the nodes attribute). For instance, the following starts the machine, waits until it has finished booting, then executes a command and checks that the output is more-or-less correct: $machine->start; $machine->waitForUnit("default.target"); $machine->succeed("uname") =~ /Linux/; The first line is actually unnecessary; machines are implicitly started when you first execute an action on them (such as waitForUnit or succeed). If you have multiple machines, you can speed up the test by starting them in parallel: startAll; The following methods are available on machine objects: start Start the virtual machine. This method is asynchronous — it does not wait for the machine to finish booting. shutdown Shut down the machine, waiting for the VM to exit. crash Simulate a sudden power failure, by telling the VM to exit immediately. block Simulate unplugging the Ethernet cable that connects the machine to the other machines. unblock Undo the effect of block. screenshot Take a picture of the display of the virtual machine, in PNG format. The screenshot is linked from the HTML log. sendMonitorCommand Send a command to the QEMU monitor. This is rarely used, but allows doing stuff such as attaching virtual USB disks to a running machine. sendKeys Simulate pressing keys on the virtual keyboard, e.g., sendKeys("ctrl-alt-delete"). sendChars Simulate typing a sequence of characters on the virtual keyboard, e.g., sendKeys("foobar\n") will type the string foobar followed by the Enter key. execute Execute a shell command, returning a list (status, stdout). succeed Execute a shell command, raising an exception if the exit status is not zero, otherwise returning the standard output. fail Like succeed, but raising an exception if the command returns a zero status. waitUntilSucceeds Repeat a shell command with 1-second intervals until it succeeds. waitUntilFails Repeat a shell command with 1-second intervals until it fails. waitForUnit Wait until the specified systemd unit has reached the “active” state. waitForFile Wait until the specified file exists. waitForOpenPort Wait until a process is listening on the given TCP port (on localhost, at least). waitForClosedPort Wait until nobody is listening on the given TCP port. waitForX Wait until the X11 server is accepting connections. waitForWindow Wait until an X11 window has appeared whose name matches the given regular expression, e.g., waitForWindow(qr/Terminal/). Running tests You can run tests using nix-build. For example, to run the test login.nix, you just do: $ nix-build '<nixpkgs/nixos/tests/login.nix>' or, if you don’t want to rely on NIX_PATH: $ cd /my/nixpkgs/nixos/tests $ nix-build login.nix … running the VM test script machine: QEMU running (pid 8841) … 6 out of 6 tests succeeded After building/downloading all required dependencies, this will perform a build that starts a QEMU/KVM virtual machine containing a NixOS system. The virtual machine mounts the Nix store of the host; this makes VM creation very fast, as no disk image needs to be created. Afterwards, you can view a pretty-printed log of the test: $ firefox result/log.html It is also possible to run the test environment interactively, allowing you to experiment with the VMs. For example: $ nix-build login.nix -A driver $ ./result/bin/nixos-run-vms The script nixos-run-vms starts the virtual machines defined by test. The root file system of the VMs is created on the fly and kept across VM restarts in ./hostname.qcow2. Finally, the test itself can be run interactively. This is particularly useful when developing or debugging a test: $ nix-build tests/ -A nfs.driver $ ./result/bin/nixos-test-driver starting VDE switch for network 1 > You can then take any Perl statement, e.g. > startAll > $machine->succeed("touch /tmp/foo") The function testScript executes the entire test script and drops you back into the test driver command line upon its completion. This allows you to inspect the state of the VMs after the test (e.g. to debug the test script).