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}