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}