about summary refs log tree commit diff
path: root/nixpkgs/nixos/lib
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/lib')
-rw-r--r--nixpkgs/nixos/lib/build-vms.nix101
-rw-r--r--nixpkgs/nixos/lib/eval-config.nix67
-rw-r--r--nixpkgs/nixos/lib/from-env.nix4
-rw-r--r--nixpkgs/nixos/lib/make-channel.nix31
-rw-r--r--nixpkgs/nixos/lib/make-disk-image.nix247
-rw-r--r--nixpkgs/nixos/lib/make-ext4-fs.nix91
-rw-r--r--nixpkgs/nixos/lib/make-iso9660-image.nix65
-rw-r--r--nixpkgs/nixos/lib/make-iso9660-image.sh136
-rw-r--r--nixpkgs/nixos/lib/make-options-doc/default.nix164
-rw-r--r--nixpkgs/nixos/lib/make-options-doc/options-to-docbook.xsl236
-rw-r--r--nixpkgs/nixos/lib/make-options-doc/postprocess-option-descriptions.xsl115
-rw-r--r--nixpkgs/nixos/lib/make-squashfs.nix28
-rw-r--r--nixpkgs/nixos/lib/make-system-tarball.nix56
-rw-r--r--nixpkgs/nixos/lib/make-system-tarball.sh57
-rw-r--r--nixpkgs/nixos/lib/qemu-flags.nix25
-rw-r--r--nixpkgs/nixos/lib/test-driver/Logger.pm75
-rw-r--r--nixpkgs/nixos/lib/test-driver/Machine.pm734
-rw-r--r--nixpkgs/nixos/lib/test-driver/log2html.xsl135
-rw-r--r--nixpkgs/nixos/lib/test-driver/logfile.css129
-rw-r--r--nixpkgs/nixos/lib/test-driver/test-driver.pl191
-rw-r--r--nixpkgs/nixos/lib/test-driver/treebits.js30
-rw-r--r--nixpkgs/nixos/lib/testing.nix269
-rw-r--r--nixpkgs/nixos/lib/testing/jquery-ui.nix24
-rw-r--r--nixpkgs/nixos/lib/testing/jquery.nix36
-rw-r--r--nixpkgs/nixos/lib/utils.nix27
25 files changed, 3073 insertions, 0 deletions
diff --git a/nixpkgs/nixos/lib/build-vms.nix b/nixpkgs/nixos/lib/build-vms.nix
new file mode 100644
index 000000000000..1bad63b9194c
--- /dev/null
+++ b/nixpkgs/nixos/lib/build-vms.nix
@@ -0,0 +1,101 @@
+{ system
+, # Use a minimal kernel?
+  minimal ? false
+, # Ignored
+  config ? null
+  # Nixpkgs, for qemu, lib and more
+, pkgs
+, # NixOS configuration to add to the VMs
+  extraConfigurations ? []
+}:
+
+with pkgs.lib;
+with import ../lib/qemu-flags.nix { inherit pkgs; };
+
+rec {
+
+  inherit pkgs;
+
+  qemu = pkgs.qemu_test;
+
+
+  # Build a virtual network from an attribute set `{ machine1 =
+  # config1; ... machineN = configN; }', where `machineX' is the
+  # hostname and `configX' is a NixOS system configuration.  Each
+  # machine is given an arbitrary IP address in the virtual network.
+  buildVirtualNetwork =
+    nodes: let nodesOut = mapAttrs (n: buildVM nodesOut) (assignIPAddresses nodes); in nodesOut;
+
+
+  buildVM =
+    nodes: configurations:
+
+    import ./eval-config.nix {
+      inherit system;
+      modules = configurations ++ extraConfigurations;
+      baseModules =  (import ../modules/module-list.nix) ++
+        [ ../modules/virtualisation/qemu-vm.nix
+          ../modules/testing/test-instrumentation.nix # !!! should only get added for automated test runs
+          { key = "no-manual"; documentation.nixos.enable = false; }
+          { key = "qemu"; system.build.qemu = qemu; }
+          { key = "nodes"; _module.args.nodes = nodes; }
+        ] ++ optional minimal ../modules/testing/minimal-kernel.nix;
+    };
+
+
+  # Given an attribute set { machine1 = config1; ... machineN =
+  # configN; }, sequentially assign IP addresses in the 192.168.1.0/24
+  # range to each machine, and set the hostname to the attribute name.
+  assignIPAddresses = nodes:
+
+    let
+
+      machines = attrNames nodes;
+
+      machinesNumbered = zipLists machines (range 1 254);
+
+      nodes_ = forEach machinesNumbered (m: nameValuePair m.fst
+        [ ( { config, nodes, ... }:
+            let
+              interfacesNumbered = zipLists config.virtualisation.vlans (range 1 255);
+              interfaces = forEach interfacesNumbered ({ fst, snd }:
+                nameValuePair "eth${toString snd}" { ipv4.addresses =
+                  [ { address = "192.168.${toString fst}.${toString m.snd}";
+                      prefixLength = 24;
+                  } ];
+                });
+            in
+            { key = "ip-address";
+              config =
+                { networking.hostName = mkDefault m.fst;
+
+                  networking.interfaces = listToAttrs interfaces;
+
+                  networking.primaryIPAddress =
+                    optionalString (interfaces != []) (head (head interfaces).value.ipv4.addresses).address;
+
+                  # Put the IP addresses of all VMs in this machine's
+                  # /etc/hosts file.  If a machine has multiple
+                  # interfaces, use the IP address corresponding to
+                  # the first interface (i.e. the first network in its
+                  # virtualisation.vlans option).
+                  networking.extraHosts = flip concatMapStrings machines
+                    (m': let config = (getAttr m' nodes).config; in
+                      optionalString (config.networking.primaryIPAddress != "")
+                        ("${config.networking.primaryIPAddress} " +
+                         optionalString (config.networking.domain != null)
+                           "${config.networking.hostName}.${config.networking.domain} " +
+                         "${config.networking.hostName}\n"));
+
+                  virtualisation.qemu.options =
+                    forEach interfacesNumbered
+                      ({ fst, snd }: qemuNICFlags snd fst m.snd);
+                };
+            }
+          )
+          (getAttr m.fst nodes)
+        ] );
+
+    in listToAttrs nodes_;
+
+}
diff --git a/nixpkgs/nixos/lib/eval-config.nix b/nixpkgs/nixos/lib/eval-config.nix
new file mode 100644
index 000000000000..77490ca3762a
--- /dev/null
+++ b/nixpkgs/nixos/lib/eval-config.nix
@@ -0,0 +1,67 @@
+# From an end-user configuration file (`configuration.nix'), build a NixOS
+# configuration object (`config') from which we can retrieve option
+# values.
+
+# !!! Please think twice before adding to this argument list!
+# Ideally eval-config.nix would be an extremely thin wrapper
+# around lib.evalModules, so that modular systems that have nixos configs
+# as subcomponents (e.g. the container feature, or nixops if network
+# expressions are ever made modular at the top level) can just use
+# types.submodule instead of using eval-config.nix
+{ # !!! system can be set modularly, would be nice to remove
+  system ? builtins.currentSystem
+, # !!! is this argument needed any more? The pkgs argument can
+  # be set modularly anyway.
+  pkgs ? null
+, # !!! what do we gain by making this configurable?
+  baseModules ? import ../modules/module-list.nix
+, # !!! See comment about args in lib/modules.nix
+  extraArgs ? {}
+, # !!! See comment about args in lib/modules.nix
+  specialArgs ? {}
+, modules
+, # !!! See comment about check in lib/modules.nix
+  check ? true
+, prefix ? []
+, lib ? import ../../lib
+}:
+
+let extraArgs_ = extraArgs; pkgs_ = pkgs;
+    extraModules = let e = builtins.getEnv "NIXOS_EXTRA_MODULE_PATH";
+                   in if e == "" then [] else [(import e)];
+in
+
+let
+  pkgsModule = rec {
+    _file = ./eval-config.nix;
+    key = _file;
+    config = {
+      # Explicit `nixpkgs.system` or `nixpkgs.localSystem` should override
+      # this.  Since the latter defaults to the former, the former should
+      # default to the argument. That way this new default could propagate all
+      # they way through, but has the last priority behind everything else.
+      nixpkgs.system = lib.mkDefault system;
+      _module.args.pkgs = lib.mkIf (pkgs_ != null) (lib.mkForce pkgs_);
+    };
+  };
+
+in rec {
+
+  # Merge the option definitions in all modules, forming the full
+  # system configuration.
+  inherit (lib.evalModules {
+    inherit prefix check;
+    modules = baseModules ++ extraModules ++ [ pkgsModule ] ++ modules;
+    args = extraArgs;
+    specialArgs =
+      { modulesPath = builtins.toString ../modules; } // specialArgs;
+  }) config options;
+
+  # These are the extra arguments passed to every module.  In
+  # particular, Nixpkgs is passed through the "pkgs" argument.
+  extraArgs = extraArgs_ // {
+    inherit baseModules extraModules modules;
+  };
+
+  inherit (config._module.args) pkgs;
+}
diff --git a/nixpkgs/nixos/lib/from-env.nix b/nixpkgs/nixos/lib/from-env.nix
new file mode 100644
index 000000000000..6bd71e40e9a1
--- /dev/null
+++ b/nixpkgs/nixos/lib/from-env.nix
@@ -0,0 +1,4 @@
+# TODO: remove this file. There is lib.maybeEnv now
+name: default:
+let value = builtins.getEnv name; in
+if value == "" then default else value
diff --git a/nixpkgs/nixos/lib/make-channel.nix b/nixpkgs/nixos/lib/make-channel.nix
new file mode 100644
index 000000000000..9b920b989fcf
--- /dev/null
+++ b/nixpkgs/nixos/lib/make-channel.nix
@@ -0,0 +1,31 @@
+/* Build a channel tarball. These contain, in addition to the nixpkgs
+ * expressions themselves, files that indicate the version of nixpkgs
+ * that they represent.
+ */
+{ pkgs, nixpkgs, version, versionSuffix }:
+
+pkgs.releaseTools.makeSourceTarball {
+  name = "nixos-channel";
+
+  src = nixpkgs;
+
+  officialRelease = false; # FIXME: fix this in makeSourceTarball
+  inherit version versionSuffix;
+
+  buildInputs = [ pkgs.nix ];
+
+  distPhase = ''
+    rm -rf .git
+    echo -n $VERSION_SUFFIX > .version-suffix
+    echo -n ${nixpkgs.rev or nixpkgs.shortRev} > .git-revision
+    releaseName=nixos-$VERSION$VERSION_SUFFIX
+    mkdir -p $out/tarballs
+    cp -prd . ../$releaseName
+    chmod -R u+w ../$releaseName
+    ln -s . ../$releaseName/nixpkgs # hack to make ‘<nixpkgs>’ work
+    NIX_STATE_DIR=$TMPDIR nix-env -f ../$releaseName/default.nix -qaP --meta --xml \* > /dev/null
+    cd ..
+    chmod -R u+w $releaseName
+    tar cfJ $out/tarballs/$releaseName.tar.xz $releaseName
+  '';
+}
diff --git a/nixpkgs/nixos/lib/make-disk-image.nix b/nixpkgs/nixos/lib/make-disk-image.nix
new file mode 100644
index 000000000000..5e86ea479d51
--- /dev/null
+++ b/nixpkgs/nixos/lib/make-disk-image.nix
@@ -0,0 +1,247 @@
+{ pkgs
+, lib
+
+, # The NixOS configuration to be installed onto the disk image.
+  config
+
+, # The size of the disk, in megabytes.
+  diskSize
+
+  # The files and directories to be placed in the target file system.
+  # This is a list of attribute sets {source, target} where `source'
+  # is the file system object (regular file or directory) to be
+  # grafted in the file system at path `target'.
+, contents ? []
+
+, # Type of partition table to use; either "legacy", "efi", or "none".
+  # For "efi" images, the GPT partition table is used and a mandatory ESP
+  #   partition of reasonable size is created in addition to the root partition.
+  #   If `installBootLoader` is true, GRUB will be installed in EFI mode.
+  # For "legacy", the msdos partition table is used and a single large root
+  #   partition is created. If `installBootLoader` is true, GRUB will be
+  #   installed in legacy mode.
+  # For "none", no partition table is created. Enabling `installBootLoader`
+  #   most likely fails as GRUB will probably refuse to install.
+  partitionTableType ? "legacy"
+
+, # The root file system type.
+  fsType ? "ext4"
+
+, # Filesystem label
+  label ? "nixos"
+
+, # The initial NixOS configuration file to be copied to
+  # /etc/nixos/configuration.nix.
+  configFile ? null
+
+, # Shell code executed after the VM has finished.
+  postVM ? ""
+
+, name ? "nixos-disk-image"
+
+, # Disk image format, one of qcow2, qcow2-compressed, vpc, raw.
+  format ? "raw"
+}:
+
+assert partitionTableType == "legacy" || partitionTableType == "efi" || partitionTableType == "none";
+# We use -E offset=X below, which is only supported by e2fsprogs
+assert partitionTableType != "none" -> fsType == "ext4";
+
+with lib;
+
+let format' = format; in let
+
+  format = if format' == "qcow2-compressed" then "qcow2" else format';
+
+  compress = optionalString (format' == "qcow2-compressed") "-c";
+
+  filename = "nixos." + {
+    qcow2 = "qcow2";
+    vpc   = "vhd";
+    raw   = "img";
+  }.${format};
+
+  rootPartition = { # switch-case
+    legacy = "1";
+    efi = "2";
+  }.${partitionTableType};
+
+  partitionDiskScript = { # switch-case
+    legacy = ''
+      parted --script $diskImage -- \
+        mklabel msdos \
+        mkpart primary ext4 1MiB -1
+    '';
+    efi = ''
+      parted --script $diskImage -- \
+        mklabel gpt \
+        mkpart ESP fat32 8MiB 256MiB \
+        set 1 boot on \
+        mkpart primary ext4 256MiB -1
+    '';
+    none = "";
+  }.${partitionTableType};
+
+  nixpkgs = cleanSource pkgs.path;
+
+  # FIXME: merge with channel.nix / make-channel.nix.
+  channelSources = pkgs.runCommand "nixos-${config.system.nixos.version}" {} ''
+    mkdir -p $out
+    cp -prd ${nixpkgs.outPath} $out/nixos
+    chmod -R u+w $out/nixos
+    if [ ! -e $out/nixos/nixpkgs ]; then
+      ln -s . $out/nixos/nixpkgs
+    fi
+    rm -rf $out/nixos/.git
+    echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
+  '';
+
+  binPath = with pkgs; makeBinPath (
+    [ rsync
+      utillinux
+      parted
+      e2fsprogs
+      lkl
+      config.system.build.nixos-install
+      config.system.build.nixos-enter
+      nix
+    ] ++ stdenv.initialPath);
+
+  # I'm preserving the line below because I'm going to search for it across nixpkgs to consolidate
+  # image building logic. The comment right below this now appears in 4 different places in nixpkgs :)
+  # !!! should use XML.
+  sources = map (x: x.source) contents;
+  targets = map (x: x.target) contents;
+
+  closureInfo = pkgs.closureInfo { rootPaths = [ config.system.build.toplevel channelSources ]; };
+
+  prepareImage = ''
+    export PATH=${binPath}
+
+    # Yes, mkfs.ext4 takes different units in different contexts. Fun.
+    sectorsToKilobytes() {
+      echo $(( ( "$1" * 512 ) / 1024 ))
+    }
+
+    sectorsToBytes() {
+      echo $(( "$1" * 512  ))
+    }
+
+    mkdir $out
+    diskImage=nixos.raw
+    truncate -s ${toString diskSize}M $diskImage
+
+    ${partitionDiskScript}
+
+    ${if partitionTableType != "none" then ''
+      # Get start & length of the root partition in sectors to $START and $SECTORS.
+      eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs)
+
+      mkfs.${fsType} -F -L ${label} $diskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
+    '' else ''
+      mkfs.${fsType} -F -L ${label} $diskImage
+    ''}
+
+    root="$PWD/root"
+    mkdir -p $root
+
+    # Copy arbitrary other files into the image
+    # Semi-shamelessly copied from make-etc.sh. I (@copumpkin) shall factor this stuff out as part of
+    # https://github.com/NixOS/nixpkgs/issues/23052.
+    set -f
+    sources_=(${concatStringsSep " " sources})
+    targets_=(${concatStringsSep " " targets})
+    set +f
+
+    for ((i = 0; i < ''${#targets_[@]}; i++)); do
+      source="''${sources_[$i]}"
+      target="''${targets_[$i]}"
+
+      if [[ "$source" =~ '*' ]]; then
+        # If the source name contains '*', perform globbing.
+        mkdir -p $root/$target
+        for fn in $source; do
+          rsync -a --no-o --no-g "$fn" $root/$target/
+        done
+      else
+        mkdir -p $root/$(dirname $target)
+        if ! [ -e $root/$target ]; then
+          rsync -a --no-o --no-g $source $root/$target
+        else
+          echo "duplicate entry $target -> $source"
+          exit 1
+        fi
+      fi
+    done
+
+    export HOME=$TMPDIR
+
+    # Provide a Nix database so that nixos-install can copy closures.
+    export NIX_STATE_DIR=$TMPDIR/state
+    nix-store --load-db < ${closureInfo}/registration
+
+    echo "running nixos-install..."
+    nixos-install --root $root --no-bootloader --no-root-passwd \
+      --system ${config.system.build.toplevel} --channel ${channelSources} --substituters ""
+
+    echo "copying staging root to image..."
+    cptofs -p ${optionalString (partitionTableType != "none") "-P ${rootPartition}"} -t ${fsType} -i $diskImage $root/* /
+  '';
+in pkgs.vmTools.runInLinuxVM (
+  pkgs.runCommand name
+    { preVM = prepareImage;
+      buildInputs = with pkgs; [ utillinux e2fsprogs dosfstools ];
+      postVM = ''
+        ${if format == "raw" then ''
+          mv $diskImage $out/${filename}
+        '' else ''
+          ${pkgs.qemu}/bin/qemu-img convert -f raw -O ${format} ${compress} $diskImage $out/${filename}
+        ''}
+        diskImage=$out/${filename}
+        ${postVM}
+      '';
+      memSize = 1024;
+    }
+    ''
+      export PATH=${binPath}:$PATH
+
+      rootDisk=${if partitionTableType != "none" then "/dev/vda${rootPartition}" else "/dev/vda"}
+
+      # Some tools assume these exist
+      ln -s vda /dev/xvda
+      ln -s vda /dev/sda
+
+      mountPoint=/mnt
+      mkdir $mountPoint
+      mount $rootDisk $mountPoint
+
+      # Create the ESP and mount it. Unlike e2fsprogs, mkfs.vfat doesn't support an
+      # '-E offset=X' option, so we can't do this outside the VM.
+      ${optionalString (partitionTableType == "efi") ''
+        mkdir -p /mnt/boot
+        mkfs.vfat -n ESP /dev/vda1
+        mount /dev/vda1 /mnt/boot
+      ''}
+
+      # Install a configuration.nix
+      mkdir -p /mnt/etc/nixos
+      ${optionalString (configFile != null) ''
+        cp ${configFile} /mnt/etc/nixos/configuration.nix
+      ''}
+
+      # Set up core system link, GRUB, etc.
+      NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root $mountPoint -- /nix/var/nix/profiles/system/bin/switch-to-configuration boot
+
+      # The above scripts will generate a random machine-id and we don't want to bake a single ID into all our images
+      rm -f $mountPoint/etc/machine-id
+
+      umount -R /mnt
+
+      # Make sure resize2fs works. Note that resize2fs has stricter criteria for resizing than a normal
+      # mount, so the `-c 0` and `-i 0` don't affect it. Setting it to `now` doesn't produce deterministic
+      # output, of course, but we can fix that when/if we start making images deterministic.
+      ${optionalString (fsType == "ext4") ''
+        tune2fs -T now -c 0 -i 0 $rootDisk
+      ''}
+    ''
+)
diff --git a/nixpkgs/nixos/lib/make-ext4-fs.nix b/nixpkgs/nixos/lib/make-ext4-fs.nix
new file mode 100644
index 000000000000..932adcd97967
--- /dev/null
+++ b/nixpkgs/nixos/lib/make-ext4-fs.nix
@@ -0,0 +1,91 @@
+# Builds an ext4 image containing a populated /nix/store with the closure
+# of store paths passed in the storePaths parameter, in addition to the
+# contents of a directory that can be populated with commands. The
+# generated image is sized to only fit its contents, with the expectation
+# that a script resizes the filesystem at boot time.
+{ pkgs
+# List of derivations to be included
+, storePaths
+# Shell commands to populate the ./files directory.
+# All files in that directory are copied to the root of the FS.
+, populateImageCommands ? ""
+, volumeLabel
+, uuid ? "44444444-4444-4444-8888-888888888888"
+, e2fsprogs
+, libfaketime
+, perl
+, lkl
+}:
+
+let
+  sdClosureInfo = pkgs.buildPackages.closureInfo { rootPaths = storePaths; };
+in
+
+pkgs.stdenv.mkDerivation {
+  name = "ext4-fs.img";
+
+  nativeBuildInputs = [e2fsprogs.bin libfaketime perl lkl];
+
+  buildCommand =
+    ''
+      (
+      mkdir -p ./files
+      ${populateImageCommands}
+      )
+      # Add the closures of the top-level store objects.
+      storePaths=$(cat ${sdClosureInfo}/store-paths)
+
+      # Make a crude approximation of the size of the target image.
+      # If the script starts failing, increase the fudge factors here.
+      numInodes=$(find $storePaths ./files | wc -l)
+      numDataBlocks=$(du -s -c -B 4096 --apparent-size $storePaths ./files | tail -1 | awk '{ print int($1 * 1.03) }')
+      bytes=$((2 * 4096 * $numInodes + 4096 * $numDataBlocks))
+      echo "Creating an EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks)"
+
+      truncate -s $bytes $out
+      faketime -f "1970-01-01 00:00:01" mkfs.ext4 -L ${volumeLabel} -U ${uuid} $out
+
+      # Also include a manifest of the closures in a format suitable for nix-store --load-db.
+      cp ${sdClosureInfo}/registration nix-path-registration
+      cptofs -t ext4 -i $out nix-path-registration /
+
+      # Create nix/store before copying paths
+      faketime -f "1970-01-01 00:00:01" mkdir -p nix/store
+      cptofs -t ext4 -i $out nix /
+
+      echo "copying store paths to image..."
+      cptofs -t ext4 -i $out $storePaths /nix/store/
+
+      (
+      echo "copying files to image..."
+      cd ./files
+      cptofs -t ext4 -i $out ./* /
+      )
+
+      # I have ended up with corrupted images sometimes, I suspect that happens when the build machine's disk gets full during the build.
+      if ! fsck.ext4 -n -f $out; then
+        echo "--- Fsck failed for EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks) ---"
+        cat errorlog
+        return 1
+      fi
+
+      (
+        # Resizes **snugly** to its actual limits (or closer to)
+        free=$(dumpe2fs $out | grep '^Free blocks:')
+        blocksize=$(dumpe2fs $out | grep '^Block size:')
+        blocks=$(dumpe2fs $out | grep '^Block count:')
+        blocks=$((''${blocks##*:})) # format the number.
+        blocksize=$((''${blocksize##*:})) # format the number.
+        # System can't boot with 0 blocks free.
+        # Add 16MiB of free space
+        fudge=$(( 16 * 1024 * 1024 / blocksize ))
+        size=$(( blocks - ''${free##*:} + fudge ))
+
+        echo "Resizing from $blocks blocks to $size blocks. (~ $((size*blocksize/1024/1024))MiB)"
+        EXT2FS_NO_MTAB_OK=yes resize2fs $out -f $size
+      )
+
+      # And a final fsck, because of the previous truncating.
+      fsck.ext4 -n -f $out
+    '';
+}
diff --git a/nixpkgs/nixos/lib/make-iso9660-image.nix b/nixpkgs/nixos/lib/make-iso9660-image.nix
new file mode 100644
index 000000000000..8cd19b6e1874
--- /dev/null
+++ b/nixpkgs/nixos/lib/make-iso9660-image.nix
@@ -0,0 +1,65 @@
+{ stdenv, closureInfo, xorriso, syslinux
+
+, # The file name of the resulting ISO image.
+  isoName ? "cd.iso"
+
+, # The files and directories to be placed in the ISO file system.
+  # This is a list of attribute sets {source, target} where `source'
+  # is the file system object (regular file or directory) to be
+  # grafted in the file system at path `target'.
+  contents
+
+, # In addition to `contents', the closure of the store paths listed
+  # in `packages' are also placed in the Nix store of the CD.  This is
+  # a list of attribute sets {object, symlink} where `object' if a
+  # store path whose closure will be copied, and `symlink' is a
+  # symlink to `object' that will be added to the CD.
+  storeContents ? []
+
+, # Whether this should be an El-Torito bootable CD.
+  bootable ? false
+
+, # Whether this should be an efi-bootable El-Torito CD.
+  efiBootable ? false
+
+, # Whether this should be an hybrid CD (bootable from USB as well as CD).
+  usbBootable ? false
+
+, # The path (in the ISO file system) of the boot image.
+  bootImage ? ""
+
+, # The path (in the ISO file system) of the efi boot image.
+  efiBootImage ? ""
+
+, # The path (outside the ISO file system) of the isohybrid-mbr image.
+  isohybridMbrImage ? ""
+
+, # Whether to compress the resulting ISO image with bzip2.
+  compressImage ? false
+
+, # The volume ID.
+  volumeID ? ""
+}:
+
+assert bootable -> bootImage != "";
+assert efiBootable -> efiBootImage != "";
+assert usbBootable -> isohybridMbrImage != "";
+
+stdenv.mkDerivation {
+  name = isoName;
+  builder = ./make-iso9660-image.sh;
+  buildInputs = [ xorriso syslinux ];
+
+  inherit isoName bootable bootImage compressImage volumeID efiBootImage efiBootable isohybridMbrImage usbBootable;
+
+  # !!! should use XML.
+  sources = map (x: x.source) contents;
+  targets = map (x: x.target) contents;
+
+  # !!! should use XML.
+  objects = map (x: x.object) storeContents;
+  symlinks = map (x: x.symlink) storeContents;
+
+  # For obtaining the closure of `storeContents'.
+  closureInfo = closureInfo { rootPaths = map (x: x.object) storeContents; };
+}
diff --git a/nixpkgs/nixos/lib/make-iso9660-image.sh b/nixpkgs/nixos/lib/make-iso9660-image.sh
new file mode 100644
index 000000000000..b7b1ab52a637
--- /dev/null
+++ b/nixpkgs/nixos/lib/make-iso9660-image.sh
@@ -0,0 +1,136 @@
+source $stdenv/setup
+
+sources_=($sources)
+targets_=($targets)
+
+objects=($objects)
+symlinks=($symlinks)
+
+
+# Remove the initial slash from a path, since genisofs likes it that way.
+stripSlash() {
+    res="$1"
+    if test "${res:0:1}" = /; then res=${res:1}; fi
+}
+
+# Escape potential equal signs (=) with backslash (\=)
+escapeEquals() {
+    echo "$1" | sed -e 's/\\/\\\\/g' -e 's/=/\\=/g'
+}
+
+# Queues an file/directory to be placed on the ISO.
+# An entry consists of a local source path (2) and
+# a destination path on the ISO (1).
+addPath() {
+    target="$1"
+    source="$2"
+    echo "$(escapeEquals "$target")=$(escapeEquals "$source")" >> pathlist
+}
+
+stripSlash "$bootImage"; bootImage="$res"
+
+
+if test -n "$bootable"; then
+
+    # The -boot-info-table option modifies the $bootImage file, so
+    # find it in `contents' and make a copy of it (since the original
+    # is read-only in the Nix store...).
+    for ((i = 0; i < ${#targets_[@]}; i++)); do
+        stripSlash "${targets_[$i]}"
+        if test "$res" = "$bootImage"; then
+            echo "copying the boot image ${sources_[$i]}"
+            cp "${sources_[$i]}" boot.img
+            chmod u+w boot.img
+            sources_[$i]=boot.img
+        fi
+    done
+
+    isoBootFlags="-eltorito-boot ${bootImage}
+                  -eltorito-catalog .boot.cat
+                  -no-emul-boot -boot-load-size 4 -boot-info-table
+                  --sort-weight 1 /isolinux" # Make sure isolinux is near the beginning of the ISO
+fi
+
+if test -n "$usbBootable"; then
+    usbBootFlags="-isohybrid-mbr ${isohybridMbrImage}"
+fi
+
+if test -n "$efiBootable"; then
+    efiBootFlags="-eltorito-alt-boot
+                  -e $efiBootImage
+                  -no-emul-boot
+                  -isohybrid-gpt-basdat"
+fi
+
+touch pathlist
+
+
+# Add the individual files.
+for ((i = 0; i < ${#targets_[@]}; i++)); do
+    stripSlash "${targets_[$i]}"
+    addPath "$res" "${sources_[$i]}"
+done
+
+
+# Add the closures of the top-level store objects.
+for i in $(< $closureInfo/store-paths); do
+    addPath "${i:1}" "$i"
+done
+
+
+# Also include a manifest of the closures in a format suitable for
+# nix-store --load-db.
+if [[ ${#objects[*]} != 0 ]]; then
+    cp $closureInfo/registration nix-path-registration
+    addPath "nix-path-registration" "nix-path-registration"
+fi
+
+
+# Add symlinks to the top-level store objects.
+for ((n = 0; n < ${#objects[*]}; n++)); do
+    object=${objects[$n]}
+    symlink=${symlinks[$n]}
+    if test "$symlink" != "none"; then
+        mkdir -p $(dirname ./$symlink)
+        ln -s $object ./$symlink
+        addPath "$symlink" "./$symlink"
+    fi
+done
+
+mkdir -p $out/iso
+
+xorriso="xorriso
+ -as mkisofs
+ -iso-level 3
+ -volid ${volumeID}
+ -appid nixos
+ -publisher nixos
+ -graft-points
+ -full-iso9660-filenames
+ ${isoBootFlags}
+ ${usbBootFlags}
+ ${efiBootFlags}
+ -r
+ -path-list pathlist
+ --sort-weight 0 /
+"
+
+$xorriso -output $out/iso/$isoName
+
+if test -n "$usbBootable"; then
+    echo "Making image hybrid..."
+    if test -n "$efiBootable"; then
+        isohybrid --uefi $out/iso/$isoName
+    else
+        isohybrid $out/iso/$isoName
+    fi
+fi
+
+if test -n "$compressImage"; then
+    echo "Compressing image..."
+    bzip2 $out/iso/$isoName
+fi
+
+mkdir -p $out/nix-support
+echo $system > $out/nix-support/system
+echo "file iso $out/iso/$isoName" >> $out/nix-support/hydra-build-products
diff --git a/nixpkgs/nixos/lib/make-options-doc/default.nix b/nixpkgs/nixos/lib/make-options-doc/default.nix
new file mode 100644
index 000000000000..88e052106a28
--- /dev/null
+++ b/nixpkgs/nixos/lib/make-options-doc/default.nix
@@ -0,0 +1,164 @@
+/* Generate JSON, XML and DocBook documentation for given NixOS options.
+
+   Minimal example:
+
+    { pkgs,  }:
+
+    let
+      eval = import (pkgs.path + "/nixos/lib/eval-config.nix") {
+        baseModules = [
+          ../module.nix
+        ];
+        modules = [];
+      };
+    in pkgs.nixosOptionsDoc {
+      options = eval.options;
+    }
+
+*/
+{ pkgs
+, lib
+, options
+, transformOptions ? lib.id  # function for additional tranformations of the options
+, revision ? "" # Specify revision for the options
+}:
+
+let
+  # Replace functions by the string <function>
+  substFunction = x:
+    if builtins.isAttrs x then lib.mapAttrs (name: substFunction) x
+    else if builtins.isList x then map substFunction x
+    else if lib.isFunction x then "<function>"
+    else x;
+
+  optionsListDesc = lib.flip map optionsListVisible
+   (opt: transformOptions opt
+    // lib.optionalAttrs (opt ? example) { example = substFunction opt.example; }
+    // lib.optionalAttrs (opt ? default) { default = substFunction opt.default; }
+    // lib.optionalAttrs (opt ? type) { type = substFunction opt.type; }
+    // lib.optionalAttrs (opt ? relatedPackages && opt.relatedPackages != []) { relatedPackages = genRelatedPackages opt.relatedPackages; }
+   );
+
+  # Generate DocBook documentation for a list of packages. This is
+  # what `relatedPackages` option of `mkOption` from
+  # ../../../lib/options.nix influences.
+  #
+  # Each element of `relatedPackages` can be either
+  # - a string:  that will be interpreted as an attribute name from `pkgs`,
+  # - a list:    that will be interpreted as an attribute path from `pkgs`,
+  # - an attrset: that can specify `name`, `path`, `package`, `comment`
+  #   (either of `name`, `path` is required, the rest are optional).
+  genRelatedPackages = packages:
+    let
+      unpack = p: if lib.isString p then { name = p; }
+                  else if lib.isList p then { path = p; }
+                  else p;
+      describe = args:
+        let
+          title = args.title or null;
+          name = args.name or (lib.concatStringsSep "." args.path);
+          path = args.path or [ args.name ];
+          package = args.package or (lib.attrByPath path (throw "Invalid package attribute path `${toString path}'") pkgs);
+        in "<listitem>"
+        + "<para><literal>${lib.optionalString (title != null) "${title} aka "}pkgs.${name} (${package.meta.name})</literal>"
+        + lib.optionalString (!package.meta.available) " <emphasis>[UNAVAILABLE]</emphasis>"
+        + ": ${package.meta.description or "???"}.</para>"
+        + lib.optionalString (args ? comment) "\n<para>${args.comment}</para>"
+        # Lots of `longDescription's break DocBook, so we just wrap them into <programlisting>
+        + lib.optionalString (package.meta ? longDescription) "\n<programlisting>${package.meta.longDescription}</programlisting>"
+        + "</listitem>";
+    in "<itemizedlist>${lib.concatStringsSep "\n" (map (p: describe (unpack p)) packages)}</itemizedlist>";
+
+  # Custom "less" that pushes up all the things ending in ".enable*"
+  # and ".package*"
+  optionLess = a: b:
+    let
+      ise = lib.hasPrefix "enable";
+      isp = lib.hasPrefix "package";
+      cmp = lib.splitByAndCompare ise lib.compare
+                                 (lib.splitByAndCompare isp lib.compare lib.compare);
+    in lib.compareLists cmp a.loc b.loc < 0;
+
+  # Remove invisible and internal options.
+  optionsListVisible = lib.filter (opt: opt.visible && !opt.internal) (lib.optionAttrSetToDocList options);
+
+  # Customly sort option list for the man page.
+  optionsList = lib.sort optionLess optionsListDesc;
+
+  # Convert the list of options into an XML file.
+  optionsXML = builtins.toFile "options.xml" (builtins.toXML optionsList);
+
+  optionsNix = builtins.listToAttrs (map (o: { name = o.name; value = removeAttrs o ["name" "visible" "internal"]; }) optionsList);
+
+  # TODO: declarations: link to github
+  singleAsciiDoc = name: value: ''
+    == ${name}
+
+    ${value.description}
+
+    [discrete]
+    === details
+
+    Type:: ${value.type}
+    ${ if lib.hasAttr "default" value
+       then ''
+        Default::
+        +
+        ----
+        ${builtins.toJSON value.default}
+        ----
+      ''
+      else "No Default:: {blank}"
+    }
+    ${ if value.readOnly
+       then "Read Only:: {blank}"
+      else ""
+    }
+    ${ if lib.hasAttr "example" value
+       then ''
+        Example::
+        +
+        ----
+        ${builtins.toJSON value.example}
+        ----
+      ''
+      else "No Example:: {blank}"
+    }
+  '';
+
+in rec {
+  inherit optionsNix;
+
+  optionsAsciiDoc = lib.concatStringsSep "\n" (lib.mapAttrsToList singleAsciiDoc optionsNix);
+
+  optionsJSON = pkgs.runCommand "options.json"
+    { meta.description = "List of NixOS options in JSON format";
+    }
+    ''
+      # Export list of options in different format.
+      dst=$out/share/doc/nixos
+      mkdir -p $dst
+
+      cp ${builtins.toFile "options.json" (builtins.unsafeDiscardStringContext (builtins.toJSON optionsNix))} $dst/options.json
+
+      mkdir -p $out/nix-support
+      echo "file json $dst/options.json" >> $out/nix-support/hydra-build-products
+    ''; # */
+
+  optionsDocBook = pkgs.runCommand "options-docbook.xml" {} ''
+    optionsXML=${optionsXML}
+    if grep /nixpkgs/nixos/modules $optionsXML; then
+      echo "The manual appears to depend on the location of Nixpkgs, which is bad"
+      echo "since this prevents sharing via the NixOS channel.  This is typically"
+      echo "caused by an option default that refers to a relative path (see above"
+      echo "for hints about the offending path)."
+      exit 1
+    fi
+
+    ${pkgs.libxslt.bin}/bin/xsltproc \
+      --stringparam revision '${revision}' \
+      -o intermediate.xml ${./options-to-docbook.xsl} $optionsXML
+    ${pkgs.libxslt.bin}/bin/xsltproc \
+      -o "$out" ${./postprocess-option-descriptions.xsl} intermediate.xml
+  '';
+}
diff --git a/nixpkgs/nixos/lib/make-options-doc/options-to-docbook.xsl b/nixpkgs/nixos/lib/make-options-doc/options-to-docbook.xsl
new file mode 100644
index 000000000000..72ac89d4ff62
--- /dev/null
+++ b/nixpkgs/nixos/lib/make-options-doc/options-to-docbook.xsl
@@ -0,0 +1,236 @@
+<?xml version="1.0"?>
+
+<xsl:stylesheet version="1.0"
+                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:str="http://exslt.org/strings"
+                xmlns:xlink="http://www.w3.org/1999/xlink"
+                xmlns:nixos="tag:nixos.org"
+                xmlns="http://docbook.org/ns/docbook"
+                extension-element-prefixes="str"
+                >
+
+  <xsl:output method='xml' encoding="UTF-8" />
+
+  <xsl:param name="revision" />
+  <xsl:param name="program" />
+
+
+  <xsl:template match="/expr/list">
+    <appendix xml:id="appendix-configuration-options">
+      <title>Configuration Options</title>
+      <variablelist xml:id="configuration-variable-list">
+        <xsl:for-each select="attrs">
+          <xsl:variable name="id" select="concat('opt-', str:replace(str:replace(str:replace(str:replace(attr[@name = 'name']/string/@value, '*', '_'), '&lt;', '_'), '>', '_'), '?', '_'))" />
+          <varlistentry>
+            <term xlink:href="#{$id}">
+              <xsl:attribute name="xml:id"><xsl:value-of select="$id"/></xsl:attribute>
+              <option>
+                <xsl:value-of select="attr[@name = 'name']/string/@value" />
+              </option>
+            </term>
+
+            <listitem>
+
+              <nixos:option-description>
+                <para>
+                  <xsl:value-of disable-output-escaping="yes"
+                                select="attr[@name = 'description']/string/@value" />
+                </para>
+              </nixos:option-description>
+
+              <xsl:if test="attr[@name = 'type']">
+                <para>
+                  <emphasis>Type:</emphasis>
+                  <xsl:text> </xsl:text>
+                  <xsl:value-of select="attr[@name = 'type']/string/@value"/>
+                  <xsl:if test="attr[@name = 'readOnly']/bool/@value = 'true'">
+                    <xsl:text> </xsl:text>
+                    <emphasis>(read only)</emphasis>
+                  </xsl:if>
+                </para>
+              </xsl:if>
+
+              <xsl:if test="attr[@name = 'default']">
+                <para>
+                  <emphasis>Default:</emphasis>
+                  <xsl:text> </xsl:text>
+                  <xsl:apply-templates select="attr[@name = 'default']" mode="top" />
+                </para>
+              </xsl:if>
+
+              <xsl:if test="attr[@name = 'example']">
+                <para>
+                  <emphasis>Example:</emphasis>
+                  <xsl:text> </xsl:text>
+                  <xsl:choose>
+                    <xsl:when test="attr[@name = 'example']/attrs[attr[@name = '_type' and string[@value = 'literalExample']]]">
+                      <programlisting><xsl:value-of select="attr[@name = 'example']/attrs/attr[@name = 'text']/string/@value" /></programlisting>
+                    </xsl:when>
+                    <xsl:otherwise>
+                      <xsl:apply-templates select="attr[@name = 'example']" mode="top" />
+                    </xsl:otherwise>
+                  </xsl:choose>
+                </para>
+              </xsl:if>
+
+              <xsl:if test="attr[@name = 'relatedPackages']">
+                <para>
+                  <emphasis>Related packages:</emphasis>
+                  <xsl:text> </xsl:text>
+                  <xsl:value-of disable-output-escaping="yes"
+                                select="attr[@name = 'relatedPackages']/string/@value" />
+                </para>
+              </xsl:if>
+
+              <xsl:if test="count(attr[@name = 'declarations']/list/*) != 0">
+                <para>
+                  <emphasis>Declared by:</emphasis>
+                </para>
+                <xsl:apply-templates select="attr[@name = 'declarations']" />
+              </xsl:if>
+
+              <xsl:if test="count(attr[@name = 'definitions']/list/*) != 0">
+                <para>
+                  <emphasis>Defined by:</emphasis>
+                </para>
+                <xsl:apply-templates select="attr[@name = 'definitions']" />
+              </xsl:if>
+
+            </listitem>
+
+          </varlistentry>
+
+        </xsl:for-each>
+
+      </variablelist>
+    </appendix>
+  </xsl:template>
+
+
+  <xsl:template match="*" mode="top">
+    <xsl:choose>
+      <xsl:when test="string[contains(@value, '&#010;')]">
+<programlisting>
+<xsl:text>''
+</xsl:text><xsl:value-of select='str:replace(string/@value, "${", "&apos;&apos;${")' /><xsl:text>''</xsl:text></programlisting>
+      </xsl:when>
+      <xsl:otherwise>
+        <literal><xsl:apply-templates /></literal>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+
+
+  <xsl:template match="null">
+    <xsl:text>null</xsl:text>
+  </xsl:template>
+
+
+  <xsl:template match="string">
+    <xsl:choose>
+      <xsl:when test="(contains(@value, '&quot;') or contains(@value, '\')) and not(contains(@value, '&#010;'))">
+        <xsl:text>''</xsl:text><xsl:value-of select='str:replace(@value, "${", "&apos;&apos;${")' /><xsl:text>''</xsl:text>
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:text>"</xsl:text><xsl:value-of select="str:replace(str:replace(str:replace(str:replace(@value, '\', '\\'), '&quot;', '\&quot;'), '&#010;', '\n'), '$', '\$')" /><xsl:text>"</xsl:text>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+
+
+  <xsl:template match="int">
+    <xsl:value-of select="@value" />
+  </xsl:template>
+
+
+  <xsl:template match="bool[@value = 'true']">
+    <xsl:text>true</xsl:text>
+  </xsl:template>
+
+
+  <xsl:template match="bool[@value = 'false']">
+    <xsl:text>false</xsl:text>
+  </xsl:template>
+
+
+  <xsl:template match="list">
+    [
+    <xsl:for-each select="*">
+      <xsl:apply-templates select="." />
+      <xsl:text> </xsl:text>
+    </xsl:for-each>
+    ]
+  </xsl:template>
+
+
+  <xsl:template match="attrs[attr[@name = '_type' and string[@value = 'literalExample']]]">
+    <xsl:value-of select="attr[@name = 'text']/string/@value" />
+  </xsl:template>
+
+
+  <xsl:template match="attrs">
+    {
+    <xsl:for-each select="attr">
+      <xsl:value-of select="@name" />
+      <xsl:text> = </xsl:text>
+      <xsl:apply-templates select="*" /><xsl:text>; </xsl:text>
+    </xsl:for-each>
+    }
+  </xsl:template>
+
+
+  <xsl:template match="derivation">
+    <replaceable>(build of <xsl:value-of select="attr[@name = 'name']/string/@value" />)</replaceable>
+  </xsl:template>
+
+  <xsl:template match="attr[@name = 'declarations' or @name = 'definitions']">
+    <simplelist>
+      <xsl:for-each select="list/string">
+        <member><filename>
+          <!-- Hyperlink the filename either to the NixOS Subversion
+          repository (if it’s a module and we have a revision number),
+          or to the local filesystem. -->
+          <xsl:choose>
+            <xsl:when test="not(starts-with(@value, '/'))">
+              <xsl:choose>
+                <xsl:when test="$revision = 'local'">
+                  <xsl:attribute name="xlink:href">https://github.com/NixOS/nixpkgs/blob/master/<xsl:value-of select="@value"/></xsl:attribute>
+                </xsl:when>
+                <xsl:otherwise>
+                  <xsl:attribute name="xlink:href">https://github.com/NixOS/nixpkgs/blob/<xsl:value-of select="$revision"/>/<xsl:value-of select="@value"/></xsl:attribute>
+                </xsl:otherwise>
+              </xsl:choose>
+            </xsl:when>
+            <xsl:when test="$revision != 'local' and $program = 'nixops' and contains(@value, '/nix/')">
+              <xsl:attribute name="xlink:href">https://github.com/NixOS/nixops/blob/<xsl:value-of select="$revision"/>/nix/<xsl:value-of select="substring-after(@value, '/nix/')"/></xsl:attribute>
+            </xsl:when>
+            <xsl:otherwise>
+              <xsl:attribute name="xlink:href">file://<xsl:value-of select="@value"/></xsl:attribute>
+            </xsl:otherwise>
+          </xsl:choose>
+          <!-- Print the filename and make it user-friendly by replacing the
+          /nix/store/<hash> prefix by the default location of nixos
+          sources. -->
+          <xsl:choose>
+            <xsl:when test="not(starts-with(@value, '/'))">
+              &lt;nixpkgs/<xsl:value-of select="@value"/>&gt;
+            </xsl:when>
+            <xsl:when test="contains(@value, 'nixops') and contains(@value, '/nix/')">
+              &lt;nixops/<xsl:value-of select="substring-after(@value, '/nix/')"/>&gt;
+            </xsl:when>
+            <xsl:otherwise>
+              <xsl:value-of select="@value" />
+            </xsl:otherwise>
+          </xsl:choose>
+        </filename></member>
+      </xsl:for-each>
+    </simplelist>
+  </xsl:template>
+
+
+  <xsl:template match="function">
+    <xsl:text>λ</xsl:text>
+  </xsl:template>
+
+
+</xsl:stylesheet>
diff --git a/nixpkgs/nixos/lib/make-options-doc/postprocess-option-descriptions.xsl b/nixpkgs/nixos/lib/make-options-doc/postprocess-option-descriptions.xsl
new file mode 100644
index 000000000000..1201c7612c2e
--- /dev/null
+++ b/nixpkgs/nixos/lib/make-options-doc/postprocess-option-descriptions.xsl
@@ -0,0 +1,115 @@
+<?xml version="1.0"?>
+
+<xsl:stylesheet version="1.0"
+                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:str="http://exslt.org/strings"
+                xmlns:exsl="http://exslt.org/common"
+                xmlns:db="http://docbook.org/ns/docbook"
+                xmlns:nixos="tag:nixos.org"
+                extension-element-prefixes="str exsl">
+  <xsl:output method='xml' encoding="UTF-8" />
+
+  <xsl:template match="@*|node()">
+    <xsl:copy>
+      <xsl:apply-templates select="@*|node()" />
+    </xsl:copy>
+  </xsl:template>
+
+  <xsl:template name="break-up-description">
+    <xsl:param name="input" />
+    <xsl:param name="buffer" />
+
+    <!-- Every time we have two newlines following each other, we want to
+         break it into </para><para>. -->
+    <xsl:variable name="parbreak" select="'&#xa;&#xa;'" />
+
+    <!-- Similar to "(head:tail) = input" in Haskell. -->
+    <xsl:variable name="head" select="$input[1]" />
+    <xsl:variable name="tail" select="$input[position() &gt; 1]" />
+
+    <xsl:choose>
+      <xsl:when test="$head/self::text() and contains($head, $parbreak)">
+        <!-- If the haystack provided to str:split() directly starts or
+             ends with $parbreak, it doesn't generate a <token/> for that,
+             so we are doing this here. -->
+        <xsl:variable name="splitted-raw">
+          <xsl:if test="starts-with($head, $parbreak)"><token /></xsl:if>
+          <xsl:for-each select="str:split($head, $parbreak)">
+            <token><xsl:value-of select="node()" /></token>
+          </xsl:for-each>
+          <!-- Something like ends-with($head, $parbreak), but there is
+               no ends-with() in XSLT, so we need to use substring(). -->
+          <xsl:if test="
+            substring($head, string-length($head) -
+                             string-length($parbreak) + 1) = $parbreak
+          "><token /></xsl:if>
+        </xsl:variable>
+        <xsl:variable name="splitted"
+                      select="exsl:node-set($splitted-raw)/token" />
+        <!-- The buffer we had so far didn't contain any text nodes that
+             contain a $parbreak, so we can put the buffer along with the
+             first token of $splitted into a para element. -->
+        <para xmlns="http://docbook.org/ns/docbook">
+          <xsl:apply-templates select="exsl:node-set($buffer)" />
+          <xsl:apply-templates select="$splitted[1]/node()" />
+        </para>
+        <!-- We have already emitted the first splitted result, so the
+             last result is going to be set as the new $buffer later
+             because its contents may not be directly followed up by a
+             $parbreak. -->
+        <xsl:for-each select="$splitted[position() &gt; 1
+                              and position() &lt; last()]">
+          <para xmlns="http://docbook.org/ns/docbook">
+            <xsl:apply-templates select="node()" />
+          </para>
+        </xsl:for-each>
+        <xsl:call-template name="break-up-description">
+          <xsl:with-param name="input" select="$tail" />
+          <xsl:with-param name="buffer" select="$splitted[last()]/node()" />
+        </xsl:call-template>
+      </xsl:when>
+      <!-- Either non-text node or one without $parbreak, which we just
+           want to buffer and continue recursing. -->
+      <xsl:when test="$input">
+        <xsl:call-template name="break-up-description">
+          <xsl:with-param name="input" select="$tail" />
+          <!-- This essentially appends $head to $buffer. -->
+          <xsl:with-param name="buffer">
+            <xsl:if test="$buffer">
+              <xsl:for-each select="exsl:node-set($buffer)">
+                <xsl:apply-templates select="." />
+              </xsl:for-each>
+            </xsl:if>
+            <xsl:apply-templates select="$head" />
+          </xsl:with-param>
+        </xsl:call-template>
+      </xsl:when>
+      <!-- No more $input, just put the remaining $buffer in a para. -->
+      <xsl:otherwise>
+        <para xmlns="http://docbook.org/ns/docbook">
+          <xsl:apply-templates select="exsl:node-set($buffer)" />
+        </para>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+
+  <xsl:template match="nixos:option-description">
+    <xsl:choose>
+      <!--
+        Only process nodes that are comprised of a single <para/> element,
+        because if that's not the case the description already contains
+        </para><para> in between and we need no further processing.
+      -->
+      <xsl:when test="count(db:para) > 1">
+        <xsl:apply-templates select="node()" />
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:call-template name="break-up-description">
+          <xsl:with-param name="input"
+                          select="exsl:node-set(db:para/node())" />
+        </xsl:call-template>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+
+</xsl:stylesheet>
diff --git a/nixpkgs/nixos/lib/make-squashfs.nix b/nixpkgs/nixos/lib/make-squashfs.nix
new file mode 100644
index 000000000000..ee76c9c5bf24
--- /dev/null
+++ b/nixpkgs/nixos/lib/make-squashfs.nix
@@ -0,0 +1,28 @@
+{ stdenv, squashfsTools, closureInfo
+
+, # The root directory of the squashfs filesystem is filled with the
+  # closures of the Nix store paths listed here.
+  storeContents ? []
+, # Compression parameters.
+  # For zstd compression you can use "zstd -Xcompression-level 6".
+  comp ? "xz -Xdict-size 100%"
+}:
+
+stdenv.mkDerivation {
+  name = "squashfs.img";
+
+  nativeBuildInputs = [ squashfsTools ];
+
+  buildCommand =
+    ''
+      closureInfo=${closureInfo { rootPaths = storeContents; }}
+
+      # Also include a manifest of the closures in a format suitable
+      # for nix-store --load-db.
+      cp $closureInfo/registration nix-path-registration
+
+      # Generate the squashfs image.
+      mksquashfs nix-path-registration $(cat $closureInfo/store-paths) $out \
+        -keep-as-directory -all-root -b 1048576 -comp ${comp}
+    '';
+}
diff --git a/nixpkgs/nixos/lib/make-system-tarball.nix b/nixpkgs/nixos/lib/make-system-tarball.nix
new file mode 100644
index 000000000000..dee91a6ce3f4
--- /dev/null
+++ b/nixpkgs/nixos/lib/make-system-tarball.nix
@@ -0,0 +1,56 @@
+{ stdenv, closureInfo, pixz
+
+, # The file name of the resulting tarball
+  fileName ? "nixos-system-${stdenv.hostPlatform.system}"
+
+, # The files and directories to be placed in the tarball.
+  # This is a list of attribute sets {source, target} where `source'
+  # is the file system object (regular file or directory) to be
+  # grafted in the file system at path `target'.
+  contents
+
+, # In addition to `contents', the closure of the store paths listed
+  # in `packages' are also placed in the Nix store of the tarball.  This is
+  # a list of attribute sets {object, symlink} where `object' if a
+  # store path whose closure will be copied, and `symlink' is a
+  # symlink to `object' that will be added to the tarball.
+  storeContents ? []
+
+  # Extra commands to be executed before archiving files
+, extraCommands ? ""
+
+  # Extra tar arguments
+, extraArgs ? ""
+  # Command used for compression
+, compressCommand ? "pixz"
+  # Extension for the compressed tarball
+, compressionExtension ? ".xz"
+  # extra inputs, like the compressor to use
+, extraInputs ? [ pixz ]
+}:
+
+let
+  symlinks = map (x: x.symlink) storeContents;
+  objects = map (x: x.object) storeContents;
+in
+
+stdenv.mkDerivation {
+  name = "tarball";
+  builder = ./make-system-tarball.sh;
+  buildInputs = extraInputs;
+
+  inherit fileName extraArgs extraCommands compressCommand;
+
+  # !!! should use XML.
+  sources = map (x: x.source) contents;
+  targets = map (x: x.target) contents;
+
+  # !!! should use XML.
+  inherit symlinks objects;
+
+  closureInfo = closureInfo {
+    rootPaths = objects;
+  };
+
+  extension = compressionExtension;
+}
diff --git a/nixpkgs/nixos/lib/make-system-tarball.sh b/nixpkgs/nixos/lib/make-system-tarball.sh
new file mode 100644
index 000000000000..1a0017a1799a
--- /dev/null
+++ b/nixpkgs/nixos/lib/make-system-tarball.sh
@@ -0,0 +1,57 @@
+source $stdenv/setup
+
+sources_=($sources)
+targets_=($targets)
+
+objects=($objects)
+symlinks=($symlinks)
+
+
+# Remove the initial slash from a path, since genisofs likes it that way.
+stripSlash() {
+    res="$1"
+    if test "${res:0:1}" = /; then res=${res:1}; fi
+}
+
+# Add the individual files.
+for ((i = 0; i < ${#targets_[@]}; i++)); do
+    stripSlash "${targets_[$i]}"
+    mkdir -p "$(dirname "$res")"
+    cp -a "${sources_[$i]}" "$res"
+done
+
+
+# Add the closures of the top-level store objects.
+chmod +w .
+mkdir -p nix/store
+for i in $(< $closureInfo/store-paths); do
+    cp -a "$i" "${i:1}"
+done
+
+
+# TODO tar ruxo
+# Also include a manifest of the closures in a format suitable for
+# nix-store --load-db.
+cp $closureInfo/registration nix-path-registration
+
+# Add symlinks to the top-level store objects.
+for ((n = 0; n < ${#objects[*]}; n++)); do
+    object=${objects[$n]}
+    symlink=${symlinks[$n]}
+    if test "$symlink" != "none"; then
+        mkdir -p $(dirname ./$symlink)
+        ln -s $object ./$symlink
+    fi
+done
+
+$extraCommands
+
+mkdir -p $out/tarball
+
+rm env-vars
+
+time tar --sort=name --mtime='@1' --owner=0 --group=0 --numeric-owner -c * $extraArgs | $compressCommand > $out/tarball/$fileName.tar${extension}
+
+mkdir -p $out/nix-support
+echo $system > $out/nix-support/system
+echo "file system-tarball $out/tarball/$fileName.tar${extension}" > $out/nix-support/hydra-build-products
diff --git a/nixpkgs/nixos/lib/qemu-flags.nix b/nixpkgs/nixos/lib/qemu-flags.nix
new file mode 100644
index 000000000000..779f0377a512
--- /dev/null
+++ b/nixpkgs/nixos/lib/qemu-flags.nix
@@ -0,0 +1,25 @@
+# QEMU flags shared between various Nix expressions.
+{ pkgs }:
+
+let
+  zeroPad = n: if n < 10 then "0${toString n}" else toString n;
+in
+
+{
+
+  qemuNICFlags = nic: net: machine:
+    [ "-device virtio-net-pci,netdev=vlan${toString nic},mac=52:54:00:12:${zeroPad net}:${zeroPad machine}"
+      "-netdev vde,id=vlan${toString nic},sock=$QEMU_VDE_SOCKET_${toString net}"
+    ];
+
+  qemuSerialDevice = if pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64 then "ttyS0"
+        else if pkgs.stdenv.isAarch32 || pkgs.stdenv.isAarch64 then "ttyAMA0"
+        else throw "Unknown QEMU serial device for system '${pkgs.stdenv.hostPlatform.system}'";
+
+  qemuBinary = qemuPkg: {
+    "x86_64-linux" = "${qemuPkg}/bin/qemu-kvm -cpu kvm64";
+    "armv7l-linux" = "${qemuPkg}/bin/qemu-system-arm -enable-kvm -machine virt -cpu host";
+    "aarch64-linux" = "${qemuPkg}/bin/qemu-system-aarch64 -enable-kvm -machine virt,gic-version=host -cpu host";
+    "x86_64-darwin" = "${qemuPkg}/bin/qemu-kvm -cpu kvm64";
+  }.${pkgs.stdenv.hostPlatform.system} or "${qemuPkg}/bin/qemu-kvm";
+}
diff --git a/nixpkgs/nixos/lib/test-driver/Logger.pm b/nixpkgs/nixos/lib/test-driver/Logger.pm
new file mode 100644
index 000000000000..080310ea34e0
--- /dev/null
+++ b/nixpkgs/nixos/lib/test-driver/Logger.pm
@@ -0,0 +1,75 @@
+package Logger;
+
+use strict;
+use Thread::Queue;
+use XML::Writer;
+use Encode qw(decode encode);
+use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
+
+sub new {
+    my ($class) = @_;
+    
+    my $logFile = defined $ENV{LOGFILE} ? "$ENV{LOGFILE}" : "/dev/null";
+    my $log = new XML::Writer(OUTPUT => new IO::File(">$logFile"));
+    
+    my $self = {
+        log => $log,
+        logQueue => Thread::Queue->new()
+    };
+    
+    $self->{log}->startTag("logfile");
+    
+    bless $self, $class;
+    return $self;
+}
+
+sub close {
+    my ($self) = @_;
+    $self->{log}->endTag("logfile");
+    $self->{log}->end;
+}
+
+sub drainLogQueue {
+    my ($self) = @_;
+    while (defined (my $item = $self->{logQueue}->dequeue_nb())) {
+        $self->{log}->dataElement("line", sanitise($item->{msg}), 'machine' => $item->{machine}, 'type' => 'serial');
+    }
+}
+
+sub maybePrefix {
+    my ($msg, $attrs) = @_;
+    $msg = $attrs->{machine} . ": " . $msg if defined $attrs->{machine};
+    return $msg;
+}
+
+sub nest {
+    my ($self, $msg, $coderef, $attrs) = @_;
+    print STDERR maybePrefix("$msg\n", $attrs);
+    $self->{log}->startTag("nest");
+    $self->{log}->dataElement("head", $msg, %{$attrs});
+    my $now = clock_gettime(CLOCK_MONOTONIC);
+    $self->drainLogQueue();
+    eval { &$coderef };
+    my $res = $@;
+    $self->drainLogQueue();
+    $self->log(sprintf("(%.2f seconds)", clock_gettime(CLOCK_MONOTONIC) - $now));
+    $self->{log}->endTag("nest");
+    die $@ if $@;
+}
+
+sub sanitise {
+    my ($s) = @_;
+    $s =~ s/[[:cntrl:]\xff]//g;
+    $s = decode('UTF-8', $s, Encode::FB_DEFAULT);
+    return encode('UTF-8', $s, Encode::FB_CROAK);
+}
+
+sub log {
+    my ($self, $msg, $attrs) = @_;
+    chomp $msg;
+    print STDERR maybePrefix("$msg\n", $attrs);
+    $self->drainLogQueue();
+    $self->{log}->dataElement("line", $msg, %{$attrs});
+}
+
+1;
diff --git a/nixpkgs/nixos/lib/test-driver/Machine.pm b/nixpkgs/nixos/lib/test-driver/Machine.pm
new file mode 100644
index 000000000000..4d3d63cd2dbf
--- /dev/null
+++ b/nixpkgs/nixos/lib/test-driver/Machine.pm
@@ -0,0 +1,734 @@
+package Machine;
+
+use strict;
+use threads;
+use Socket;
+use IO::Handle;
+use POSIX qw(dup2);
+use FileHandle;
+use Cwd;
+use File::Basename;
+use File::Path qw(make_path);
+use File::Slurp;
+use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
+
+
+my $showGraphics = defined $ENV{'DISPLAY'};
+
+my $sharedDir;
+
+
+sub new {
+    my ($class, $args) = @_;
+
+    my $startCommand = $args->{startCommand};
+
+    my $name = $args->{name};
+    if (!$name) {
+        $startCommand =~ /run-(.*)-vm$/ if defined $startCommand;
+        $name = $1 || "machine";
+    }
+
+    if (!$startCommand) {
+        # !!! merge with qemu-vm.nix.
+        my $netBackend = "-netdev user,id=net0";
+        my $netFrontend = "-device virtio-net-pci,netdev=net0";
+
+        $netBackend .= "," . $args->{netBackendArgs}
+            if defined $args->{netBackendArgs};
+
+        $netFrontend .= "," . $args->{netFrontendArgs}
+            if defined $args->{netFrontendArgs};
+
+        $startCommand =
+            "qemu-kvm -m 384 $netBackend $netFrontend \$QEMU_OPTS ";
+
+        if (defined $args->{hda}) {
+            if ($args->{hdaInterface} eq "scsi") {
+                $startCommand .= "-drive id=hda,file="
+                               . Cwd::abs_path($args->{hda})
+                               . ",werror=report,if=none "
+                               . "-device scsi-hd,drive=hda ";
+            } else {
+                $startCommand .= "-drive file=" . Cwd::abs_path($args->{hda})
+                               . ",if=" . $args->{hdaInterface}
+                               . ",werror=report ";
+            }
+        }
+
+        $startCommand .= "-cdrom $args->{cdrom} "
+            if defined $args->{cdrom};
+        $startCommand .= "-device piix3-usb-uhci -drive id=usbdisk,file=$args->{usb},if=none,readonly -device usb-storage,drive=usbdisk "
+            if defined $args->{usb};
+        $startCommand .= "-bios $args->{bios} "
+            if defined $args->{bios};
+        $startCommand .= $args->{qemuFlags} || "";
+    }
+
+    my $tmpDir = $ENV{'TMPDIR'} || "/tmp";
+    unless (defined $sharedDir) {
+        $sharedDir = $tmpDir . "/xchg-shared";
+        make_path($sharedDir, { mode => 0700, owner => $< });
+    }
+
+    my $allowReboot = 0;
+    $allowReboot = $args->{allowReboot} if defined $args->{allowReboot};
+
+    my $self = {
+        startCommand => $startCommand,
+        name => $name,
+        allowReboot => $allowReboot,
+        booted => 0,
+        pid => 0,
+        connected => 0,
+        socket => undef,
+        stateDir => "$tmpDir/vm-state-$name",
+        monitor => undef,
+        log => $args->{log},
+        redirectSerial => $args->{redirectSerial} // 1,
+    };
+
+    mkdir $self->{stateDir}, 0700;
+
+    bless $self, $class;
+    return $self;
+}
+
+
+sub log {
+    my ($self, $msg) = @_;
+    $self->{log}->log($msg, { machine => $self->{name} });
+}
+
+
+sub nest {
+    my ($self, $msg, $coderef, $attrs) = @_;
+    $self->{log}->nest($msg, $coderef, { %{$attrs || {}}, machine => $self->{name} });
+}
+
+
+sub name {
+    my ($self) = @_;
+    return $self->{name};
+}
+
+
+sub stateDir {
+    my ($self) = @_;
+    return $self->{stateDir};
+}
+
+
+sub start {
+    my ($self) = @_;
+    return if $self->{booted};
+
+    $self->log("starting vm");
+
+    # Create a socket pair for the serial line input/output of the VM.
+    my ($serialP, $serialC);
+    socketpair($serialP, $serialC, PF_UNIX, SOCK_STREAM, 0) or die;
+
+    # Create a Unix domain socket to which QEMU's monitor will connect.
+    my $monitorPath = $self->{stateDir} . "/monitor";
+    unlink $monitorPath;
+    my $monitorS;
+    socket($monitorS, PF_UNIX, SOCK_STREAM, 0) or die;
+    bind($monitorS, sockaddr_un($monitorPath)) or die "cannot bind monitor socket: $!";
+    listen($monitorS, 1) or die;
+
+    # Create a Unix domain socket to which the root shell in the guest will connect.
+    my $shellPath = $self->{stateDir} . "/shell";
+    unlink $shellPath;
+    my $shellS;
+    socket($shellS, PF_UNIX, SOCK_STREAM, 0) or die;
+    bind($shellS, sockaddr_un($shellPath)) or die "cannot bind shell socket: $!";
+    listen($shellS, 1) or die;
+
+    # Start the VM.
+    my $pid = fork();
+    die if $pid == -1;
+
+    if ($pid == 0) {
+        close $serialP;
+        close $monitorS;
+        close $shellS;
+        if ($self->{redirectSerial}) {
+            open NUL, "</dev/null" or die;
+            dup2(fileno(NUL), fileno(STDIN));
+            dup2(fileno($serialC), fileno(STDOUT));
+            dup2(fileno($serialC), fileno(STDERR));
+        }
+        $ENV{TMPDIR} = $self->{stateDir};
+        $ENV{SHARED_DIR} = $sharedDir;
+        $ENV{USE_TMPDIR} = 1;
+        $ENV{QEMU_OPTS} =
+            ($self->{allowReboot} ? "" : "-no-reboot ") .
+            "-monitor unix:./monitor -chardev socket,id=shell,path=./shell " .
+            "-device virtio-serial -device virtconsole,chardev=shell " .
+            "-device virtio-rng-pci " .
+            ($showGraphics ? "-serial stdio" : "-nographic") . " " . ($ENV{QEMU_OPTS} || "");
+        chdir $self->{stateDir} or die;
+        exec $self->{startCommand};
+        die "running VM script: $!";
+    }
+
+    # Process serial line output.
+    close $serialC;
+
+    threads->create(\&processSerialOutput, $self, $serialP)->detach;
+
+    sub processSerialOutput {
+        my ($self, $serialP) = @_;
+        while (<$serialP>) {
+            chomp;
+            s/\r$//;
+            print STDERR $self->{name}, "# $_\n";
+            $self->{log}->{logQueue}->enqueue({msg => $_, machine => $self->{name}}); # !!!
+        }
+    }
+
+    eval {
+        local $SIG{CHLD} = sub { die "QEMU died prematurely\n"; };
+
+        # Wait until QEMU connects to the monitor.
+        accept($self->{monitor}, $monitorS) or die;
+
+        # Wait until QEMU connects to the root shell socket.  QEMU
+        # does so immediately; this doesn't mean that the root shell
+        # has connected yet inside the guest.
+        accept($self->{socket}, $shellS) or die;
+        $self->{socket}->autoflush(1);
+    };
+    die "$@" if $@;
+
+    $self->waitForMonitorPrompt;
+
+    $self->log("QEMU running (pid $pid)");
+
+    $self->{pid} = $pid;
+    $self->{booted} = 1;
+}
+
+
+# Send a command to the monitor and wait for it to finish.  TODO: QEMU
+# also has a JSON-based monitor interface now, but it doesn't support
+# all commands yet.  We should use it once it does.
+sub sendMonitorCommand {
+    my ($self, $command) = @_;
+    $self->log("sending monitor command: $command");
+    syswrite $self->{monitor}, "$command\n";
+    return $self->waitForMonitorPrompt;
+}
+
+
+# Wait until the monitor sends "(qemu) ".
+sub waitForMonitorPrompt {
+    my ($self) = @_;
+    my $res = "";
+    my $s;
+    while (sysread($self->{monitor}, $s, 1024)) {
+        $res .= $s;
+        last if $res =~ s/\(qemu\) $//;
+    }
+    return $res;
+}
+
+
+# Call the given code reference repeatedly, with 1 second intervals,
+# until it returns 1 or a timeout is reached.
+sub retry {
+    my ($coderef) = @_;
+    my $n;
+    for ($n = 899; $n >=0; $n--) {
+        return if &$coderef($n);
+        sleep 1;
+    }
+    die "action timed out after $n seconds";
+}
+
+
+sub connect {
+    my ($self) = @_;
+    return if $self->{connected};
+
+    $self->nest("waiting for the VM to finish booting", sub {
+
+        $self->start;
+
+        my $now = clock_gettime(CLOCK_MONOTONIC);
+        local $SIG{ALRM} = sub { die "timed out waiting for the VM to connect\n"; };
+        alarm 600;
+        readline $self->{socket} or die "the VM quit before connecting\n";
+        alarm 0;
+
+        $self->log("connected to guest root shell");
+        # We're interested in tracking how close we are to `alarm`.
+        $self->log(sprintf("(connecting took %.2f seconds)", clock_gettime(CLOCK_MONOTONIC) - $now));
+        $self->{connected} = 1;
+
+    });
+}
+
+
+sub waitForShutdown {
+    my ($self) = @_;
+    return unless $self->{booted};
+
+    $self->nest("waiting for the VM to power off", sub {
+        waitpid $self->{pid}, 0;
+        $self->{pid} = 0;
+        $self->{booted} = 0;
+        $self->{connected} = 0;
+    });
+}
+
+
+sub isUp {
+    my ($self) = @_;
+    return $self->{booted} && $self->{connected};
+}
+
+
+sub execute_ {
+    my ($self, $command) = @_;
+
+    $self->connect;
+
+    print { $self->{socket} } ("( $command ); echo '|!=EOF' \$?\n");
+
+    my $out = "";
+
+    while (1) {
+        my $line = readline($self->{socket});
+        die "connection to VM lost unexpectedly" unless defined $line;
+        #$self->log("got line: $line");
+        if ($line =~ /^(.*)\|\!\=EOF\s+(\d+)$/) {
+            $out .= $1;
+            $self->log("exit status $2");
+            return ($2, $out);
+        }
+        $out .= $line;
+    }
+}
+
+
+sub execute {
+    my ($self, $command) = @_;
+    my @res;
+    $self->nest("running command: $command", sub {
+        @res = $self->execute_($command);
+    });
+    return @res;
+}
+
+
+sub succeed {
+    my ($self, @commands) = @_;
+
+    my $res;
+    foreach my $command (@commands) {
+        $self->nest("must succeed: $command", sub {
+            my ($status, $out) = $self->execute_($command);
+            if ($status != 0) {
+                $self->log("output: $out");
+                die "command `$command' did not succeed (exit code $status)\n";
+            }
+            $res .= $out;
+        });
+    }
+
+    return $res;
+}
+
+
+sub mustSucceed {
+    succeed @_;
+}
+
+
+sub waitUntilSucceeds {
+    my ($self, $command) = @_;
+    $self->nest("waiting for success: $command", sub {
+        retry sub {
+            my ($status, $out) = $self->execute($command);
+            return 1 if $status == 0;
+        };
+    });
+}
+
+
+sub waitUntilFails {
+    my ($self, $command) = @_;
+    $self->nest("waiting for failure: $command", sub {
+        retry sub {
+            my ($status, $out) = $self->execute($command);
+            return 1 if $status != 0;
+        };
+    });
+}
+
+
+sub fail {
+    my ($self, $command) = @_;
+    $self->nest("must fail: $command", sub {
+        my ($status, $out) = $self->execute_($command);
+        die "command `$command' unexpectedly succeeded"
+            if $status == 0;
+    });
+}
+
+
+sub mustFail {
+    fail @_;
+}
+
+
+sub getUnitInfo {
+    my ($self, $unit, $user) = @_;
+    my ($status, $lines) = $self->systemctl("--no-pager show \"$unit\"", $user);
+    return undef if $status != 0;
+    my $info = {};
+    foreach my $line (split '\n', $lines) {
+        $line =~ /^([^=]+)=(.*)$/ or next;
+        $info->{$1} = $2;
+    }
+    return $info;
+}
+
+sub systemctl {
+    my ($self, $q, $user) = @_;
+    if ($user) {
+        $q =~ s/'/\\'/g;
+        return $self->execute("su -l $user -c \$'XDG_RUNTIME_DIR=/run/user/`id -u` systemctl --user $q'");
+    }
+
+    return $self->execute("systemctl $q");
+}
+
+# Fail if the given systemd unit is not in the "active" state.
+sub requireActiveUnit {
+    my ($self, $unit) = @_;
+    $self->nest("checking if unit ‘$unit’ has reached state 'active'", sub {
+        my $info = $self->getUnitInfo($unit);
+        my $state = $info->{ActiveState};
+        if ($state ne "active") {
+            die "Expected unit ‘$unit’ to to be in state 'active' but it is in state ‘$state’\n";
+        };
+    });
+}
+
+# Wait for a systemd unit to reach the "active" state.
+sub waitForUnit {
+    my ($self, $unit, $user) = @_;
+    $self->nest("waiting for unit ‘$unit’", sub {
+        retry sub {
+            my $info = $self->getUnitInfo($unit, $user);
+            my $state = $info->{ActiveState};
+            die "unit ‘$unit’ reached state ‘$state’\n" if $state eq "failed";
+            if ($state eq "inactive") {
+                # If there are no pending jobs, then assume this unit
+                # will never reach active state.
+                my ($status, $jobs) = $self->systemctl("list-jobs --full 2>&1", $user);
+                if ($jobs =~ /No jobs/) {  # FIXME: fragile
+                    # Handle the case where the unit may have started
+                    # between the previous getUnitInfo() and
+                    # list-jobs.
+                    my $info2 = $self->getUnitInfo($unit);
+                    die "unit ‘$unit’ is inactive and there are no pending jobs\n"
+                        if $info2->{ActiveState} eq $state;
+                }
+            }
+            return 1 if $state eq "active";
+        };
+    });
+}
+
+
+sub waitForJob {
+    my ($self, $jobName) = @_;
+    return $self->waitForUnit($jobName);
+}
+
+
+# Wait until the specified file exists.
+sub waitForFile {
+    my ($self, $fileName) = @_;
+    $self->nest("waiting for file ‘$fileName’", sub {
+        retry sub {
+            my ($status, $out) = $self->execute("test -e $fileName");
+            return 1 if $status == 0;
+        }
+    });
+}
+
+sub startJob {
+    my ($self, $jobName, $user) = @_;
+    $self->systemctl("start $jobName", $user);
+    # FIXME: check result
+}
+
+sub stopJob {
+    my ($self, $jobName, $user) = @_;
+    $self->systemctl("stop $jobName", $user);
+}
+
+
+# Wait until the machine is listening on the given TCP port.
+sub waitForOpenPort {
+    my ($self, $port) = @_;
+    $self->nest("waiting for TCP port $port", sub {
+        retry sub {
+            my ($status, $out) = $self->execute("nc -z localhost $port");
+            return 1 if $status == 0;
+        }
+    });
+}
+
+
+# Wait until the machine is not listening on the given TCP port.
+sub waitForClosedPort {
+    my ($self, $port) = @_;
+    retry sub {
+        my ($status, $out) = $self->execute("nc -z localhost $port");
+        return 1 if $status != 0;
+    }
+}
+
+
+sub shutdown {
+    my ($self) = @_;
+    return unless $self->{booted};
+
+    print { $self->{socket} } ("poweroff\n");
+
+    $self->waitForShutdown;
+}
+
+
+sub crash {
+    my ($self) = @_;
+    return unless $self->{booted};
+
+    $self->log("forced crash");
+
+    $self->sendMonitorCommand("quit");
+
+    $self->waitForShutdown;
+}
+
+
+# Make the machine unreachable by shutting down eth1 (the multicast
+# interface used to talk to the other VMs).  We keep eth0 up so that
+# the test driver can continue to talk to the machine.
+sub block {
+    my ($self) = @_;
+    $self->sendMonitorCommand("set_link virtio-net-pci.1 off");
+}
+
+
+# Make the machine reachable.
+sub unblock {
+    my ($self) = @_;
+    $self->sendMonitorCommand("set_link virtio-net-pci.1 on");
+}
+
+
+# Take a screenshot of the X server on :0.0.
+sub screenshot {
+    my ($self, $filename) = @_;
+    my $dir = $ENV{'out'} || Cwd::abs_path(".");
+    $filename = "$dir/${filename}.png" if $filename =~ /^\w+$/;
+    my $tmp = "${filename}.ppm";
+    my $name = basename($filename);
+    $self->nest("making screenshot ‘$name’", sub {
+        $self->sendMonitorCommand("screendump $tmp");
+        system("pnmtopng $tmp > ${filename}") == 0
+            or die "cannot convert screenshot";
+        unlink $tmp;
+    }, { image => $name } );
+}
+
+# Get the text of TTY<n>
+sub getTTYText {
+    my ($self, $tty) = @_;
+
+    my ($status, $out) = $self->execute("fold -w\$(stty -F /dev/tty${tty} size | awk '{print \$2}') /dev/vcs${tty}");
+    return $out;
+}
+
+# Wait until TTY<n>'s text matches a particular regular expression
+sub waitUntilTTYMatches {
+    my ($self, $tty, $regexp) = @_;
+
+    $self->nest("waiting for $regexp to appear on tty $tty", sub {
+        retry sub {
+            my ($retries_remaining) = @_;
+            if ($retries_remaining == 0) {
+                $self->log("Last chance to match /$regexp/ on TTY$tty, which currently contains:");
+                $self->log($self->getTTYText($tty));
+            }
+
+            return 1 if $self->getTTYText($tty) =~ /$regexp/;
+        }
+    });
+}
+
+# Debugging: Dump the contents of the TTY<n>
+sub dumpTTYContents {
+    my ($self, $tty) = @_;
+
+    $self->execute("fold -w 80 /dev/vcs${tty} | systemd-cat");
+}
+
+# Take a screenshot and return the result as text using optical character
+# recognition.
+sub getScreenText {
+    my ($self) = @_;
+
+    system("command -v tesseract &> /dev/null") == 0
+        or die "getScreenText used but enableOCR is false";
+
+    my $text;
+    $self->nest("performing optical character recognition", sub {
+        my $tmpbase = Cwd::abs_path(".")."/ocr";
+        my $tmpin = $tmpbase."in.ppm";
+
+        $self->sendMonitorCommand("screendump $tmpin");
+
+        my $magickArgs = "-filter Catrom -density 72 -resample 300 "
+                       . "-contrast -normalize -despeckle -type grayscale "
+                       . "-sharpen 1 -posterize 3 -negate -gamma 100 "
+                       . "-blur 1x65535";
+        my $tessArgs = "-c debug_file=/dev/null --psm 11 --oem 2";
+
+        $text = `convert $magickArgs $tmpin tiff:- | tesseract - - $tessArgs`;
+        my $status = $? >> 8;
+        unlink $tmpin;
+
+        die "OCR failed with exit code $status" if $status != 0;
+    });
+    return $text;
+}
+
+
+# Wait until a specific regexp matches the textual contents of the screen.
+sub waitForText {
+    my ($self, $regexp) = @_;
+    $self->nest("waiting for $regexp to appear on the screen", sub {
+        retry sub {
+            my ($retries_remaining) = @_;
+            if ($retries_remaining == 0) {
+                $self->log("Last chance to match /$regexp/ on the screen, which currently contains:");
+                $self->log($self->getScreenText);
+            }
+
+            return 1 if $self->getScreenText =~ /$regexp/;
+        }
+    });
+}
+
+
+# Wait until it is possible to connect to the X server.  Note that
+# testing the existence of /tmp/.X11-unix/X0 is insufficient.
+sub waitForX {
+    my ($self, $regexp) = @_;
+    $self->nest("waiting for the X11 server", sub {
+        retry sub {
+            my ($status, $out) = $self->execute("journalctl -b SYSLOG_IDENTIFIER=systemd | grep 'Reached target Current graphical'");
+            return 0 if $status != 0;
+            ($status, $out) = $self->execute("[ -e /tmp/.X11-unix/X0 ]");
+            return 1 if $status == 0;
+        }
+    });
+}
+
+
+sub getWindowNames {
+    my ($self) = @_;
+    my $res = $self->mustSucceed(
+        q{xwininfo -root -tree | sed 's/.*0x[0-9a-f]* \"\([^\"]*\)\".*/\1/; t; d'});
+    return split /\n/, $res;
+}
+
+
+sub waitForWindow {
+    my ($self, $regexp) = @_;
+    $self->nest("waiting for a window to appear", sub {
+        retry sub {
+            my @names = $self->getWindowNames;
+
+            my ($retries_remaining) = @_;
+            if ($retries_remaining == 0) {
+                $self->log("Last chance to match /$regexp/ on the the window list, which currently contains:");
+                $self->log(join(", ", @names));
+            }
+
+            foreach my $n (@names) {
+                return 1 if $n =~ /$regexp/;
+            }
+        }
+    });
+}
+
+
+sub copyFileFromHost {
+    my ($self, $from, $to) = @_;
+    my $s = `cat $from` or die;
+    $s =~ s/'/'\\''/g;
+    $self->mustSucceed("echo '$s' > $to");
+}
+
+
+my %charToKey = (
+    'A' => "shift-a", 'N' => "shift-n",  '-' => "0x0C", '_' => "shift-0x0C", '!' => "shift-0x02",
+    'B' => "shift-b", 'O' => "shift-o",  '=' => "0x0D", '+' => "shift-0x0D", '@' => "shift-0x03",
+    'C' => "shift-c", 'P' => "shift-p",  '[' => "0x1A", '{' => "shift-0x1A", '#' => "shift-0x04",
+    'D' => "shift-d", 'Q' => "shift-q",  ']' => "0x1B", '}' => "shift-0x1B", '$' => "shift-0x05",
+    'E' => "shift-e", 'R' => "shift-r",  ';' => "0x27", ':' => "shift-0x27", '%' => "shift-0x06",
+    'F' => "shift-f", 'S' => "shift-s", '\'' => "0x28", '"' => "shift-0x28", '^' => "shift-0x07",
+    'G' => "shift-g", 'T' => "shift-t",  '`' => "0x29", '~' => "shift-0x29", '&' => "shift-0x08",
+    'H' => "shift-h", 'U' => "shift-u", '\\' => "0x2B", '|' => "shift-0x2B", '*' => "shift-0x09",
+    'I' => "shift-i", 'V' => "shift-v",  ',' => "0x33", '<' => "shift-0x33", '(' => "shift-0x0A",
+    'J' => "shift-j", 'W' => "shift-w",  '.' => "0x34", '>' => "shift-0x34", ')' => "shift-0x0B",
+    'K' => "shift-k", 'X' => "shift-x",  '/' => "0x35", '?' => "shift-0x35",
+    'L' => "shift-l", 'Y' => "shift-y",  ' ' => "spc",
+    'M' => "shift-m", 'Z' => "shift-z", "\n" => "ret",
+);
+
+
+sub sendKeys {
+    my ($self, @keys) = @_;
+    foreach my $key (@keys) {
+        $key = $charToKey{$key} if exists $charToKey{$key};
+        $self->sendMonitorCommand("sendkey $key");
+    }
+}
+
+
+sub sendChars {
+    my ($self, $chars) = @_;
+    $self->nest("sending keys ‘$chars’", sub {
+        $self->sendKeys(split //, $chars);
+    });
+}
+
+
+# Sleep N seconds (in virtual guest time, not real time).
+sub sleep {
+    my ($self, $time) = @_;
+    $self->succeed("sleep $time");
+}
+
+
+# Forward a TCP port on the host to a TCP port on the guest.  Useful
+# during interactive testing.
+sub forwardPort {
+    my ($self, $hostPort, $guestPort) = @_;
+    $hostPort = 8080 unless defined $hostPort;
+    $guestPort = 80 unless defined $guestPort;
+    $self->sendMonitorCommand("hostfwd_add tcp::$hostPort-:$guestPort");
+}
+
+
+1;
diff --git a/nixpkgs/nixos/lib/test-driver/log2html.xsl b/nixpkgs/nixos/lib/test-driver/log2html.xsl
new file mode 100644
index 000000000000..0485412b4c8e
--- /dev/null
+++ b/nixpkgs/nixos/lib/test-driver/log2html.xsl
@@ -0,0 +1,135 @@
+<?xml version="1.0"?>
+
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+  <xsl:output method='html' encoding="UTF-8"
+              doctype-public="-//W3C//DTD HTML 4.01//EN"
+              doctype-system="http://www.w3.org/TR/html4/strict.dtd" />
+
+  <xsl:template match="logfile">
+    <html>
+      <head>
+        <script type="text/javascript" src="jquery.min.js"></script>
+        <script type="text/javascript" src="jquery-ui.min.js"></script>
+        <script type="text/javascript" src="treebits.js" />
+        <link rel="stylesheet" href="logfile.css" type="text/css" />
+        <title>Log File</title>
+      </head>
+      <body>
+        <h1>VM build log</h1>
+        <p>
+          <a href="javascript:" class="logTreeExpandAll">Expand all</a> |
+          <a href="javascript:" class="logTreeCollapseAll">Collapse all</a>
+        </p>
+        <ul class='toplevel'>
+          <xsl:for-each select='line|nest'>
+            <li>
+              <xsl:apply-templates select='.'/>
+            </li>
+          </xsl:for-each>
+        </ul>
+
+        <xsl:if test=".//*[@image]">
+          <h1>Screenshots</h1>
+          <ul class="vmScreenshots">
+            <xsl:for-each select='.//*[@image]'>
+              <li><a href="{@image}"><xsl:value-of select="@image" /></a></li>
+            </xsl:for-each>
+          </ul>
+        </xsl:if>
+
+      </body>
+    </html>
+  </xsl:template>
+
+
+  <xsl:template match="nest">
+
+    <!-- The tree should be collapsed by default if all children are
+         unimportant or if the header is unimportant. -->
+    <xsl:variable name="collapsed" select="not(./head[@expanded]) and count(.//*[@error]) = 0"/>
+
+    <xsl:variable name="style"><xsl:if test="$collapsed">display: none;</xsl:if></xsl:variable>
+
+    <xsl:if test="line|nest">
+      <a href="javascript:" class="logTreeToggle">
+        <xsl:choose>
+          <xsl:when test="$collapsed"><xsl:text>+</xsl:text></xsl:when>
+          <xsl:otherwise><xsl:text>-</xsl:text></xsl:otherwise>
+        </xsl:choose>
+      </a>
+      <xsl:text> </xsl:text>
+    </xsl:if>
+
+    <xsl:apply-templates select='head'/>
+
+    <!-- Be careful to only generate <ul>s if there are <li>s, otherwise it’s malformed. -->
+    <xsl:if test="line|nest">
+
+      <ul class='nesting' style="{$style}">
+        <xsl:for-each select='line|nest'>
+
+          <!-- Is this the last line?  If so, mark it as such so that it
+               can be rendered differently. -->
+          <xsl:variable name="class"><xsl:choose><xsl:when test="position() != last()">line</xsl:when><xsl:otherwise>lastline</xsl:otherwise></xsl:choose></xsl:variable>
+
+          <li class='{$class}'>
+            <span class='lineconn' />
+            <span class='linebody'>
+              <xsl:apply-templates select='.'/>
+            </span>
+          </li>
+        </xsl:for-each>
+      </ul>
+    </xsl:if>
+
+  </xsl:template>
+
+
+  <xsl:template match="head|line">
+    <code>
+      <xsl:if test="@error">
+        <xsl:attribute name="class">errorLine</xsl:attribute>
+      </xsl:if>
+      <xsl:if test="@warning">
+        <xsl:attribute name="class">warningLine</xsl:attribute>
+      </xsl:if>
+      <xsl:if test="@priority = 3">
+        <xsl:attribute name="class">prio3</xsl:attribute>
+      </xsl:if>
+
+      <xsl:if test="@type = 'serial'">
+        <xsl:attribute name="class">serial</xsl:attribute>
+      </xsl:if>
+
+      <xsl:if test="@machine">
+        <xsl:choose>
+          <xsl:when test="@type = 'serial'">
+            <span class="machine"><xsl:value-of select="@machine"/># </span>
+          </xsl:when>
+          <xsl:otherwise>
+            <span class="machine"><xsl:value-of select="@machine"/>: </span>
+          </xsl:otherwise>
+        </xsl:choose>
+      </xsl:if>
+
+      <xsl:choose>
+        <xsl:when test="@image">
+          <a href="{@image}"><xsl:apply-templates/></a>
+        </xsl:when>
+        <xsl:otherwise>
+          <xsl:apply-templates/>
+        </xsl:otherwise>
+      </xsl:choose>
+    </code>
+  </xsl:template>
+
+
+  <xsl:template match="storeref">
+    <em class='storeref'>
+      <span class='popup'><xsl:apply-templates/></span>
+      <span class='elided'>/...</span><xsl:apply-templates select='name'/><xsl:apply-templates select='path'/>
+    </em>
+  </xsl:template>
+
+</xsl:stylesheet>
diff --git a/nixpkgs/nixos/lib/test-driver/logfile.css b/nixpkgs/nixos/lib/test-driver/logfile.css
new file mode 100644
index 000000000000..a54d8504a867
--- /dev/null
+++ b/nixpkgs/nixos/lib/test-driver/logfile.css
@@ -0,0 +1,129 @@
+body {
+    font-family: sans-serif;
+    background: white;
+}
+
+h1
+{
+    color: #005aa0;
+    font-size: 180%;
+}
+
+a {
+    text-decoration: none;
+}
+
+
+ul.nesting, ul.toplevel {
+    padding: 0;
+    margin: 0;
+}
+
+ul.toplevel {
+    list-style-type: none;
+}
+
+.line, .head {
+    padding-top: 0em;
+}
+
+ul.nesting li.line, ul.nesting li.lastline {
+    position: relative;
+    list-style-type: none;
+}
+
+ul.nesting li.line {
+    padding-left: 2.0em;
+}
+
+ul.nesting li.lastline {
+    padding-left: 2.1em; /* for the 0.1em border-left in .lastline > .lineconn */
+}
+
+li.line {
+    border-left: 0.1em solid #6185a0;
+}
+
+li.line > span.lineconn, li.lastline > span.lineconn {
+    position: absolute;
+    height: 0.65em;
+    left: 0em;
+    width: 1.5em;
+    border-bottom: 0.1em solid #6185a0;
+}
+
+li.lastline > span.lineconn {
+    border-left: 0.1em solid #6185a0;
+}
+
+
+em.storeref {
+    color: #500000;
+    position: relative; 
+    width: 100%;
+}
+
+em.storeref:hover {
+    background-color: #eeeeee;
+}
+
+*.popup {
+    display: none;
+/*    background: url('http://losser.st-lab.cs.uu.nl/~mbravenb/menuback.png') repeat; */
+    background: #ffffcd;
+    border: solid #555555 1px;
+    position: absolute;
+    top: 0em;
+    left: 0em;
+    margin: 0;
+    padding: 0;
+    z-index: 100;
+}
+
+em.storeref:hover span.popup {
+    display: inline;
+    width: 40em;
+}
+
+
+.logTreeToggle {
+    text-decoration: none;
+    font-family: monospace;
+    font-size: larger;
+}
+
+.errorLine {
+    color: #ff0000;
+    font-weight: bold;
+}
+
+.warningLine {
+    color: darkorange;
+    font-weight: bold;
+}
+
+.prio3 {
+    font-style: italic;
+}
+
+code {
+    white-space: pre-wrap;
+}
+
+.serial {
+    color: #56115c;
+}
+
+.machine {
+    color: #002399;
+    font-style: italic;
+}
+
+ul.vmScreenshots {
+    padding-left: 1em;
+}
+
+ul.vmScreenshots li {
+    font-family: monospace;
+    list-style: square;
+}
diff --git a/nixpkgs/nixos/lib/test-driver/test-driver.pl b/nixpkgs/nixos/lib/test-driver/test-driver.pl
new file mode 100644
index 000000000000..a3354fb0e1eb
--- /dev/null
+++ b/nixpkgs/nixos/lib/test-driver/test-driver.pl
@@ -0,0 +1,191 @@
+#! /somewhere/perl -w
+
+use strict;
+use Machine;
+use Term::ReadLine;
+use IO::File;
+use IO::Pty;
+use Logger;
+use Cwd;
+use POSIX qw(_exit dup2);
+use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
+
+$SIG{PIPE} = 'IGNORE'; # because Unix domain sockets may die unexpectedly
+
+STDERR->autoflush(1);
+
+my $log = new Logger;
+
+
+# Start vde_switch for each network required by the test.
+my %vlans;
+foreach my $vlan (split / /, $ENV{VLANS} || "") {
+    next if defined $vlans{$vlan};
+    # Start vde_switch as a child process.  We don't run it in daemon
+    # mode because we want the child process to be cleaned up when we
+    # die.  Since we have to make sure that the control socket is
+    # ready, we send a dummy command to vde_switch (via stdin) and
+    # wait for a reply.  Note that vde_switch requires stdin to be a
+    # TTY, so we create one.
+    $log->log("starting VDE switch for network $vlan");
+    my $socket = Cwd::abs_path "./vde$vlan.ctl";
+    my $pty = new IO::Pty;
+    my ($stdoutR, $stdoutW); pipe $stdoutR, $stdoutW;
+    my $pid = fork(); die "cannot fork" unless defined $pid;
+    if ($pid == 0) {
+        dup2(fileno($pty->slave), 0);
+        dup2(fileno($stdoutW), 1);
+        exec "vde_switch -s $socket --dirmode 0700" or _exit(1);
+    }
+    close $stdoutW;
+    print $pty "version\n";
+    readline $stdoutR or die "cannot start vde_switch";
+    $ENV{"QEMU_VDE_SOCKET_$vlan"} = $socket;
+    $vlans{$vlan} = $pty;
+    die unless -e "$socket/ctl";
+}
+
+
+my %vms;
+my $context = "";
+
+sub createMachine {
+    my ($args) = @_;
+    my $vm = Machine->new({%{$args}, log => $log, redirectSerial => ($ENV{USE_SERIAL} // "0") ne "1"});
+    $vms{$vm->name} = $vm;
+    $context .= "my \$" . $vm->name . " = \$vms{'" . $vm->name . "'}; ";
+    return $vm;
+}
+
+foreach my $vmScript (@ARGV) {
+    my $vm = createMachine({startCommand => $vmScript});
+}
+
+
+sub startAll {
+    $log->nest("starting all VMs", sub {
+        $_->start foreach values %vms;
+    });
+}
+
+
+# Wait until all VMs have terminated.
+sub joinAll {
+    $log->nest("waiting for all VMs to finish", sub {
+        $_->waitForShutdown foreach values %vms;
+    });
+}
+
+
+# In interactive tests, this allows the non-interactive test script to
+# be executed conveniently.
+sub testScript {
+    eval "$context $ENV{testScript};\n";
+    warn $@ if $@;
+}
+
+
+my $nrTests = 0;
+my $nrSucceeded = 0;
+
+
+sub subtest {
+    my ($name, $coderef) = @_;
+    $log->nest("subtest: $name", sub {
+        $nrTests++;
+        eval { &$coderef };
+        if ($@) {
+            $log->log("error: $@", { error => 1 });
+        } else {
+            $nrSucceeded++;
+        }
+    });
+}
+
+
+sub runTests {
+    if (defined $ENV{tests}) {
+        $log->nest("running the VM test script", sub {
+            eval "$context $ENV{tests}";
+            if ($@) {
+                $log->log("error: $@", { error => 1 });
+                die $@;
+            }
+        }, { expanded => 1 });
+    } else {
+        my $term = Term::ReadLine->new('nixos-vm-test');
+        $term->ReadHistory;
+        while (defined ($_ = $term->readline("> "))) {
+            eval "$context $_\n";
+            warn $@ if $@;
+        }
+        $term->WriteHistory;
+    }
+
+    # Copy the kernel coverage data for each machine, if the kernel
+    # has been compiled with coverage instrumentation.
+    $log->nest("collecting coverage data", sub {
+        foreach my $vm (values %vms) {
+            my $gcovDir = "/sys/kernel/debug/gcov";
+
+            next unless $vm->isUp();
+
+            my ($status, $out) = $vm->execute("test -e $gcovDir");
+            next if $status != 0;
+
+            # Figure out where to put the *.gcda files so that the
+            # report generator can find the corresponding kernel
+            # sources.
+            my $kernelDir = $vm->mustSucceed("echo \$(dirname \$(readlink -f /run/current-system/kernel))/.build/linux-*");
+            chomp $kernelDir;
+            my $coverageDir = "/tmp/xchg/coverage-data/$kernelDir";
+
+            # Copy all the *.gcda files.
+            $vm->execute("for d in $gcovDir/nix/store/*/.build/linux-*; do for i in \$(cd \$d && find -name '*.gcda'); do echo \$i; mkdir -p $coverageDir/\$(dirname \$i); cp -v \$d/\$i $coverageDir/\$i; done; done");
+        }
+    });
+
+    $log->nest("syncing", sub {
+        foreach my $vm (values %vms) {
+            next unless $vm->isUp();
+            $vm->execute("sync");
+        }
+    });
+
+    if ($nrTests != 0) {
+        $log->log("$nrSucceeded out of $nrTests tests succeeded",
+            ($nrSucceeded < $nrTests ? { error => 1 } : { }));
+    }
+}
+
+
+# Create an empty raw virtual disk with the given name and size (in
+# MiB).
+sub createDisk {
+    my ($name, $size) = @_;
+    system("qemu-img create -f raw $name ${size}M") == 0
+        or die "cannot create image of size $size";
+}
+
+
+END {
+    $log->nest("cleaning up", sub {
+        foreach my $vm (values %vms) {
+            if ($vm->{pid}) {
+                $log->log("killing " . $vm->{name} . " (pid " . $vm->{pid} . ")");
+                kill 9, $vm->{pid};
+            }
+        }
+    });
+    $log->close();
+}
+
+my $now1 = clock_gettime(CLOCK_MONOTONIC);
+
+runTests;
+
+my $now2 = clock_gettime(CLOCK_MONOTONIC);
+
+printf STDERR "test script finished in %.2fs\n", $now2 - $now1;
+
+exit ($nrSucceeded < $nrTests ? 1 : 0);
diff --git a/nixpkgs/nixos/lib/test-driver/treebits.js b/nixpkgs/nixos/lib/test-driver/treebits.js
new file mode 100644
index 000000000000..9754093dfd07
--- /dev/null
+++ b/nixpkgs/nixos/lib/test-driver/treebits.js
@@ -0,0 +1,30 @@
+$(document).ready(function() {
+
+    /* When a toggle is clicked, show or hide the subtree. */
+    $(".logTreeToggle").click(function() {
+        if ($(this).siblings("ul:hidden").length != 0) {
+            $(this).siblings("ul").show();
+            $(this).text("-");
+        } else {
+            $(this).siblings("ul").hide();
+            $(this).text("+");
+        }
+    });
+
+    /* Implementation of the expand all link. */
+    $(".logTreeExpandAll").click(function() {
+        $(".logTreeToggle", $(this).parent().siblings(".toplevel")).map(function() {
+            $(this).siblings("ul").show();
+            $(this).text("-");
+        });
+    });
+
+    /* Implementation of the collapse all link. */
+    $(".logTreeCollapseAll").click(function() {
+        $(".logTreeToggle", $(this).parent().siblings(".toplevel")).map(function() {
+            $(this).siblings("ul").hide();
+            $(this).text("+");
+        });
+    });
+
+});
diff --git a/nixpkgs/nixos/lib/testing.nix b/nixpkgs/nixos/lib/testing.nix
new file mode 100644
index 000000000000..767068771036
--- /dev/null
+++ b/nixpkgs/nixos/lib/testing.nix
@@ -0,0 +1,269 @@
+{ system
+, pkgs ? import ../.. { inherit system config; }
+  # Use a minimal kernel?
+, minimal ? false
+  # Ignored
+, config ? {}
+  # Modules to add to each VM
+, extraConfigurations ? [] }:
+
+with import ./build-vms.nix { inherit system pkgs minimal extraConfigurations; };
+with pkgs;
+
+let
+  jquery-ui = callPackage ./testing/jquery-ui.nix { };
+  jquery = callPackage ./testing/jquery.nix { };
+
+in rec {
+
+  inherit pkgs;
+
+
+  testDriver = stdenv.mkDerivation {
+    name = "nixos-test-driver";
+
+    buildInputs = [ makeWrapper perl ];
+
+    dontUnpack = true;
+
+    preferLocalBuild = true;
+
+    installPhase =
+      ''
+        mkdir -p $out/bin
+        cp ${./test-driver/test-driver.pl} $out/bin/nixos-test-driver
+        chmod u+x $out/bin/nixos-test-driver
+
+        libDir=$out/${perl.libPrefix}
+        mkdir -p $libDir
+        cp ${./test-driver/Machine.pm} $libDir/Machine.pm
+        cp ${./test-driver/Logger.pm} $libDir/Logger.pm
+
+        wrapProgram $out/bin/nixos-test-driver \
+          --prefix PATH : "${lib.makeBinPath [ qemu_test vde2 netpbm coreutils ]}" \
+          --prefix PERL5LIB : "${with perlPackages; makePerlPath [ TermReadLineGnu XMLWriter IOTty FileSlurp ]}:$out/${perl.libPrefix}"
+      '';
+  };
+
+
+  # Run an automated test suite in the given virtual network.
+  # `driver' is the script that runs the network.
+  runTests = driver:
+    stdenv.mkDerivation {
+      name = "vm-test-run-${driver.testName}";
+
+      requiredSystemFeatures = [ "kvm" "nixos-test" ];
+
+      buildInputs = [ libxslt ];
+
+      buildCommand =
+        ''
+          mkdir -p $out/nix-support
+
+          LOGFILE=$out/log.xml tests='eval $ENV{testScript}; die $@ if $@;' ${driver}/bin/nixos-test-driver
+
+          # Generate a pretty-printed log.
+          xsltproc --output $out/log.html ${./test-driver/log2html.xsl} $out/log.xml
+          ln -s ${./test-driver/logfile.css} $out/logfile.css
+          ln -s ${./test-driver/treebits.js} $out/treebits.js
+          ln -s ${jquery}/js/jquery.min.js $out/
+          ln -s ${jquery-ui}/js/jquery-ui.min.js $out/
+
+          touch $out/nix-support/hydra-build-products
+          echo "report testlog $out log.html" >> $out/nix-support/hydra-build-products
+
+          for i in */xchg/coverage-data; do
+            mkdir -p $out/coverage-data
+            mv $i $out/coverage-data/$(dirname $(dirname $i))
+          done
+        '';
+    };
+
+
+  makeTest =
+    { testScript
+    , makeCoverageReport ? false
+    , enableOCR ? false
+    , name ? "unnamed"
+    , ...
+    } @ t:
+
+    let
+      # A standard store path to the vm monitor is built like this:
+      #   /tmp/nix-build-vm-test-run-$name.drv-0/vm-state-machine/monitor
+      # The max filename length of a unix domain socket is 108 bytes.
+      # This means $name can at most be 50 bytes long.
+      maxTestNameLen = 50;
+      testNameLen = builtins.stringLength name;
+
+      testDriverName = with builtins;
+        if testNameLen > maxTestNameLen then
+          abort ("The name of the test '${name}' must not be longer than ${toString maxTestNameLen} " +
+            "it's currently ${toString testNameLen} characters long.")
+        else
+          "nixos-test-driver-${name}";
+
+      nodes = buildVirtualNetwork (
+        t.nodes or (if t ? machine then { machine = t.machine; } else { }));
+
+      testScript' =
+        # Call the test script with the computed nodes.
+        if lib.isFunction testScript
+        then testScript { inherit nodes; }
+        else testScript;
+
+      vlans = map (m: m.config.virtualisation.vlans) (lib.attrValues nodes);
+
+      vms = map (m: m.config.system.build.vm) (lib.attrValues nodes);
+
+      ocrProg = tesseract4.override { enableLanguages = [ "eng" ]; };
+
+      imagemagick_tiff = imagemagick_light.override { inherit libtiff; };
+
+      # Generate onvenience wrappers for running the test driver
+      # interactively with the specified network, and for starting the
+      # VMs from the command line.
+      driver = runCommand testDriverName
+        { buildInputs = [ makeWrapper];
+          testScript = testScript';
+          preferLocalBuild = true;
+          testName = name;
+        }
+        ''
+          mkdir -p $out/bin
+          echo "$testScript" > $out/test-script
+          ln -s ${testDriver}/bin/nixos-test-driver $out/bin/
+          vms=($(for i in ${toString vms}; do echo $i/bin/run-*-vm; done))
+          wrapProgram $out/bin/nixos-test-driver \
+            --add-flags "''${vms[*]}" \
+            ${lib.optionalString enableOCR
+              "--prefix PATH : '${ocrProg}/bin:${imagemagick_tiff}/bin'"} \
+            --run "export testScript=\"\$(cat $out/test-script)\"" \
+            --set VLANS '${toString vlans}'
+          ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-run-vms
+          wrapProgram $out/bin/nixos-run-vms \
+            --add-flags "''${vms[*]}" \
+            ${lib.optionalString enableOCR "--prefix PATH : '${ocrProg}/bin'"} \
+            --set tests 'startAll; joinAll;' \
+            --set VLANS '${toString vlans}' \
+            ${lib.optionalString (builtins.length vms == 1) "--set USE_SERIAL 1"}
+        ''; # "
+
+      passMeta = drv: drv // lib.optionalAttrs (t ? meta) {
+        meta = (drv.meta or {}) // t.meta;
+      };
+
+      test = passMeta (runTests driver);
+      report = passMeta (releaseTools.gcovReport { coverageRuns = [ test ]; });
+
+      nodeNames = builtins.attrNames nodes;
+      invalidNodeNames = lib.filter
+        (node: builtins.match "^[A-z_][A-z0-9_]+$" node == null) nodeNames;
+
+    in
+      if lib.length invalidNodeNames > 0 then
+        throw ''
+          Cannot create machines out of (${lib.concatStringsSep ", " invalidNodeNames})!
+          All machines are referenced as perl variables in the testing framework which will break the
+          script when special characters are used.
+
+          Please stick to alphanumeric chars and underscores as separation.
+        ''
+      else
+        (if makeCoverageReport then report else test) // {
+          inherit nodes driver test;
+        };
+
+  runInMachine =
+    { drv
+    , machine
+    , preBuild ? ""
+    , postBuild ? ""
+    , ... # ???
+    }:
+    let
+      vm = buildVM { }
+        [ machine
+          { key = "run-in-machine";
+            networking.hostName = "client";
+            nix.readOnlyStore = false;
+            virtualisation.writableStore = false;
+          }
+        ];
+
+      buildrunner = writeText "vm-build" ''
+        source $1
+
+        ${coreutils}/bin/mkdir -p $TMPDIR
+        cd $TMPDIR
+
+        exec $origBuilder $origArgs
+      '';
+
+      testScript = ''
+        startAll;
+        $client->waitForUnit("multi-user.target");
+        ${preBuild}
+        $client->succeed("env -i ${bash}/bin/bash ${buildrunner} /tmp/xchg/saved-env >&2");
+        ${postBuild}
+        $client->succeed("sync"); # flush all data before pulling the plug
+      '';
+
+      vmRunCommand = writeText "vm-run" ''
+        xchg=vm-state-client/xchg
+        ${coreutils}/bin/mkdir $out
+        ${coreutils}/bin/mkdir -p $xchg
+
+        for i in $passAsFile; do
+          i2=''${i}Path
+          _basename=$(${coreutils}/bin/basename ''${!i2})
+          ${coreutils}/bin/cp ''${!i2} $xchg/$_basename
+          eval $i2=/tmp/xchg/$_basename
+          ${coreutils}/bin/ls -la $xchg
+        done
+
+        unset i i2 _basename
+        export | ${gnugrep}/bin/grep -v '^xchg=' > $xchg/saved-env
+        unset xchg
+
+        export tests='${testScript}'
+        ${testDriver}/bin/nixos-test-driver ${vm.config.system.build.vm}/bin/run-*-vm
+      ''; # */
+
+    in
+      lib.overrideDerivation drv (attrs: {
+        requiredSystemFeatures = [ "kvm" ];
+        builder = "${bash}/bin/sh";
+        args = ["-e" vmRunCommand];
+        origArgs = attrs.args;
+        origBuilder = attrs.builder;
+      });
+
+
+  runInMachineWithX = { require ? [], ... } @ args:
+    let
+      client =
+        { ... }:
+        {
+          inherit require;
+          virtualisation.memorySize = 1024;
+          services.xserver.enable = true;
+          services.xserver.displayManager.slim.enable = false;
+          services.xserver.displayManager.auto.enable = true;
+          services.xserver.windowManager.default = "icewm";
+          services.xserver.windowManager.icewm.enable = true;
+          services.xserver.desktopManager.default = "none";
+        };
+    in
+      runInMachine ({
+        machine = client;
+        preBuild =
+          ''
+            $client->waitForX;
+          '';
+      } // args);
+
+
+  simpleTest = as: (makeTest as).test;
+
+}
diff --git a/nixpkgs/nixos/lib/testing/jquery-ui.nix b/nixpkgs/nixos/lib/testing/jquery-ui.nix
new file mode 100644
index 000000000000..e65107a3c2fb
--- /dev/null
+++ b/nixpkgs/nixos/lib/testing/jquery-ui.nix
@@ -0,0 +1,24 @@
+{ stdenv, fetchurl, unzip }:
+
+stdenv.mkDerivation rec {
+  name = "jquery-ui-1.11.4";
+
+  src = fetchurl {
+    url = "http://jqueryui.com/resources/download/${name}.zip";
+    sha256 = "0ciyaj1acg08g8hpzqx6whayq206fvf4whksz2pjgxlv207lqgjh";
+  };
+
+  buildInputs = [ unzip ];
+
+  installPhase =
+    ''
+      mkdir -p "$out/js"
+      cp -rv . "$out/js"
+    '';
+
+  meta = {
+    homepage = http://jqueryui.com/;
+    description = "A library of JavaScript widgets and effects";
+    platforms = stdenv.lib.platforms.all;
+  };
+}
diff --git a/nixpkgs/nixos/lib/testing/jquery.nix b/nixpkgs/nixos/lib/testing/jquery.nix
new file mode 100644
index 000000000000..e272f66a5765
--- /dev/null
+++ b/nixpkgs/nixos/lib/testing/jquery.nix
@@ -0,0 +1,36 @@
+{ stdenv, fetchurl, compressed ? true }:
+
+with stdenv.lib;
+
+stdenv.mkDerivation rec {
+  name = "jquery-1.11.3";
+
+  src = if compressed then
+    fetchurl {
+      url = "http://code.jquery.com/${name}.min.js";
+      sha256 = "1f4glgxxn3jnvry3dpzmazj3207baacnap5w20gr2xlk789idfgc";
+    }
+    else
+    fetchurl {
+      url = "http://code.jquery.com/${name}.js";
+      sha256 = "1v956yf5spw0156rni5z77hzqwmby7ajwdcd6mkhb6zvl36awr90";
+    };
+
+  dontUnpack = true;
+
+  installPhase =
+    ''
+      mkdir -p "$out/js"
+      cp -v "$src" "$out/js/jquery.js"
+      ${optionalString compressed ''
+        (cd "$out/js" && ln -s jquery.js jquery.min.js)
+      ''}
+    '';
+
+  meta = with stdenv.lib; {
+    description = "JavaScript library designed to simplify the client-side scripting of HTML";
+    homepage = http://jquery.com/;
+    license = licenses.mit;
+    platforms = platforms.all;
+  };
+}
diff --git a/nixpkgs/nixos/lib/utils.nix b/nixpkgs/nixos/lib/utils.nix
new file mode 100644
index 000000000000..b68e55a40b90
--- /dev/null
+++ b/nixpkgs/nixos/lib/utils.nix
@@ -0,0 +1,27 @@
+pkgs: with pkgs.lib;
+
+rec {
+
+  # Check whenever fileSystem is needed for boot
+  fsNeededForBoot = fs: fs.neededForBoot
+                     || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ];
+
+  # Check whenever `b` depends on `a` as a fileSystem
+  fsBefore = a: b: a.mountPoint == b.device
+                || hasPrefix "${a.mountPoint}${optionalString (!(hasSuffix "/" a.mountPoint)) "/"}" b.mountPoint;
+
+  # Escape a path according to the systemd rules, e.g. /dev/xyzzy
+  # becomes dev-xyzzy.  FIXME: slow.
+  escapeSystemdPath = s:
+   replaceChars ["/" "-" " "] ["-" "\\x2d" "\\x20"]
+    (if hasPrefix "/" s then substring 1 (stringLength s) s else s);
+
+  # Returns a system path for a given shell package
+  toShellPath = shell:
+    if types.shellPackage.check shell then
+      "/run/current-system/sw${shell.shellPath}"
+    else if types.package.check shell then
+      throw "${shell} is not a shell package"
+    else
+      shell;
+}