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