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 partitionOptions = {
10 options = {
11 storePaths = lib.mkOption {
12 type = with lib.types; listOf path;
13 default = [ ];
14 description = lib.mdDoc "The store paths to include in the partition.";
15 };
16
17 stripNixStorePrefix = lib.mkOption {
18 type = lib.types.bool;
19 default = false;
20 description = lib.mdDoc ''
21 Whether to strip `/nix/store/` from the store paths. This is useful
22 when you want to build a partition that only contains store paths and
23 is mounted under `/nix/store`.
24 '';
25 };
26
27 contents = lib.mkOption {
28 type = with lib.types; attrsOf (submodule {
29 options = {
30 source = lib.mkOption {
31 type = types.path;
32 description = lib.mdDoc "Path of the source file.";
33 };
34 };
35 });
36 default = { };
37 example = lib.literalExpression ''
38 {
39 "/EFI/BOOT/BOOTX64.EFI".source =
40 "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
41
42 "/loader/entries/nixos.conf".source = systemdBootEntry;
43 }
44 '';
45 description = lib.mdDoc "The contents to end up in the filesystem image.";
46 };
47
48 repartConfig = lib.mkOption {
49 type = with lib.types; attrsOf (oneOf [ str int bool ]);
50 example = {
51 Type = "home";
52 SizeMinBytes = "512M";
53 SizeMaxBytes = "2G";
54 };
55 description = lib.mdDoc ''
56 Specify the repart options for a partiton as a structural setting.
57 See <https://www.freedesktop.org/software/systemd/man/repart.d.html>
58 for all available options.
59 '';
60 };
61 };
62 };
63in
64{
65 options.image.repart = {
66
67 name = lib.mkOption {
68 type = lib.types.str;
69 description = lib.mdDoc "The name of the image.";
70 };
71
72 seed = lib.mkOption {
73 type = with lib.types; nullOr str;
74 # Generated with `uuidgen`. Random but fixed to improve reproducibility.
75 default = "0867da16-f251-457d-a9e8-c31f9a3c220b";
76 description = lib.mdDoc ''
77 A UUID to use as a seed. You can set this to `null` to explicitly
78 randomize the partition UUIDs.
79 '';
80 };
81
82 split = lib.mkOption {
83 type = lib.types.bool;
84 default = false;
85 description = lib.mdDoc ''
86 Enables generation of split artifacts from partitions. If enabled, for
87 each partition with SplitName= set, a separate output file containing
88 just the contents of that partition is generated.
89 '';
90 };
91
92 package = lib.mkPackageOption pkgs "systemd-repart" {
93 default = "systemd";
94 example = "pkgs.systemdMinimal.override { withCryptsetup = true; }";
95 };
96
97 partitions = lib.mkOption {
98 type = with lib.types; attrsOf (submodule partitionOptions);
99 default = { };
100 example = lib.literalExpression ''
101 {
102 "10-esp" = {
103 contents = {
104 "/EFI/BOOT/BOOTX64.EFI".source =
105 "''${pkgs.systemd}/lib/systemd/boot/efi/systemd-bootx64.efi";
106 }
107 repartConfig = {
108 Type = "esp";
109 Format = "fat";
110 };
111 };
112 "20-root" = {
113 storePaths = [ config.system.build.toplevel ];
114 repartConfig = {
115 Type = "root";
116 Format = "ext4";
117 Minimize = "guess";
118 };
119 };
120 };
121 '';
122 description = lib.mdDoc ''
123 Specify partitions as a set of the names of the partitions with their
124 configuration as the key.
125 '';
126 };
127
128 };
129
130 config = {
131
132 system.build.image =
133 let
134 fileSystemToolMapping = with pkgs; {
135 "vfat" = [ dosfstools mtools ];
136 "ext4" = [ e2fsprogs.bin ];
137 "squashfs" = [ squashfsTools ];
138 "erofs" = [ erofs-utils ];
139 "btrfs" = [ btrfs-progs ];
140 "xfs" = [ xfsprogs ];
141 };
142
143 fileSystems = lib.filter
144 (f: f != null)
145 (lib.mapAttrsToList (_n: v: v.repartConfig.Format or null) cfg.partitions);
146
147 fileSystemTools = builtins.concatMap (f: fileSystemToolMapping."${f}") fileSystems;
148
149
150 makeClosure = paths: pkgs.closureInfo { rootPaths = paths; };
151
152 # Add the closure of the provided Nix store paths to cfg.partitions so
153 # that amend-repart-definitions.py can read it.
154 addClosure = _name: partitionConfig: partitionConfig // (
155 lib.optionalAttrs
156 (partitionConfig.storePaths or [ ] != [ ])
157 { closure = "${makeClosure partitionConfig.storePaths}/store-paths"; }
158 );
159
160
161 finalPartitions = lib.mapAttrs addClosure cfg.partitions;
162
163
164 amendRepartDefinitions = pkgs.runCommand "amend-repart-definitions.py"
165 {
166 nativeBuildInputs = with pkgs; [ black ruff mypy ];
167 buildInputs = [ pkgs.python3 ];
168 } ''
169 install ${./amend-repart-definitions.py} $out
170 patchShebangs --host $out
171
172 black --check --diff $out
173 ruff --line-length 88 $out
174 mypy --strict $out
175 '';
176
177 format = pkgs.formats.ini { };
178
179 definitionsDirectory = utils.systemdUtils.lib.definitions
180 "repart.d"
181 format
182 (lib.mapAttrs (_n: v: { Partition = v.repartConfig; }) finalPartitions);
183
184 partitions = pkgs.writeText "partitions.json" (builtins.toJSON finalPartitions);
185 in
186 pkgs.runCommand cfg.name
187 {
188 nativeBuildInputs = [
189 cfg.package
190 pkgs.fakeroot
191 pkgs.util-linux
192 ] ++ fileSystemTools;
193 } ''
194 amendedRepartDefinitions=$(${amendRepartDefinitions} ${partitions} ${definitionsDirectory})
195
196 mkdir -p $out
197 cd $out
198
199 unshare --map-root-user fakeroot systemd-repart \
200 --dry-run=no \
201 --empty=create \
202 --size=auto \
203 --seed="${cfg.seed}" \
204 --definitions="$amendedRepartDefinitions" \
205 --split="${lib.boolToString cfg.split}" \
206 --json=pretty \
207 image.raw \
208 | tee repart-output.json
209 '';
210
211 meta.maintainers = with lib.maintainers; [ nikstur ];
212
213 };
214}