1# Management of static files in /etc.
2{
3 config,
4 lib,
5 pkgs,
6 ...
7}:
8let
9
10 etc' = lib.filter (f: f.enable) (lib.attrValues config.environment.etc);
11
12 etc =
13 pkgs.runCommandLocal "etc"
14 {
15 # This is needed for the systemd module
16 passthru.targets = map (x: x.target) etc';
17 } # sh
18 ''
19 set -euo pipefail
20
21 makeEtcEntry() {
22 src="$1"
23 target="$2"
24 mode="$3"
25 user="$4"
26 group="$5"
27
28 if [[ "$src" = *'*'* ]]; then
29 # If the source name contains '*', perform globbing.
30 mkdir -p "$out/etc/$target"
31 for fn in $src; do
32 ln -s "$fn" "$out/etc/$target/"
33 done
34 else
35
36 mkdir -p "$out/etc/$(dirname "$target")"
37 if ! [ -e "$out/etc/$target" ]; then
38 ln -s "$src" "$out/etc/$target"
39 else
40 echo "duplicate entry $target -> $src"
41 if [ "$(readlink "$out/etc/$target")" != "$src" ]; then
42 echo "mismatched duplicate entry $(readlink "$out/etc/$target") <-> $src"
43 ret=1
44 fi
45 fi
46
47 if [ "$mode" != symlink ]; then
48 echo "$mode" > "$out/etc/$target.mode"
49 echo "$user" > "$out/etc/$target.uid"
50 echo "$group" > "$out/etc/$target.gid"
51 fi
52 fi
53 }
54
55 mkdir -p "$out/etc"
56 ${lib.concatMapStringsSep "\n" (
57 etcEntry:
58 lib.escapeShellArgs [
59 "makeEtcEntry"
60 # Force local source paths to be added to the store
61 "${etcEntry.source}"
62 etcEntry.target
63 etcEntry.mode
64 etcEntry.user
65 etcEntry.group
66 ]
67 ) etc'}
68 '';
69
70 etcHardlinks = lib.filter (f: f.mode != "symlink" && f.mode != "direct-symlink") etc';
71
72in
73
74{
75
76 imports = [ ../build.nix ];
77
78 ###### interface
79
80 options = {
81
82 system.etc.overlay = {
83 enable = lib.mkOption {
84 type = lib.types.bool;
85 default = false;
86 description = ''
87 Mount `/etc` as an overlayfs instead of generating it via a perl script.
88
89 Note: This is currently experimental. Only enable this option if you're
90 confident that you can recover your system if it breaks.
91 '';
92 };
93
94 mutable = lib.mkOption {
95 type = lib.types.bool;
96 default = true;
97 description = ''
98 Whether to mount `/etc` mutably (i.e. read-write) or immutably (i.e. read-only).
99
100 If this is false, only the immutable lowerdir is mounted. If it is
101 true, a writable upperdir is mounted on top.
102 '';
103 };
104 };
105
106 environment.etc = lib.mkOption {
107 default = { };
108 example = lib.literalExpression ''
109 { example-configuration-file =
110 { source = "/nix/store/.../etc/dir/file.conf.example";
111 mode = "0440";
112 };
113 "default/useradd".text = "GROUP=100 ...";
114 }
115 '';
116 description = ''
117 Set of files that have to be linked in {file}`/etc`.
118 '';
119
120 type =
121 with lib.types;
122 attrsOf (
123 submodule (
124 {
125 name,
126 config,
127 options,
128 ...
129 }:
130 {
131 options = {
132
133 enable = lib.mkOption {
134 type = lib.types.bool;
135 default = true;
136 description = ''
137 Whether this /etc file should be generated. This
138 option allows specific /etc files to be disabled.
139 '';
140 };
141
142 target = lib.mkOption {
143 type = lib.types.str;
144 description = ''
145 Name of symlink (relative to
146 {file}`/etc`). Defaults to the attribute
147 name.
148 '';
149 };
150
151 text = lib.mkOption {
152 default = null;
153 type = lib.types.nullOr lib.types.lines;
154 description = "Text of the file.";
155 };
156
157 source = lib.mkOption {
158 type = lib.types.path;
159 description = "Path of the source file.";
160 };
161
162 mode = lib.mkOption {
163 type = lib.types.str;
164 default = "symlink";
165 example = "0600";
166 description = ''
167 If set to something else than `symlink`,
168 the file is copied instead of symlinked, with the given
169 file mode.
170 '';
171 };
172
173 uid = lib.mkOption {
174 default = 0;
175 type = lib.types.int;
176 description = ''
177 UID of created file. Only takes effect when the file is
178 copied (that is, the mode is not 'symlink').
179 '';
180 };
181
182 gid = lib.mkOption {
183 default = 0;
184 type = lib.types.int;
185 description = ''
186 GID of created file. Only takes effect when the file is
187 copied (that is, the mode is not 'symlink').
188 '';
189 };
190
191 user = lib.mkOption {
192 default = "+${toString config.uid}";
193 type = lib.types.str;
194 description = ''
195 User name of file owner.
196
197 Only takes effect when the file is copied (that is, the
198 mode is not `symlink`).
199
200 When `services.userborn.enable`, this option has no effect.
201 You have to assign a `uid` instead. Otherwise this option
202 takes precedence over `uid`.
203 '';
204 };
205
206 group = lib.mkOption {
207 default = "+${toString config.gid}";
208 type = lib.types.str;
209 description = ''
210 Group name of file owner.
211
212 Only takes effect when the file is copied (that is, the
213 mode is not `symlink`).
214
215 When `services.userborn.enable`, this option has no effect.
216 You have to assign a `gid` instead. Otherwise this option
217 takes precedence over `gid`.
218 '';
219 };
220
221 };
222
223 config = {
224 target = lib.mkDefault name;
225 source = lib.mkIf (config.text != null) (
226 let
227 name' = "etc-" + lib.replaceStrings [ "/" ] [ "-" ] name;
228 in
229 lib.mkDerivedConfig options.text (pkgs.writeText name')
230 );
231 };
232
233 }
234 )
235 );
236
237 };
238
239 };
240
241 ###### implementation
242
243 config = {
244
245 system.build.etc = etc;
246 system.build.etcActivationCommands =
247 let
248 etcOverlayOptions = lib.concatStringsSep "," (
249 [
250 "relatime"
251 "redirect_dir=on"
252 "metacopy=on"
253 ]
254 ++ lib.optionals config.system.etc.overlay.mutable [
255 "upperdir=/.rw-etc/upper"
256 "workdir=/.rw-etc/work"
257 ]
258 );
259 in
260 if config.system.etc.overlay.enable then
261 #bash
262 ''
263 # This script atomically remounts /etc when switching configuration.
264 # On a (re-)boot this should not run because /etc is mounted via a
265 # systemd mount unit instead.
266 # The activation script can also be called in cases where we didn't have
267 # an initrd though, like for instance when using nixos-enter,
268 # so we cannot assume that /etc has already been mounted.
269 #
270 # To a large extent this mimics what composefs does. Because
271 # it's relatively simple, however, we avoid the composefs dependency.
272 # Since this script is not idempotent, it should not run when etc hasn't
273 # changed.
274 if [[ ! $IN_NIXOS_SYSTEMD_STAGE1 ]] && [[ "${config.system.build.etc}/etc" != "$(readlink -f /run/current-system/etc)" ]]; then
275 echo "remounting /etc..."
276
277 ${lib.optionalString config.system.etc.overlay.mutable ''
278 # These directories are usually created in initrd,
279 # but we need to create them here when we're called directly,
280 # for instance by nixos-enter
281 mkdir --parents /.rw-etc/upper /.rw-etc/work
282 chmod 0755 /.rw-etc /.rw-etc/upper /.rw-etc/work
283 ''}
284
285 tmpMetadataMount=$(TMPDIR="/run" mktemp --directory -t nixos-etc-metadata.XXXXXXXXXX)
286 mount --type erofs -o ro ${config.system.build.etcMetadataImage} $tmpMetadataMount
287
288 # There was no previous /etc mounted. This happens when we're called
289 # directly without an initrd, like with nixos-enter.
290 if ! mountpoint -q /etc; then
291 mount --type overlay overlay \
292 --options lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \
293 /etc
294 else
295 # Mount the new /etc overlay to a temporary private mount.
296 # This needs the indirection via a private bind mount because you
297 # cannot move shared mounts.
298 tmpEtcMount=$(TMPDIR="/run" mktemp --directory -t nixos-etc.XXXXXXXXXX)
299 mount --bind --make-private $tmpEtcMount $tmpEtcMount
300 mount --type overlay overlay \
301 --options lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \
302 $tmpEtcMount
303
304 # Before moving the new /etc overlay under the old /etc, we have to
305 # move mounts on top of /etc to the new /etc mountpoint.
306 findmnt /etc --submounts --list --noheading --kernel --output TARGET | while read -r mountPoint; do
307 if [[ "$mountPoint" = "/etc" ]]; then
308 continue
309 fi
310
311 tmpMountPoint="$tmpEtcMount/''${mountPoint:5}"
312 ${
313 if config.system.etc.overlay.mutable then
314 ''
315 if [[ -f "$mountPoint" ]]; then
316 touch "$tmpMountPoint"
317 elif [[ -d "$mountPoint" ]]; then
318 mkdir -p "$tmpMountPoint"
319 fi
320 ''
321 else
322 ''
323 if [[ ! -e "$tmpMountPoint" ]]; then
324 echo "Skipping undeclared mountpoint in environment.etc: $mountPoint"
325 continue
326 fi
327 ''
328 }
329 mount --bind "$mountPoint" "$tmpMountPoint"
330 done
331
332 # Move the new temporary /etc mount underneath the current /etc mount.
333 #
334 # This should eventually use util-linux to perform this move beneath,
335 # however, this functionality is not yet in util-linux. See this
336 # tracking issue: https://github.com/util-linux/util-linux/issues/2604
337 ${pkgs.move-mount-beneath}/bin/move-mount --move --beneath $tmpEtcMount /etc
338
339 # Unmount the top /etc mount to atomically reveal the new mount.
340 umount --lazy --recursive /etc
341
342 # Unmount the temporary mount
343 umount --lazy "$tmpEtcMount"
344 rmdir "$tmpEtcMount"
345 fi
346
347 # Unmount old metadata mounts
348 # For some reason, `findmnt /tmp --submounts` does not show the nested
349 # mounts. So we'll just find all mounts of type erofs and filter on the
350 # name of the mountpoint.
351 findmnt --type erofs --list --kernel --output TARGET | while read -r mountPoint; do
352 if [[ ("$mountPoint" =~ ^/run/nixos-etc-metadata\..{10}$ || "$mountPoint" =~ ^/run/nixos-etc-metadata$ ) &&
353 "$mountPoint" != "$tmpMetadataMount" ]]; then
354 umount --lazy "$mountPoint"
355 rmdir "$mountPoint"
356 fi
357 done
358 fi
359 ''
360 else
361 ''
362 # Set up the statically computed bits of /etc.
363 echo "setting up /etc..."
364 ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc
365 '';
366
367 system.build.etcBasedir = pkgs.runCommandLocal "etc-lowerdir" { } ''
368 set -euo pipefail
369
370 makeEtcEntry() {
371 src="$1"
372 target="$2"
373
374 mkdir -p "$out/$(dirname "$target")"
375 cp "$src" "$out/$target"
376 }
377
378 mkdir -p "$out"
379 ${lib.concatMapStringsSep "\n" (
380 etcEntry:
381 lib.escapeShellArgs [
382 "makeEtcEntry"
383 # Force local source paths to be added to the store
384 "${etcEntry.source}"
385 etcEntry.target
386 ]
387 ) etcHardlinks}
388 '';
389
390 system.build.etcMetadataImage =
391 let
392 etcJson = pkgs.writeText "etc-json" (builtins.toJSON etc');
393 etcDump = pkgs.runCommand "etc-dump" { } ''
394 ${lib.getExe pkgs.buildPackages.python3} ${./build-composefs-dump.py} ${etcJson} > $out
395 '';
396 in
397 pkgs.runCommand "etc-metadata.erofs"
398 {
399 nativeBuildInputs = with pkgs.buildPackages; [
400 composefs
401 erofs-utils
402 ];
403 }
404 ''
405 mkcomposefs --from-file ${etcDump} $out
406 fsck.erofs $out
407 '';
408
409 };
410
411}