at 21.11-pre 14 kB view raw
1{ pkgs 2, lib 3 4, # The NixOS configuration to be installed onto the disk image. 5 config 6 7, # The size of the disk, in megabytes. 8 # if "auto" size is calculated based on the contents copied to it and 9 # additionalSpace is taken into account. 10 diskSize ? "auto" 11 12, # additional disk space to be added to the image if diskSize "auto" 13 # is used 14 additionalSpace ? "512M" 15 16, # size of the boot partition, is only used if partitionTableType is 17 # either "efi" or "hybrid" 18 # This will be undersized slightly, as this is actually the offset of 19 # the end of the partition. Generally it will be 1MiB smaller. 20 bootSize ? "256M" 21 22, # The files and directories to be placed in the target file system. 23 # This is a list of attribute sets {source, target, mode, user, group} where 24 # `source' is the file system object (regular file or directory) to be 25 # grafted in the file system at path `target', `mode' is a string containing 26 # the permissions that will be set (ex. "755"), `user' and `group' are the 27 # user and group name that will be set as owner of the files. 28 # `mode', `user', and `group' are optional. 29 # When setting one of `user' or `group', the other needs to be set too. 30 contents ? [] 31 32, # Type of partition table to use; either "legacy", "efi", or "none". 33 # For "efi" images, the GPT partition table is used and a mandatory ESP 34 # partition of reasonable size is created in addition to the root partition. 35 # For "legacy", the msdos partition table is used and a single large root 36 # partition is created. 37 # For "legacy+gpt", the GPT partition table is used, a 1MiB no-fs partition for 38 # use by the bootloader is created, and a single large root partition is 39 # created. 40 # For "hybrid", the GPT partition table is used and a mandatory ESP 41 # partition of reasonable size is created in addition to the root partition. 42 # Also a legacy MBR will be present. 43 # For "none", no partition table is created. Enabling `installBootLoader` 44 # most likely fails as GRUB will probably refuse to install. 45 partitionTableType ? "legacy" 46 47, # The root file system type. 48 fsType ? "ext4" 49 50, # Filesystem label 51 label ? "nixos" 52 53, # The initial NixOS configuration file to be copied to 54 # /etc/nixos/configuration.nix. 55 configFile ? null 56 57, # Shell code executed after the VM has finished. 58 postVM ? "" 59 60, name ? "nixos-disk-image" 61 62, # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw. 63 format ? "raw" 64}: 65 66assert partitionTableType == "legacy" || partitionTableType == "legacy+gpt" || partitionTableType == "efi" || partitionTableType == "hybrid" || partitionTableType == "none"; 67# We use -E offset=X below, which is only supported by e2fsprogs 68assert partitionTableType != "none" -> fsType == "ext4"; 69# Either both or none of {user,group} need to be set 70assert lib.all 71 (attrs: ((attrs.user or null) == null) 72 == ((attrs.group or null) == null)) 73 contents; 74 75with lib; 76 77let format' = format; in let 78 79 format = if format' == "qcow2-compressed" then "qcow2" else format'; 80 81 compress = optionalString (format' == "qcow2-compressed") "-c"; 82 83 filename = "nixos." + { 84 qcow2 = "qcow2"; 85 vdi = "vdi"; 86 vpc = "vhd"; 87 raw = "img"; 88 }.${format} or format; 89 90 rootPartition = { # switch-case 91 legacy = "1"; 92 "legacy+gpt" = "2"; 93 efi = "2"; 94 hybrid = "3"; 95 }.${partitionTableType}; 96 97 partitionDiskScript = { # switch-case 98 legacy = '' 99 parted --script $diskImage -- \ 100 mklabel msdos \ 101 mkpart primary ext4 1MiB -1 102 ''; 103 "legacy+gpt" = '' 104 parted --script $diskImage -- \ 105 mklabel gpt \ 106 mkpart no-fs 1MB 2MB \ 107 set 1 bios_grub on \ 108 align-check optimal 1 \ 109 mkpart primary ext4 2MB -1 \ 110 align-check optimal 2 \ 111 print 112 ''; 113 efi = '' 114 parted --script $diskImage -- \ 115 mklabel gpt \ 116 mkpart ESP fat32 8MiB ${bootSize} \ 117 set 1 boot on \ 118 mkpart primary ext4 ${bootSize} -1 119 ''; 120 hybrid = '' 121 parted --script $diskImage -- \ 122 mklabel gpt \ 123 mkpart ESP fat32 8MiB ${bootSize} \ 124 set 1 boot on \ 125 mkpart no-fs 0 1024KiB \ 126 set 2 bios_grub on \ 127 mkpart primary ext4 ${bootSize} -1 128 ''; 129 none = ""; 130 }.${partitionTableType}; 131 132 nixpkgs = cleanSource pkgs.path; 133 134 # FIXME: merge with channel.nix / make-channel.nix. 135 channelSources = pkgs.runCommand "nixos-${config.system.nixos.version}" {} '' 136 mkdir -p $out 137 cp -prd ${nixpkgs.outPath} $out/nixos 138 chmod -R u+w $out/nixos 139 if [ ! -e $out/nixos/nixpkgs ]; then 140 ln -s . $out/nixos/nixpkgs 141 fi 142 rm -rf $out/nixos/.git 143 echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix 144 ''; 145 146 binPath = with pkgs; makeBinPath ( 147 [ rsync 148 util-linux 149 parted 150 e2fsprogs 151 lkl 152 config.system.build.nixos-install 153 config.system.build.nixos-enter 154 nix 155 ] ++ stdenv.initialPath); 156 157 # I'm preserving the line below because I'm going to search for it across nixpkgs to consolidate 158 # image building logic. The comment right below this now appears in 4 different places in nixpkgs :) 159 # !!! should use XML. 160 sources = map (x: x.source) contents; 161 targets = map (x: x.target) contents; 162 modes = map (x: x.mode or "''") contents; 163 users = map (x: x.user or "''") contents; 164 groups = map (x: x.group or "''") contents; 165 166 closureInfo = pkgs.closureInfo { rootPaths = [ config.system.build.toplevel channelSources ]; }; 167 168 blockSize = toString (4 * 1024); # ext4fs block size (not block device sector size) 169 170 prepareImage = '' 171 export PATH=${binPath} 172 173 # Yes, mkfs.ext4 takes different units in different contexts. Fun. 174 sectorsToKilobytes() { 175 echo $(( ( "$1" * 512 ) / 1024 )) 176 } 177 178 sectorsToBytes() { 179 echo $(( "$1" * 512 )) 180 } 181 182 # Given lines of numbers, adds them together 183 sum_lines() { 184 local acc=0 185 while read -r number; do 186 acc=$((acc+number)) 187 done 188 echo "$acc" 189 } 190 191 mebibyte=$(( 1024 * 1024 )) 192 193 # Approximative percentage of reserved space in an ext4 fs over 512MiB. 194 # 0.05208587646484375 195 # × 1000, integer part: 52 196 compute_fudge() { 197 echo $(( $1 * 52 / 1000 )) 198 } 199 200 mkdir $out 201 202 root="$PWD/root" 203 mkdir -p $root 204 205 # Copy arbitrary other files into the image 206 # Semi-shamelessly copied from make-etc.sh. I (@copumpkin) shall factor this stuff out as part of 207 # https://github.com/NixOS/nixpkgs/issues/23052. 208 set -f 209 sources_=(${concatStringsSep " " sources}) 210 targets_=(${concatStringsSep " " targets}) 211 modes_=(${concatStringsSep " " modes}) 212 set +f 213 214 for ((i = 0; i < ''${#targets_[@]}; i++)); do 215 source="''${sources_[$i]}" 216 target="''${targets_[$i]}" 217 mode="''${modes_[$i]}" 218 219 if [ -n "$mode" ]; then 220 rsync_chmod_flags="--chmod=$mode" 221 else 222 rsync_chmod_flags="" 223 fi 224 # Unfortunately cptofs only supports modes, not ownership, so we can't use 225 # rsync's --chown option. Instead, we change the ownerships in the 226 # VM script with chown. 227 rsync_flags="-a --no-o --no-g $rsync_chmod_flags" 228 if [[ "$source" =~ '*' ]]; then 229 # If the source name contains '*', perform globbing. 230 mkdir -p $root/$target 231 for fn in $source; do 232 rsync $rsync_flags "$fn" $root/$target/ 233 done 234 else 235 mkdir -p $root/$(dirname $target) 236 if ! [ -e $root/$target ]; then 237 rsync $rsync_flags $source $root/$target 238 else 239 echo "duplicate entry $target -> $source" 240 exit 1 241 fi 242 fi 243 done 244 245 export HOME=$TMPDIR 246 247 # Provide a Nix database so that nixos-install can copy closures. 248 export NIX_STATE_DIR=$TMPDIR/state 249 nix-store --load-db < ${closureInfo}/registration 250 251 chmod 755 "$TMPDIR" 252 echo "running nixos-install..." 253 nixos-install --root $root --no-bootloader --no-root-passwd \ 254 --system ${config.system.build.toplevel} --channel ${channelSources} --substituters "" 255 256 diskImage=nixos.raw 257 258 ${if diskSize == "auto" then '' 259 ${if partitionTableType == "efi" || partitionTableType == "hybrid" then '' 260 # Add the GPT at the end 261 gptSpace=$(( 512 * 34 * 1 )) 262 # Normally we'd need to account for alignment and things, if bootSize 263 # represented the actual size of the boot partition. But it instead 264 # represents the offset at which it ends. 265 # So we know bootSize is the reserved space in front of the partition. 266 reservedSpace=$(( gptSpace + $(numfmt --from=iec '${bootSize}') )) 267 '' else if partitionTableType == "legacy+gpt" then '' 268 # Add the GPT at the end 269 gptSpace=$(( 512 * 34 * 1 )) 270 # And include the bios_grub partition; the ext4 partition starts at 2MB exactly. 271 reservedSpace=$(( gptSpace + 2 * mebibyte )) 272 '' else if partitionTableType == "legacy" then '' 273 # Add the 1MiB aligned reserved space (includes MBR) 274 reservedSpace=$(( mebibyte )) 275 '' else '' 276 reservedSpace=0 277 ''} 278 additionalSpace=$(( $(numfmt --from=iec '${additionalSpace}') + reservedSpace )) 279 280 # Compute required space in filesystem blocks 281 diskUsage=$(find . ! -type d -exec 'du' '--apparent-size' '--block-size' "${blockSize}" '{}' ';' | cut -f1 | sum_lines) 282 # Each inode takes space! 283 numInodes=$(find . | wc -l) 284 # Convert to bytes, inodes take two blocks each! 285 diskUsage=$(( (diskUsage + 2 * numInodes) * ${blockSize} )) 286 # Then increase the required space to account for the reserved blocks. 287 fudge=$(compute_fudge $diskUsage) 288 requiredFilesystemSpace=$(( diskUsage + fudge )) 289 290 diskSize=$(( requiredFilesystemSpace + additionalSpace )) 291 292 # Round up to the nearest mebibyte. 293 # This ensures whole 512 bytes sector sizes in the disk image 294 # and helps towards aligning partitions optimally. 295 if (( diskSize % mebibyte )); then 296 diskSize=$(( ( diskSize / mebibyte + 1) * mebibyte )) 297 fi 298 299 truncate -s "$diskSize" $diskImage 300 301 printf "Automatic disk size...\n" 302 printf " Closure space use: %d bytes\n" $diskUsage 303 printf " fudge: %d bytes\n" $fudge 304 printf " Filesystem size needed: %d bytes\n" $requiredFilesystemSpace 305 printf " Additional space: %d bytes\n" $additionalSpace 306 printf " Disk image size: %d bytes\n" $diskSize 307 '' else '' 308 truncate -s ${toString diskSize}M $diskImage 309 ''} 310 311 ${partitionDiskScript} 312 313 ${if partitionTableType != "none" then '' 314 # Get start & length of the root partition in sectors to $START and $SECTORS. 315 eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs) 316 317 mkfs.${fsType} -b ${blockSize} -F -L ${label} $diskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K 318 '' else '' 319 mkfs.${fsType} -b ${blockSize} -F -L ${label} $diskImage 320 ''} 321 322 echo "copying staging root to image..." 323 cptofs -p ${optionalString (partitionTableType != "none") "-P ${rootPartition}"} -t ${fsType} -i $diskImage $root/* / || 324 (echo >&2 "ERROR: cptofs failed. diskSize might be too small for closure."; exit 1) 325 ''; 326in pkgs.vmTools.runInLinuxVM ( 327 pkgs.runCommand name 328 { preVM = prepareImage; 329 buildInputs = with pkgs; [ util-linux e2fsprogs dosfstools ]; 330 postVM = '' 331 ${if format == "raw" then '' 332 mv $diskImage $out/${filename} 333 '' else '' 334 ${pkgs.qemu}/bin/qemu-img convert -f raw -O ${format} ${compress} $diskImage $out/${filename} 335 ''} 336 diskImage=$out/${filename} 337 ${postVM} 338 ''; 339 memSize = 1024; 340 } 341 '' 342 export PATH=${binPath}:$PATH 343 344 rootDisk=${if partitionTableType != "none" then "/dev/vda${rootPartition}" else "/dev/vda"} 345 346 # Some tools assume these exist 347 ln -s vda /dev/xvda 348 ln -s vda /dev/sda 349 # make systemd-boot find ESP without udev 350 mkdir /dev/block 351 ln -s /dev/vda1 /dev/block/254:1 352 353 mountPoint=/mnt 354 mkdir $mountPoint 355 mount $rootDisk $mountPoint 356 357 # Create the ESP and mount it. Unlike e2fsprogs, mkfs.vfat doesn't support an 358 # '-E offset=X' option, so we can't do this outside the VM. 359 ${optionalString (partitionTableType == "efi" || partitionTableType == "hybrid") '' 360 mkdir -p /mnt/boot 361 mkfs.vfat -n ESP /dev/vda1 362 mount /dev/vda1 /mnt/boot 363 ''} 364 365 # Install a configuration.nix 366 mkdir -p /mnt/etc/nixos 367 ${optionalString (configFile != null) '' 368 cp ${configFile} /mnt/etc/nixos/configuration.nix 369 ''} 370 371 # Set up core system link, GRUB, etc. 372 NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root $mountPoint -- /nix/var/nix/profiles/system/bin/switch-to-configuration boot 373 374 # The above scripts will generate a random machine-id and we don't want to bake a single ID into all our images 375 rm -f $mountPoint/etc/machine-id 376 377 # Set the ownerships of the contents. The modes are set in preVM. 378 # No globbing on targets, so no need to set -f 379 targets_=(${concatStringsSep " " targets}) 380 users_=(${concatStringsSep " " users}) 381 groups_=(${concatStringsSep " " groups}) 382 for ((i = 0; i < ''${#targets_[@]}; i++)); do 383 target="''${targets_[$i]}" 384 user="''${users_[$i]}" 385 group="''${groups_[$i]}" 386 if [ -n "$user$group" ]; then 387 # We have to nixos-enter since we need to use the user and group of the VM 388 nixos-enter --root $mountPoint -- chown -R "$user:$group" "$target" 389 fi 390 done 391 392 umount -R /mnt 393 394 # Make sure resize2fs works. Note that resize2fs has stricter criteria for resizing than a normal 395 # mount, so the `-c 0` and `-i 0` don't affect it. Setting it to `now` doesn't produce deterministic 396 # output, of course, but we can fix that when/if we start making images deterministic. 397 ${optionalString (fsType == "ext4") '' 398 tune2fs -T now -c 0 -i 0 $rootDisk 399 ''} 400 '' 401)