1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8
9let
10 # The scripted initrd contains some magic to add the prefix to the
11 # paths just in time, so we don't add it here.
12 sysrootPrefix =
13 fs:
14 if
15 config.boot.initrd.systemd.enable
16 && fs.overlay.useStage1BaseDirectories
17 && (utils.fsNeededForBoot fs)
18 then
19 "/sysroot"
20 else
21 "";
22
23 # Returns a service that creates the required directories before the mount is
24 # created.
25 preMountService =
26 _name: fs:
27 let
28 prefix = sysrootPrefix fs;
29
30 escapedMountpoint = utils.escapeSystemdPath (prefix + fs.mountPoint);
31 mountUnit = "${escapedMountpoint}.mount";
32
33 upperdir = prefix + fs.overlay.upperdir;
34 workdir = prefix + fs.overlay.workdir;
35 in
36 lib.mkIf (fs.overlay.upperdir != null) {
37 "rw-${escapedMountpoint}" = {
38 requiredBy = [ mountUnit ];
39 before = [ mountUnit ];
40 unitConfig = {
41 DefaultDependencies = false;
42 RequiresMountsFor = "${upperdir} ${workdir}";
43 };
44 serviceConfig = {
45 Type = "oneshot";
46 ExecStart = "${pkgs.coreutils}/bin/mkdir -p -m 0755 ${upperdir} ${workdir}";
47 };
48 };
49 };
50
51 overlayOpts =
52 {
53 config,
54 ...
55 }:
56 {
57 options.overlay = {
58 lowerdir = lib.mkOption {
59 type = with lib.types; nullOr (nonEmptyListOf (either str pathInStore));
60 default = null;
61 description = ''
62 The list of path(s) to the lowerdir(s).
63
64 To create a writable overlay, you MUST provide an `upperdir` and a
65 `workdir`.
66
67 You can create a read-only overlay when you provide multiple (at
68 least 2!) lowerdirs and neither an `upperdir` nor a `workdir`.
69 '';
70 };
71
72 upperdir = lib.mkOption {
73 type = lib.types.nullOr lib.types.str;
74 default = null;
75 description = ''
76 The path to the upperdir.
77
78 If this is null, a read-only overlay is created using the lowerdir.
79
80 If the filesystem is `neededForBoot`, this will be prefixed with `/sysroot`,
81 unless `useStage1BaseDirectories` is set to `true`.
82
83 If you set this to some value you MUST also set `workdir`.
84 '';
85 };
86
87 workdir = lib.mkOption {
88 type = lib.types.nullOr lib.types.str;
89 default = null;
90 description = ''
91 The path to the workdir.
92
93 If the filesystem is `neededForBoot`, this will be prefixed with `/sysroot`,
94 unless `useStage1BaseDirectories` is set to `true`.
95
96 This MUST be set if you set `upperdir`.
97 '';
98 };
99
100 useStage1BaseDirectories = lib.mkOption {
101 type = lib.types.bool;
102 default = true;
103 description = ''
104 If enabled, `lowerdir`, `upperdir` and `workdir` will be prefixed with `/sysroot`.
105
106 Disabling this can be useful to create an overlay over directories which aren't on the real root.
107
108 Disabling this does not work with the scripted (i.e. non-systemd) initrd.
109 '';
110 };
111 };
112
113 config = lib.mkIf (config.overlay.lowerdir != null) {
114 fsType = "overlay";
115 device = lib.mkDefault "overlay";
116 depends = map (x: "${x}") (
117 config.overlay.lowerdir
118 ++ lib.optionals (config.overlay.upperdir != null) [
119 config.overlay.upperdir
120 config.overlay.workdir
121 ]
122 );
123
124 options =
125 let
126 prefix = sysrootPrefix config;
127
128 lowerdir = map (s: prefix + s) config.overlay.lowerdir;
129 upperdir = prefix + config.overlay.upperdir;
130 workdir = prefix + config.overlay.workdir;
131 in
132 [
133 "lowerdir=${lib.concatStringsSep ":" lowerdir}"
134 ]
135 ++ lib.optionals (config.overlay.upperdir != null) [
136 "upperdir=${upperdir}"
137 "workdir=${workdir}"
138 ];
139 };
140 };
141in
142
143{
144
145 options = {
146
147 # Merge the overlay options into the fileSystems option.
148 fileSystems = lib.mkOption {
149 type = lib.types.attrsOf (lib.types.submodule [ overlayOpts ]);
150 };
151
152 };
153
154 config =
155 let
156 overlayFileSystems = lib.filterAttrs (_name: fs: (fs.overlay.lowerdir != null)) config.fileSystems;
157 initrdFileSystems = lib.filterAttrs (_name: utils.fsNeededForBoot) overlayFileSystems;
158 userspaceFileSystems = lib.filterAttrs (_name: fs: (!utils.fsNeededForBoot fs)) overlayFileSystems;
159 in
160 {
161
162 boot.initrd.availableKernelModules = lib.mkIf (initrdFileSystems != { }) [ "overlay" ];
163
164 assertions =
165 lib.concatLists (
166 lib.mapAttrsToList (_name: fs: [
167 {
168 assertion = (fs.overlay.upperdir == null) == (fs.overlay.workdir == null);
169 message = "You cannot define a `lowerdir` without a `workdir` and vice versa for mount point: ${fs.mountPoint}";
170 }
171 {
172 assertion =
173 (fs.overlay.lowerdir != null && fs.overlay.upperdir == null)
174 -> (lib.length fs.overlay.lowerdir) >= 2;
175 message = "A read-only overlay (without an `upperdir`) requires at least 2 `lowerdir`s: ${fs.mountPoint}";
176 }
177 {
178 assertion = !fs.overlay.useStage1BaseDirectories -> config.boot.initrd.systemd.enable;
179 message = ''
180 Stage 1 overlay file system ${fs.mountPoint} has 'useStage1BaseDirectories' set to false,
181 which is not supported with scripted initrd. Please enable 'boot.initrd.systemd.enable'.
182 '';
183 }
184 ]) overlayFileSystems
185 )
186 ++ lib.mapAttrsToList (_: fs: {
187 assertion = fs.overlay.upperdir == null -> config.boot.initrd.systemd.enable;
188 message = ''
189 Stage 1 overlay file system ${fs.mountPoint} has no upperdir,
190 which is not supported with scripted initrd. Please enable
191 'boot.initrd.systemd.enable'.
192 '';
193 }) initrdFileSystems;
194
195 boot.initrd.systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService initrdFileSystems);
196 systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService userspaceFileSystems);
197
198 };
199
200}