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