280 lines
9.2 KiB
Nix
280 lines
9.2 KiB
Nix
|
{ pkgs
|
|||
|
, lib
|
|||
|
, name ? "nixos-disk-image"
|
|||
|
, # The NixOS configuration to be installed onto the disk image.
|
|||
|
config
|
|||
|
, # The size of the disk, in megabytes. If "auto", the size is calculated based
|
|||
|
# on the contents copied to it and `extraSize` is taken into account.
|
|||
|
diskSize ? "auto"
|
|||
|
, # Extra disk space, in megabytes. Added to the image if diskSize "auto" is
|
|||
|
# used.
|
|||
|
extraSize ? 512
|
|||
|
, # Swap space, in megabytes. Addeded to the image unless set to null.
|
|||
|
swapSize ? 512
|
|||
|
, # The unique identifier for the swap partition.
|
|||
|
swapUUID ? "44444444-4444-4444-8888-888888888887"
|
|||
|
, # The label given to the swap partition.
|
|||
|
swapLabel ? "swap"
|
|||
|
, # Whether to invoke `switch-to-configuration boot` during image creation.
|
|||
|
installBootLoader ? true
|
|||
|
, # The filesystem label.
|
|||
|
label ? "nixos"
|
|||
|
, # The initial NixOS configuration file set at `/etc/nixos/configuration.nix`.
|
|||
|
configFile ? null
|
|||
|
, # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
|
|||
|
format ? "raw"
|
|||
|
, # The root Filesystem Unique Identifier.
|
|||
|
rootFSUID ? "F222513B-DED1-49FA-B591-20CE86A2FE7F"
|
|||
|
, # Whether a nix channel based on the current source tree should be
|
|||
|
# made available inside the image. Useful for interactive use of nix
|
|||
|
# utils, but changes the hash of the image when the sources are
|
|||
|
# updated.
|
|||
|
copyChannel ? true
|
|||
|
, # Shell code executed after the VM has finished.
|
|||
|
postVM ? ""
|
|||
|
, # Guest memory size
|
|||
|
memSize ? 1024
|
|||
|
}:
|
|||
|
let
|
|||
|
format' = if format == "qcow2-compressed" then "qcow2" else format;
|
|||
|
|
|||
|
compress = lib.optionalString (format == "qcow2-compressed") "-c";
|
|||
|
|
|||
|
filename = "nixos." + {
|
|||
|
qcow2 = "qcow2";
|
|||
|
vdi = "vdi";
|
|||
|
vpc = "vhd";
|
|||
|
raw = "img";
|
|||
|
}.${format'} or format';
|
|||
|
|
|||
|
swapPartition = "1";
|
|||
|
rootPartition = "2";
|
|||
|
swapEnd = toString (1 + swapSize);
|
|||
|
|
|||
|
nixpkgs = lib.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 = lib.makeBinPath (with pkgs; [
|
|||
|
rsync
|
|||
|
util-linux
|
|||
|
parted
|
|||
|
e2fsprogs
|
|||
|
lkl
|
|||
|
config.system.build.nixos-install
|
|||
|
config.system.build.nixos-enter
|
|||
|
nix
|
|||
|
systemdMinimal
|
|||
|
gptfdisk
|
|||
|
] ++ stdenv.initialPath);
|
|||
|
|
|||
|
closureInfo = pkgs.closureInfo {
|
|||
|
rootPaths = [ config.system.build.toplevel ] ++
|
|||
|
lib.optional copyChannel channelSources;
|
|||
|
};
|
|||
|
|
|||
|
# ext4fs block size (not block device sector size)
|
|||
|
blockSize = toString (4 * 1024);
|
|||
|
|
|||
|
prepareImage = ''
|
|||
|
export PATH=${binPath}
|
|||
|
|
|||
|
# Yes, mkfs.ext4 takes different units in different contexts. Fun.
|
|||
|
sectorsToKilobytes() {
|
|||
|
echo $(( ( "$1" * 512 ) / 1024 ))
|
|||
|
}
|
|||
|
|
|||
|
sectorsToBytes() {
|
|||
|
echo $(( "$1" * 512 ))
|
|||
|
}
|
|||
|
|
|||
|
# Given lines of numbers, adds them together
|
|||
|
sum_lines() {
|
|||
|
local acc=0
|
|||
|
while read -r number; do
|
|||
|
acc=$((acc+number))
|
|||
|
done
|
|||
|
echo "$acc"
|
|||
|
}
|
|||
|
|
|||
|
mebibyte=$(( 1024 * 1024 ))
|
|||
|
|
|||
|
# Approximative percentage of reserved space in an ext4 fs over 512MiB.
|
|||
|
# 0.05208587646484375 × 1000, integer part: 52
|
|||
|
compute_fudge() {
|
|||
|
echo $(( $1 * 52 / 1000 ))
|
|||
|
}
|
|||
|
|
|||
|
mkdir $out
|
|||
|
|
|||
|
root="$PWD/root"
|
|||
|
mkdir -p $root
|
|||
|
|
|||
|
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
|
|||
|
|
|||
|
chmod 755 "$TMPDIR"
|
|||
|
echo "running nixos-install..."
|
|||
|
nixos-install --root $root --no-bootloader --no-root-passwd \
|
|||
|
--system ${config.system.build.toplevel} \
|
|||
|
${if copyChannel then "--channel ${channelSources}" else "--no-channel-copy"} \
|
|||
|
--substituters ""
|
|||
|
|
|||
|
diskImage=nixos.raw
|
|||
|
|
|||
|
${if diskSize == "auto" then ''
|
|||
|
# Add the 1MiB aligned reserved space (includes MBR)
|
|||
|
reservedSpace=$(( mebibyte ))
|
|||
|
|
|||
|
swapSpace=$((
|
|||
|
$(numfmt --from=iec '${toString swapSize}M') + reservedSpace
|
|||
|
))
|
|||
|
|
|||
|
extraSpace=$((
|
|||
|
$(numfmt --from=iec '${toString extraSize}M') + reservedSpace
|
|||
|
))
|
|||
|
|
|||
|
# Compute required space in filesystem blocks
|
|||
|
diskUsage=$(
|
|||
|
find . ! -type d -print0 |
|
|||
|
du --files0-from=- --apparent-size --block-size "${blockSize}" |
|
|||
|
cut -f1 |
|
|||
|
sum_lines
|
|||
|
)
|
|||
|
# Each inode takes space!
|
|||
|
numInodes=$(find . | wc -l)
|
|||
|
# Convert to bytes, inodes take two blocks each!
|
|||
|
diskUsage=$(( (diskUsage + 2 * numInodes) * ${blockSize} ))
|
|||
|
# Then increase the required space to account for the reserved blocks.
|
|||
|
fudge=$(compute_fudge $diskUsage)
|
|||
|
requiredFilesystemSpace=$(( diskUsage + fudge ))
|
|||
|
|
|||
|
diskSize=$(( requiredFilesystemSpace + swapSpace + extraSpace ))
|
|||
|
|
|||
|
# Round up to the nearest mebibyte. This ensures whole 512 bytes sector
|
|||
|
# sizes in the disk image and helps towards aligning partitions optimally.
|
|||
|
if (( diskSize % mebibyte )); then
|
|||
|
diskSize=$(( ( diskSize / mebibyte + 1) * mebibyte ))
|
|||
|
fi
|
|||
|
|
|||
|
truncate -s "$diskSize" $diskImage
|
|||
|
|
|||
|
printf "Automatic disk size...\n"
|
|||
|
printf " Closure space use: %d bytes\n" $diskUsage
|
|||
|
printf " fudge: %d bytes\n" $fudge
|
|||
|
printf " Filesystem size needed: %d bytes\n" $requiredFilesystemSpace
|
|||
|
printf " Swap space: %d bytes\n" $swapSpace
|
|||
|
printf " Extra space: %d bytes\n" $extraSpace
|
|||
|
printf " Disk image size: %d bytes\n" $diskSize
|
|||
|
'' else ''
|
|||
|
truncate -s ${toString diskSize}M $diskImage
|
|||
|
''}
|
|||
|
|
|||
|
parted --script $diskImage -- mklabel msdos
|
|||
|
parted --script $diskImage -- \
|
|||
|
mkpart primary linux-swap 1MiB ${swapEnd}MiB \
|
|||
|
mkpart primary ext4 ${swapEnd}MiB -1
|
|||
|
|
|||
|
# Get start & length of the root partition in sectors to $START and
|
|||
|
# $SECTORS.
|
|||
|
eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs)
|
|||
|
|
|||
|
mkfs.ext4 -b ${blockSize} -F -L ${label} $diskImage -E \
|
|||
|
offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
|
|||
|
|
|||
|
echo "copying staging root to image..."
|
|||
|
cptofs -p -P ${rootPartition} -t ext4 -i $diskImage $root/* / ||
|
|||
|
(echo >&2 "ERROR: cptofs failed. diskSize might be too small for closure."; exit 1)
|
|||
|
'';
|
|||
|
|
|||
|
moveOrConvertImage = ''
|
|||
|
${if format' == "raw" then "mv $diskImage $out/${filename}" else ''
|
|||
|
${pkgs.qemu-utils}/bin/qemu-img convert -f raw -O ${format'} ${compress} \
|
|||
|
$diskImage $out/${filename}
|
|||
|
''}
|
|||
|
diskImage=$out/${filename}
|
|||
|
'';
|
|||
|
|
|||
|
buildImage = pkgs.vmTools.runInLinuxVM (
|
|||
|
pkgs.runCommand name {
|
|||
|
preVM = prepareImage;
|
|||
|
buildInputs = with pkgs; [ util-linux e2fsprogs dosfstools ];
|
|||
|
postVM = moveOrConvertImage + postVM;
|
|||
|
inherit memSize;
|
|||
|
} ''
|
|||
|
export PATH=${binPath}:$PATH
|
|||
|
|
|||
|
rootDisk="/dev/vda${rootPartition}"
|
|||
|
|
|||
|
# It is necessary to set root filesystem unique identifier in advance,
|
|||
|
# otherwise the bootloader might get the wrong one and fail to boot. At
|
|||
|
# the end, we reset again because we want deterministic timestamps.
|
|||
|
tune2fs -T now -U ${rootFSUID} -c 0 -i 0 $rootDisk
|
|||
|
# Make systemd-boot find ESP without udev.
|
|||
|
mkdir /dev/block
|
|||
|
ln -s /dev/vda1 /dev/block/254:1
|
|||
|
|
|||
|
mountPoint=/mnt
|
|||
|
mkdir $mountPoint
|
|||
|
mount $rootDisk $mountPoint
|
|||
|
|
|||
|
# Create the swapspace without turning it on.
|
|||
|
mkswap -U ${swapUUID} -L ${swapLabel} /dev/vda${swapPartition}
|
|||
|
swapon /dev/vda${swapPartition}
|
|||
|
|
|||
|
# Install a configuration.nix
|
|||
|
mkdir -p /mnt/etc/nixos
|
|||
|
${lib.optionalString (configFile != null) ''
|
|||
|
cp ${configFile} /mnt/etc/nixos/configuration.nix
|
|||
|
''}
|
|||
|
|
|||
|
${lib.optionalString installBootLoader ''
|
|||
|
# In this throwaway resource, we only have `/dev/vda`, but the actual VM
|
|||
|
# may refer to another disk for bootloader, e.g. `/dev/vdb`. Use this
|
|||
|
# option to create a symlink from vda to any arbitrary device you want.
|
|||
|
${lib.optionalString (
|
|||
|
config.boot.loader.grub.enable &&
|
|||
|
config.boot.loader.grub.device != "/dev/vda"
|
|||
|
) ''
|
|||
|
mkdir -p $(dirname ${config.boot.loader.grub.device})
|
|||
|
ln -s /dev/vda ${config.boot.loader.grub.device}
|
|||
|
''}
|
|||
|
|
|||
|
# Set up core system link, bootloader (sd-boot, GRUB, uboot, etc.), 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. This is
|
|||
|
# fixed to 1970-01-01 (UNIX timestamp 0). This two-step approach is
|
|||
|
# necessary otherwise `tune2fs` will want a fresher filesystem to perform
|
|||
|
# some changes.
|
|||
|
tune2fs -T now -U ${rootFSUID} -c 0 -i 0 $rootDisk
|
|||
|
tune2fs -f -T 19700101 $rootDisk
|
|||
|
''
|
|||
|
);
|
|||
|
in
|
|||
|
buildImage
|