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}