# Note: This is a private API, internal to NixOS. Its interface is subject
# to change without notice.
#
# The result of this builder is two disk images:
# * `boot` - a small disk formatted with FAT to be used for /boot. FAT is
# chosen to support EFI.
# * `root` - a larger disk with a zpool taking the entire disk.
# This two-disk approach is taken to satisfy ZFS's requirements for
# autoexpand.
# # Why doesn't autoexpand work with ZFS in a partition?
# When ZFS owns the whole disk doesn’t really use a partition: it has
# a marker partition at the start and a marker partition at the end of
# the disk.
# If ZFS is constrained to a partition, ZFS leaves expanding the partition
# up to the user. Obviously, the user may not choose to do so.
# Once the user expands the partition, calling zpool online -e expands the
# vdev to use the whole partition. It doesn’t happen automatically
# presumably because zed doesn’t get an event saying it’s partition grew,
# whereas it can and does get an event saying the whole disk it is on is
# now larger.
{ lib
, pkgs
, # The NixOS configuration to be installed onto the disk image.
config
, # size of the FAT boot disk, in megabytes.
bootSize ? 1024
, # The size of the root disk, in megabytes.
rootSize ? 2048
, # The name of the ZFS pool
rootPoolName ? "tank"
, # zpool properties
rootPoolProperties ? {
autoexpand = "on";
}
, # pool-wide filesystem properties
rootPoolFilesystemProperties ? {
acltype = "posixacl";
atime = "off";
compression = "on";
mountpoint = "legacy";
xattr = "sa";
, # datasets, with per-attribute options:
# mount: (optional) mount point in the VM
# properties: (optional) ZFS properties on the dataset, like filesystemProperties
# Notes:
# 1. datasets will be created from shorter to longer names as a simple topo-sort
# 2. you should define a root's dataset's mount for `/`
datasets ? { }
, # 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 ? []
, # The initial NixOS configuration file to be copied to
# /etc/nixos/configuration.nix. This configuration will be embedded
# inside a configuration which includes the described ZFS fileSystems.
configFile ? null
, # Shell code executed after the VM has finished.
postVM ? ""
, # Guest memory size
memSize ? 1024
, name ? "nixos-disk-image"
, # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
format ? "raw"
, # Include a copy of Nixpkgs in the disk image
includeChannel ? true
}:
let
formatOpt = if format == "qcow2-compressed" then "qcow2" else format;
compress = lib.optionalString (format == "qcow2-compressed") "-c";
filenameSuffix = "." + {
qcow2 = "qcow2";
vdi = "vdi";
vpc = "vhd";
raw = "img";
}.${formatOpt} or formatOpt;
bootFilename = "nixos.boot${filenameSuffix}";
rootFilename = "nixos.root${filenameSuffix}";
# FIXME: merge with channel.nix / make-channel.nix.
channelSources =
nixpkgs = lib.cleanSource pkgs.path;
in
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
'';
closureInfo = pkgs.closureInfo {
rootPaths = [ config.system.build.toplevel ]
++ (lib.optional includeChannel channelSources);
};
modulesTree = pkgs.aggregateModules
(with config.boot.kernelPackages; [ kernel zfs ]);
tools = lib.makeBinPath (
with pkgs; [
nixos-enter
config.system.build.nixos-install
dosfstools
e2fsprogs
gptfdisk
nix
parted
util-linux
zfs
]
);
hasDefinedMount = disk: ((disk.mount or null) != null);
stringifyProperties = prefix: properties: lib.concatStringsSep " \\\n" (
lib.mapAttrsToList
(
property: value: "${prefix} ${lib.escapeShellArg property}=${lib.escapeShellArg value}"
)
properties
createDatasets =
datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
sorted = lib.sort (left: right: (lib.stringLength left.name) < (lib.stringLength right.name)) datasetlist;
cmd = { name, value }:
properties = stringifyProperties "-o" (value.properties or {});
"zfs create -p ${properties} ${name}";
lib.concatMapStringsSep "\n" cmd sorted;
mountDatasets =
mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist;
sorted = lib.sort (left: right: (lib.stringLength left.value.mount) < (lib.stringLength right.value.mount)) mounts;
''
mkdir -p /mnt${lib.escapeShellArg value.mount}
mount -t zfs ${name} /mnt${lib.escapeShellArg value.mount}
unmountDatasets =
sorted = lib.sort (left: right: (lib.stringLength left.value.mount) > (lib.stringLength right.value.mount)) mounts;
umount /mnt${lib.escapeShellArg value.mount}
fileSystemsCfgFile =
mountable = lib.filterAttrs (_: value: hasDefinedMount value) datasets;
pkgs.runCommand "filesystem-config.nix" {
buildInputs = with pkgs; [ jq nixpkgs-fmt ];
filesystems = builtins.toJSON {
fileSystems = lib.mapAttrs'
dataset: attrs:
{
name = attrs.mount;
value = {
fsType = "zfs";
device = "${dataset}";
mountable;
passAsFile = [ "filesystems" ];
} ''
echo "builtins.fromJSON '''"
jq . < "$filesystemsPath"
echo "'''"
) > $out
nixpkgs-fmt $out
mergedConfig =
if configFile == null
then fileSystemsCfgFile
else
pkgs.runCommand "configuration.nix" {
buildInputs = with pkgs; [ nixpkgs-fmt ];
echo '{ imports = ['
printf "(%s)\n" "$(cat ${fileSystemsCfgFile})";
printf "(%s)\n" "$(cat ${configFile})";
echo ']; }'
image = (
pkgs.vmTools.override {
rootModules =
[ "zfs" "9p" "9pnet_virtio" "virtio_pci" "virtio_blk" ];
kernel = modulesTree;
).runInLinuxVM (
pkgs.runCommand name
QEMU_OPTS = "-drive file=$bootDiskImage,if=virtio,cache=unsafe,werror=report"
+ " -drive file=$rootDiskImage,if=virtio,cache=unsafe,werror=report";
inherit memSize;
preVM = ''
PATH=$PATH:${pkgs.qemu_kvm}/bin
mkdir $out
bootDiskImage=boot.raw
qemu-img create -f raw $bootDiskImage ${toString bootSize}M
rootDiskImage=root.raw
qemu-img create -f raw $rootDiskImage ${toString rootSize}M
postVM = ''
${if formatOpt == "raw" then ''
mv $bootDiskImage $out/${bootFilename}
mv $rootDiskImage $out/${rootFilename}
'' else ''
${pkgs.qemu_kvm}/bin/qemu-img convert -f raw -O ${formatOpt} ${compress} $bootDiskImage $out/${bootFilename}
${pkgs.qemu_kvm}/bin/qemu-img convert -f raw -O ${formatOpt} ${compress} $rootDiskImage $out/${rootFilename}
''}
bootDiskImage=$out/${bootFilename}
rootDiskImage=$out/${rootFilename}
set -x
${postVM}
export PATH=${tools}:$PATH
cp -sv /dev/vda /dev/sda
cp -sv /dev/vda /dev/xvda
parted --script /dev/vda -- \
mklabel gpt \
mkpart no-fs 1MiB 2MiB \
set 1 bios_grub on \
align-check optimal 1 \
mkpart ESP fat32 2MiB -1MiB \
align-check optimal 2 \
print
sfdisk --dump /dev/vda
zpool create \
${stringifyProperties " -o" rootPoolProperties} \
${stringifyProperties " -O" rootPoolFilesystemProperties} \
${rootPoolName} /dev/vdb
parted --script /dev/vdb -- print
${createDatasets}
${mountDatasets}
mkdir -p /mnt/boot
mkfs.vfat -n ESP /dev/vda2
mount /dev/vda2 /mnt/boot
mount
# Install a configuration.nix
mkdir -p /mnt/etc/nixos
# `cat` so it is mutable on the fs
cat ${mergedConfig} > /mnt/etc/nixos/configuration.nix
export NIX_STATE_DIR=$TMPDIR/state
nix-store --load-db < ${closureInfo}/registration
nixos-install \
--root /mnt \
--no-root-passwd \
--system ${config.system.build.toplevel} \
--substituters "" \
${lib.optionalString includeChannel ''--channel ${channelSources}''}
df -h
umount /mnt/boot
${unmountDatasets}
zpool export ${rootPoolName}
image