at 24.11-pre 11 kB view raw
1# This module exposes options to build a disk image with a GUID Partition Table 2# (GPT). It uses systemd-repart to build the image. 3 4{ config, pkgs, lib, utils, ... }: 5 6let 7 cfg = config.image.repart; 8 9 inherit (utils.systemdUtils.lib) GPTMaxLabelLength; 10 11 partitionOptions = { 12 options = { 13 storePaths = lib.mkOption { 14 type = with lib.types; listOf path; 15 default = [ ]; 16 description = "The store paths to include in the partition."; 17 }; 18 19 stripNixStorePrefix = lib.mkOption { 20 type = lib.types.bool; 21 default = false; 22 description = '' 23 Whether to strip `/nix/store/` from the store paths. This is useful 24 when you want to build a partition that only contains store paths and 25 is mounted under `/nix/store`. 26 ''; 27 }; 28 29 contents = lib.mkOption { 30 type = with lib.types; attrsOf (submodule { 31 options = { 32 source = lib.mkOption { 33 type = types.path; 34 description = "Path of the source file."; 35 }; 36 }; 37 }); 38 default = { }; 39 example = lib.literalExpression '' 40 { 41 "/EFI/BOOT/BOOTX64.EFI".source = 42 "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi"; 43 44 "/loader/entries/nixos.conf".source = systemdBootEntry; 45 } 46 ''; 47 description = "The contents to end up in the filesystem image."; 48 }; 49 50 repartConfig = lib.mkOption { 51 type = with lib.types; attrsOf (oneOf [ str int bool ]); 52 example = { 53 Type = "home"; 54 SizeMinBytes = "512M"; 55 SizeMaxBytes = "2G"; 56 }; 57 description = '' 58 Specify the repart options for a partiton as a structural setting. 59 See <https://www.freedesktop.org/software/systemd/man/repart.d.html> 60 for all available options. 61 ''; 62 }; 63 }; 64 }; 65 66 mkfsOptionsToEnv = opts: lib.mapAttrs' (fsType: options: { 67 name = "SYSTEMD_REPART_MKFS_OPTIONS_${lib.toUpper fsType}"; 68 value = builtins.concatStringsSep " " options; 69 }) opts; 70in 71{ 72 options.image.repart = { 73 74 name = lib.mkOption { 75 type = lib.types.str; 76 description = '' 77 Name of the image. 78 79 If this option is unset but config.system.image.id is set, 80 config.system.image.id is used as the default value. 81 ''; 82 }; 83 84 version = lib.mkOption { 85 type = lib.types.nullOr lib.types.str; 86 default = config.system.image.version; 87 defaultText = lib.literalExpression "config.system.image.version"; 88 description = "Version of the image"; 89 }; 90 91 imageFileBasename = lib.mkOption { 92 type = lib.types.str; 93 readOnly = true; 94 description = '' 95 Basename of the image filename without any extension (e.g. `image_1`). 96 ''; 97 }; 98 99 imageFile = lib.mkOption { 100 type = lib.types.str; 101 readOnly = true; 102 description = '' 103 Filename of the image including all extensions (e.g `image_1.raw` or 104 `image_1.raw.zst`). 105 ''; 106 }; 107 108 compression = { 109 enable = lib.mkEnableOption "Image compression"; 110 111 algorithm = lib.mkOption { 112 type = lib.types.enum [ "zstd" "xz" ]; 113 default = "zstd"; 114 description = "Compression algorithm"; 115 }; 116 117 level = lib.mkOption { 118 type = lib.types.int; 119 description = '' 120 Compression level. The available range depends on the used algorithm. 121 ''; 122 }; 123 }; 124 125 seed = lib.mkOption { 126 type = with lib.types; nullOr str; 127 # Generated with `uuidgen`. Random but fixed to improve reproducibility. 128 default = "0867da16-f251-457d-a9e8-c31f9a3c220b"; 129 description = '' 130 A UUID to use as a seed. You can set this to `null` to explicitly 131 randomize the partition UUIDs. 132 ''; 133 }; 134 135 split = lib.mkOption { 136 type = lib.types.bool; 137 default = false; 138 description = '' 139 Enables generation of split artifacts from partitions. If enabled, for 140 each partition with SplitName= set, a separate output file containing 141 just the contents of that partition is generated. 142 ''; 143 }; 144 145 sectorSize = lib.mkOption { 146 type = with lib.types; nullOr int; 147 default = 512; 148 example = lib.literalExpression "4096"; 149 description = '' 150 The sector size of the disk image produced by systemd-repart. This 151 value must be a power of 2 between 512 and 4096. 152 ''; 153 }; 154 155 package = lib.mkPackageOption pkgs "systemd-repart" { 156 # We use buildPackages so that repart images are built with the build 157 # platform's systemd, allowing for cross-compiled systems to work. 158 default = [ "buildPackages" "systemd" ]; 159 example = "pkgs.buildPackages.systemdMinimal.override { withCryptsetup = true; }"; 160 }; 161 162 partitions = lib.mkOption { 163 type = with lib.types; attrsOf (submodule partitionOptions); 164 default = { }; 165 example = lib.literalExpression '' 166 { 167 "10-esp" = { 168 contents = { 169 "/EFI/BOOT/BOOTX64.EFI".source = 170 "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi"; 171 } 172 repartConfig = { 173 Type = "esp"; 174 Format = "fat"; 175 }; 176 }; 177 "20-root" = { 178 storePaths = [ config.system.build.toplevel ]; 179 repartConfig = { 180 Type = "root"; 181 Format = "ext4"; 182 Minimize = "guess"; 183 }; 184 }; 185 }; 186 ''; 187 description = '' 188 Specify partitions as a set of the names of the partitions with their 189 configuration as the key. 190 ''; 191 }; 192 193 mkfsOptions = lib.mkOption { 194 type = with lib.types; attrsOf (listOf str); 195 default = {}; 196 example = lib.literalExpression '' 197 { 198 vfat = [ "-S 512" "-c" ]; 199 } 200 ''; 201 description = '' 202 Specify extra options for created file systems. The specified options 203 are converted to individual environment variables of the format 204 `SYSTEMD_REPART_MKFS_OPTIONS_<FSTYPE>`. 205 206 See [upstream systemd documentation](https://github.com/systemd/systemd/blob/v255/docs/ENVIRONMENT.md?plain=1#L575-L577) 207 for information about the usage of these environment variables. 208 209 The example would produce the following environment variable: 210 ``` 211 SYSTEMD_REPART_MKFS_OPTIONS_VFAT="-S 512 -c" 212 ``` 213 ''; 214 }; 215 216 finalPartitions = lib.mkOption { 217 type = lib.types.attrs; 218 internal = true; 219 readOnly = true; 220 description = '' 221 Convenience option to access partitions with added closures. 222 ''; 223 }; 224 225 }; 226 227 config = { 228 229 assertions = lib.mapAttrsToList (fileName: partitionConfig: 230 let 231 inherit (partitionConfig) repartConfig; 232 labelLength = builtins.stringLength repartConfig.Label; 233 in 234 { 235 assertion = repartConfig ? Label -> GPTMaxLabelLength >= labelLength; 236 message = '' 237 The partition label '${repartConfig.Label}' 238 defined for '${fileName}' is ${toString labelLength} characters long, 239 but the maximum label length supported by UEFI is ${toString 240 GPTMaxLabelLength}. 241 ''; 242 } 243 ) cfg.partitions; 244 245 warnings = lib.filter (v: v != null) (lib.mapAttrsToList (fileName: partitionConfig: 246 let 247 inherit (partitionConfig) repartConfig; 248 suggestedMaxLabelLength = GPTMaxLabelLength - 2; 249 labelLength = builtins.stringLength repartConfig.Label; 250 in 251 if (repartConfig ? Label && labelLength >= suggestedMaxLabelLength) then '' 252 The partition label '${repartConfig.Label}' 253 defined for '${fileName}' is ${toString labelLength} characters long. 254 The suggested maximum label length is ${toString 255 suggestedMaxLabelLength}. 256 257 If you use sytemd-sysupdate style A/B updates, this might 258 not leave enough space to increment the version number included in 259 the label in a future release. For example, if your label is 260 ${toString GPTMaxLabelLength} characters long (the maximum enforced by UEFI) and 261 you're at version 9, you cannot increment this to 10. 262 '' else null 263 ) cfg.partitions); 264 265 image.repart = 266 let 267 version = config.image.repart.version; 268 versionInfix = if version != null then "_${version}" else ""; 269 compressionSuffix = lib.optionalString cfg.compression.enable 270 { 271 "zstd" = ".zst"; 272 "xz" = ".xz"; 273 }."${cfg.compression.algorithm}"; 274 275 makeClosure = paths: pkgs.closureInfo { rootPaths = paths; }; 276 277 # Add the closure of the provided Nix store paths to cfg.partitions so 278 # that amend-repart-definitions.py can read it. 279 addClosure = _name: partitionConfig: partitionConfig // ( 280 lib.optionalAttrs 281 (partitionConfig.storePaths or [ ] != [ ]) 282 { closure = "${makeClosure partitionConfig.storePaths}/store-paths"; } 283 ); 284 in 285 { 286 name = lib.mkIf (config.system.image.id != null) (lib.mkOptionDefault config.system.image.id); 287 imageFileBasename = cfg.name + versionInfix; 288 imageFile = cfg.imageFileBasename + ".raw" + compressionSuffix; 289 290 compression = { 291 # Generally default to slightly faster than default compression 292 # levels under the assumption that most of the building will be done 293 # for development and release builds will be customized. 294 level = lib.mkOptionDefault { 295 "zstd" = 3; 296 "xz" = 3; 297 }."${cfg.compression.algorithm}"; 298 }; 299 300 finalPartitions = lib.mapAttrs addClosure cfg.partitions; 301 }; 302 303 system.build.image = 304 let 305 fileSystems = lib.filter 306 (f: f != null) 307 (lib.mapAttrsToList (_n: v: v.repartConfig.Format or null) cfg.partitions); 308 309 310 format = pkgs.formats.ini { }; 311 312 definitionsDirectory = utils.systemdUtils.lib.definitions 313 "repart.d" 314 format 315 (lib.mapAttrs (_n: v: { Partition = v.repartConfig; }) cfg.finalPartitions); 316 317 partitionsJSON = pkgs.writeText "partitions.json" (builtins.toJSON cfg.finalPartitions); 318 319 mkfsEnv = mkfsOptionsToEnv cfg.mkfsOptions; 320 in 321 pkgs.callPackage ./repart-image.nix { 322 systemd = cfg.package; 323 inherit (cfg) name version imageFileBasename compression split seed sectorSize; 324 inherit fileSystems definitionsDirectory partitionsJSON mkfsEnv; 325 }; 326 327 meta.maintainers = with lib.maintainers; [ nikstur willibutz ]; 328 329 }; 330}