DevelopmentThis chapter describes how you can modify and extend
NixOS.Getting the sourcesBy 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.)It’s often inconvenient to develop directly on the master
branch, since if somebody has just committed (say) a change to GCC,
then the binary cache may not have caught up yet and you’ll have to
rebuild everything from source. So you may want to create a local
branch based on your current NixOS version:
$ nixos-version
14.04.273.ea1952b (Baboon)
$ git checkout -b local ea1952b
Or, to base your local branch on the latest version available in the
NixOS channel:
$ curl -sI http://nixos.org/channels/nixos-unstable/ | grep Location
Location: http://releases.nixos.org/nixos/unstable/nixos-14.10pre43986.acaf4a6/
$ git checkout -b local acaf4a6
You can then use git rebase to sync your local
branch with the upstream branch, and use git
cherry-pick to copy commits from your local branch to the
upstream branch.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 modulesNixOS 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 declarationsAn 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.
typeThe 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.defaultThe 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.exampleAn example value that will be shown in the NixOS manual.descriptionA 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.boolA Boolean.types.intAn integer.types.strA string.types.linesA string. If there are multiple definitions, they are
concatenated, with newline characters in between.types.pathA path, defined as anything that, when coerced to a
string, starts with a slash. This includes derivations.types.listOftA list of elements of type t
(e.g., types.listOf types.str is a list of
strings). Multiple definitions are concatenated together.types.attrsOftA set of elements of type t
(e.g., types.attrsOf types.int is a set of
name/value pairs, the values being integers).types.nullOrtEither 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 definitionsOption 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 conditionalsIf 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 prioritiesA 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 configurationsIn 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 optionsNixOS 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 NixOSWith 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.toplevelThe 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 systemsystem.build.manual.manualThe NixOS manual.system.build.etcA tree of symlinks that form the static parts of
/etc.system.build.initialRamdisksystem.build.kernelThe 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-rebuildsystem.build.nixos-installsystem.build.nixos-generate-configThese build the corresponding NixOS commands.systemd.units.unit-name.unitThis 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 CDBuilding 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.nixBefore 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/isoTesting the installerBuilding, 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
$ mount -t tmpfs none /mnt
$ ./result/bin/nixos-install
To start a login shell in the new NixOS installation in
/mnt:
$ ./result/bin/nixos-install --chroot
NixOS testsWhen 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 testsA 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:
startStart the virtual machine. This method is
asynchronous — it does not wait for the machine to finish
booting.shutdownShut down the machine, waiting for the VM to
exit.crashSimulate a sudden power failure, by telling the VM
to exit immediately.blockSimulate unplugging the Ethernet cable that
connects the machine to the other machines.unblockUndo the effect of
block.screenshotTake a picture of the display of the virtual
machine, in PNG format. The screenshot is linked from the HTML
log.sendMonitorCommandSend a command to the QEMU monitor. This is rarely
used, but allows doing stuff such as attaching virtual USB disks
to a running machine.sendKeysSimulate pressing keys on the virtual keyboard,
e.g., sendKeys("ctrl-alt-delete").sendCharsSimulate typing a sequence of characters on the
virtual keyboard, e.g., sendKeys("foobar\n")
will type the string foobar followed by the
Enter key.executeExecute a shell command, returning a list
(status,
stdout).succeedExecute a shell command, raising an exception if
the exit status is not zero, otherwise returning the standard
output.failLike succeed, but raising
an exception if the command returns a zero status.waitUntilSucceedsRepeat a shell command with 1-second intervals
until it succeeds.waitUntilFailsRepeat a shell command with 1-second intervals
until it fails.waitForUnitWait until the specified systemd unit has reached
the “active” state.waitForFileWait until the specified file
exists.waitForOpenPortWait until a process is listening on the given TCP
port (on localhost, at least).waitForClosedPortWait until nobody is listening on the given TCP
port.waitForXWait until the X11 server is accepting
connections.waitForWindowWait until an X11 window has appeared whose name
matches the given regular expression, e.g.,
waitForWindow(qr/Terminal/).Running testsYou 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).