nixpkgs/nixos/modules/installer/cd-dvd/sd-image.nix
2019-06-16 17:47:31 -04:00

182 lines
6.4 KiB
Nix

# This module creates a bootable SD card image containing the given NixOS
# configuration. The generated image is MBR partitioned, with a FAT /boot
# partition, and ext4 root partition. The generated image is sized to fit
# its contents, and a boot script automatically resizes the root partition
# to fit the device on the first boot.
#
# The derivation for the SD image will be placed in
# config.system.build.sdImage
{ config, lib, pkgs, ... }:
with lib;
let
rootfsImage = pkgs.callPackage ../../../lib/make-ext4-fs.nix ({
inherit (config.sdImage) storePaths;
populateImageCommands = config.sdImage.populateRootCommands;
volumeLabel = "NIXOS_SD";
} // optionalAttrs (config.sdImage.rootPartitionUUID != null) {
uuid = config.sdImage.rootPartitionUUID;
});
in
{
options.sdImage = {
imageName = mkOption {
default = "${config.sdImage.imageBaseName}-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.img";
description = ''
Name of the generated image file.
'';
};
imageBaseName = mkOption {
default = "nixos-sd-image";
description = ''
Prefix of the name of the generated image file.
'';
};
storePaths = mkOption {
type = with types; listOf package;
example = literalExample "[ pkgs.stdenv ]";
description = ''
Derivations to be included in the Nix store in the generated SD image.
'';
};
firmwarePartitionID = mkOption {
type = types.string;
default = "0x2178694e";
description = ''
Volume ID for the /boot/firmware partition on the SD card. This value
must be a 32-bit hexadecimal number.
'';
};
rootPartitionUUID = mkOption {
type = types.nullOr types.string;
default = null;
example = "14e19a7b-0ae0-484d-9d54-43bd6fdc20c7";
description = ''
UUID for the main NixOS partition on the SD card.
'';
};
firmwareSize = mkOption {
type = types.int;
# As of 2019-05-31 the Raspberry pi firmware + u-bot takes ~13MiB
default = 20;
description = ''
Size of the /boot/firmware partition, in megabytes.
'';
};
populateFirmwareCommands = mkOption {
example = literalExample "'' cp \${pkgs.myBootLoader}/u-boot.bin firmware/ ''";
description = ''
Shell commands to populate the ./firmware directory.
All files in that directory are copied to the
/boot/firmware partition on the SD image.
'';
};
populateRootCommands = mkOption {
example = literalExample "''\${extlinux-conf-builder} -t 3 -c \${config.system.build.toplevel} -d ./files/boot''";
description = ''
Shell commands to populate the ./files directory.
All files in that directory are copied to the
root (/) partition on the SD image. Use this to
populate the ./files/boot (/boot) directory.
'';
};
};
config = {
fileSystems = {
"/boot/firmware" = {
device = "/dev/disk/by-label/FIRMWARE";
fsType = "vfat";
options = [ "nofail" "noauto" ];
};
"/" = {
device = "/dev/disk/by-label/NIXOS_SD";
fsType = "ext4";
};
};
sdImage.storePaths = [ config.system.build.toplevel ];
system.build.sdImage = pkgs.callPackage ({ stdenv, dosfstools, e2fsprogs, mtools, libfaketime, utillinux }: stdenv.mkDerivation {
name = config.sdImage.imageName;
nativeBuildInputs = [ dosfstools e2fsprogs mtools libfaketime utillinux ];
buildCommand = ''
mkdir -p $out/nix-support $out/sd-image
export img=$out/sd-image/${config.sdImage.imageName}
echo "${pkgs.stdenv.buildPlatform.system}" > $out/nix-support/system
echo "file sd-image $img" >> $out/nix-support/hydra-build-products
# Create the image file sized to fit /boot/firmware and /, plus 20M of slack
rootSizeBlocks=$(du -B 512 --apparent-size ${rootfsImage} | awk '{ print $1 }')
firmwareSizeBlocks=$((${toString config.sdImage.firmwareSize} * 1024 * 1024 / 512))
imageSize=$((rootSizeBlocks * 512 + firmwareSizeBlocks * 512 + 20 * 1024 * 1024))
truncate -s $imageSize $img
# type=b is 'W95 FAT32', type=83 is 'Linux'.
sfdisk $img <<EOF
label: dos
label-id: ${config.sdImage.firmwarePartitionID}
start=8M, size=$firmwareSizeBlocks, type=b
start=${toString (8 + config.sdImage.firmwareSize)}M, type=83, bootable
EOF
# Copy the rootfs into the SD image
eval $(partx $img -o START,SECTORS --nr 2 --pairs)
dd conv=notrunc if=${rootfsImage} of=$img seek=$START count=$SECTORS
# Create a FAT32 /boot/firmware partition of suitable size into firmware_part.img
eval $(partx $img -o START,SECTORS --nr 1 --pairs)
truncate -s $((SECTORS * 512)) firmware_part.img
faketime "1970-01-01 00:00:00" mkfs.vfat -i ${config.sdImage.firmwarePartitionID} -n FIRMWARE firmware_part.img
# Populate the files intended for /boot/firmware
mkdir firmware
${config.sdImage.populateFirmwareCommands}
# Copy the populated /boot/firmware into the SD image
(cd firmware; mcopy -psvm -i ../firmware_part.img ./* ::)
# Verify the FAT partition before copying it.
fsck.vfat -vn firmware_part.img
dd conv=notrunc if=firmware_part.img of=$img seek=$START count=$SECTORS
'';
}) {};
boot.postBootCommands = ''
# On the first boot do some maintenance tasks
if [ -f /nix-path-registration ]; then
# Figure out device names for the boot device and root filesystem.
rootPart=$(readlink -f /dev/disk/by-label/NIXOS_SD)
bootDevice=$(lsblk -npo PKNAME $rootPart)
# Resize the root partition and the filesystem to fit the disk
echo ",+," | sfdisk -N2 --no-reread $bootDevice
${pkgs.parted}/bin/partprobe
${pkgs.e2fsprogs}/bin/resize2fs $rootPart
# Register the contents of the initial Nix store
${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration
# nixos-rebuild also requires a "system" profile and an /etc/NIXOS tag.
touch /etc/NIXOS
${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
# Prevents this from running on later boots.
rm -f /nix-path-registration
fi
'';
};
}