1# Management of static files in /etc.
2
3{ config, lib, pkgs, ... }:
4
5with lib;
6
7let
8
9 etc' = filter (f: f.enable) (attrValues config.environment.etc);
10
11 etc = pkgs.runCommandLocal "etc" {
12 # This is needed for the systemd module
13 passthru.targets = map (x: x.target) etc';
14 } /* sh */ ''
15 set -euo pipefail
16
17 makeEtcEntry() {
18 src="$1"
19 target="$2"
20 mode="$3"
21 user="$4"
22 group="$5"
23
24 if [[ "$src" = *'*'* ]]; then
25 # If the source name contains '*', perform globbing.
26 mkdir -p "$out/etc/$target"
27 for fn in $src; do
28 ln -s "$fn" "$out/etc/$target/"
29 done
30 else
31
32 mkdir -p "$out/etc/$(dirname "$target")"
33 if ! [ -e "$out/etc/$target" ]; then
34 ln -s "$src" "$out/etc/$target"
35 else
36 echo "duplicate entry $target -> $src"
37 if [ "$(readlink "$out/etc/$target")" != "$src" ]; then
38 echo "mismatched duplicate entry $(readlink "$out/etc/$target") <-> $src"
39 ret=1
40
41 continue
42 fi
43 fi
44
45 if [ "$mode" != symlink ]; then
46 echo "$mode" > "$out/etc/$target.mode"
47 echo "$user" > "$out/etc/$target.uid"
48 echo "$group" > "$out/etc/$target.gid"
49 fi
50 fi
51 }
52
53 mkdir -p "$out/etc"
54 ${concatMapStringsSep "\n" (etcEntry: escapeShellArgs [
55 "makeEtcEntry"
56 # Force local source paths to be added to the store
57 "${etcEntry.source}"
58 etcEntry.target
59 etcEntry.mode
60 etcEntry.user
61 etcEntry.group
62 ]) etc'}
63 '';
64
65 etcHardlinks = filter (f: f.mode != "symlink") etc';
66
67 build-composefs-dump = pkgs.runCommand "build-composefs-dump.py"
68 {
69 buildInputs = [ pkgs.python3 ];
70 } ''
71 install ${./build-composefs-dump.py} $out
72 patchShebangs --host $out
73 '';
74
75in
76
77{
78
79 imports = [ ../build.nix ];
80
81 ###### interface
82
83 options = {
84
85 system.etc.overlay = {
86 enable = mkOption {
87 type = types.bool;
88 default = false;
89 description = ''
90 Mount `/etc` as an overlayfs instead of generating it via a perl script.
91
92 Note: This is currently experimental. Only enable this option if you're
93 confident that you can recover your system if it breaks.
94 '';
95 };
96
97 mutable = mkOption {
98 type = types.bool;
99 default = true;
100 description = ''
101 Whether to mount `/etc` mutably (i.e. read-write) or immutably (i.e. read-only).
102
103 If this is false, only the immutable lowerdir is mounted. If it is
104 true, a writable upperdir is mounted on top.
105 '';
106 };
107 };
108
109 environment.etc = mkOption {
110 default = {};
111 example = literalExpression ''
112 { example-configuration-file =
113 { source = "/nix/store/.../etc/dir/file.conf.example";
114 mode = "0440";
115 };
116 "default/useradd".text = "GROUP=100 ...";
117 }
118 '';
119 description = ''
120 Set of files that have to be linked in {file}`/etc`.
121 '';
122
123 type = with types; attrsOf (submodule (
124 { name, config, options, ... }:
125 { options = {
126
127 enable = mkOption {
128 type = types.bool;
129 default = true;
130 description = ''
131 Whether this /etc file should be generated. This
132 option allows specific /etc files to be disabled.
133 '';
134 };
135
136 target = mkOption {
137 type = types.str;
138 description = ''
139 Name of symlink (relative to
140 {file}`/etc`). Defaults to the attribute
141 name.
142 '';
143 };
144
145 text = mkOption {
146 default = null;
147 type = types.nullOr types.lines;
148 description = "Text of the file.";
149 };
150
151 source = mkOption {
152 type = types.path;
153 description = "Path of the source file.";
154 };
155
156 mode = mkOption {
157 type = types.str;
158 default = "symlink";
159 example = "0600";
160 description = ''
161 If set to something else than `symlink`,
162 the file is copied instead of symlinked, with the given
163 file mode.
164 '';
165 };
166
167 uid = mkOption {
168 default = 0;
169 type = types.int;
170 description = ''
171 UID of created file. Only takes effect when the file is
172 copied (that is, the mode is not 'symlink').
173 '';
174 };
175
176 gid = mkOption {
177 default = 0;
178 type = types.int;
179 description = ''
180 GID of created file. Only takes effect when the file is
181 copied (that is, the mode is not 'symlink').
182 '';
183 };
184
185 user = mkOption {
186 default = "+${toString config.uid}";
187 type = types.str;
188 description = ''
189 User name of created file.
190 Only takes effect when the file is copied (that is, the mode is not 'symlink').
191 Changing this option takes precedence over `uid`.
192 '';
193 };
194
195 group = mkOption {
196 default = "+${toString config.gid}";
197 type = types.str;
198 description = ''
199 Group name of created file.
200 Only takes effect when the file is copied (that is, the mode is not 'symlink').
201 Changing this option takes precedence over `gid`.
202 '';
203 };
204
205 };
206
207 config = {
208 target = mkDefault name;
209 source = mkIf (config.text != null) (
210 let name' = "etc-" + lib.replaceStrings ["/"] ["-"] name;
211 in mkDerivedConfig options.text (pkgs.writeText name')
212 );
213 };
214
215 }));
216
217 };
218
219 };
220
221
222 ###### implementation
223
224 config = {
225
226 system.build.etc = etc;
227 system.build.etcActivationCommands = let
228 etcOverlayOptions = lib.concatStringsSep "," ([
229 "relatime"
230 "redirect_dir=on"
231 "metacopy=on"
232 ] ++ lib.optionals config.system.etc.overlay.mutable [
233 "upperdir=/.rw-etc/upper"
234 "workdir=/.rw-etc/work"
235 ]);
236 in if config.system.etc.overlay.enable then ''
237 # This script atomically remounts /etc when switching configuration. On a (re-)boot
238 # this should not run because /etc is mounted via a systemd mount unit
239 # instead. To a large extent this mimics what composefs does. Because
240 # it's relatively simple, however, we avoid the composefs dependency.
241 # Since this script is not idempotent, it should not run when etc hasn't
242 # changed.
243 if [[ ! $IN_NIXOS_SYSTEMD_STAGE1 ]] && [[ "${config.system.build.etc}/etc" != "$(readlink -f /run/current-system/etc)" ]]; then
244 echo "remounting /etc..."
245
246 tmpMetadataMount=$(mktemp --directory)
247 mount --type erofs ${config.system.build.etcMetadataImage} $tmpMetadataMount
248
249 # Mount the new /etc overlay to a temporary private mount.
250 # This needs the indirection via a private bind mount because you
251 # cannot move shared mounts.
252 tmpEtcMount=$(mktemp --directory)
253 mount --bind --make-private $tmpEtcMount $tmpEtcMount
254 mount --type overlay overlay \
255 --options lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \
256 $tmpEtcMount
257
258 # Move the new temporary /etc mount underneath the current /etc mount.
259 #
260 # This should eventually use util-linux to perform this move beneath,
261 # however, this functionality is not yet in util-linux. See this
262 # tracking issue: https://github.com/util-linux/util-linux/issues/2604
263 ${pkgs.move-mount-beneath}/bin/move-mount --move --beneath $tmpEtcMount /etc
264
265 # Unmount the top /etc mount to atomically reveal the new mount.
266 umount /etc
267
268 fi
269 '' else ''
270 # Set up the statically computed bits of /etc.
271 echo "setting up /etc..."
272 ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc
273 '';
274
275 system.build.etcBasedir = pkgs.runCommandLocal "etc-lowerdir" { } ''
276 set -euo pipefail
277
278 makeEtcEntry() {
279 src="$1"
280 target="$2"
281
282 mkdir -p "$out/$(dirname "$target")"
283 cp "$src" "$out/$target"
284 }
285
286 mkdir -p "$out"
287 ${concatMapStringsSep "\n" (etcEntry: escapeShellArgs [
288 "makeEtcEntry"
289 # Force local source paths to be added to the store
290 "${etcEntry.source}"
291 etcEntry.target
292 ]) etcHardlinks}
293 '';
294
295 system.build.etcMetadataImage =
296 let
297 etcJson = pkgs.writeText "etc-json" (builtins.toJSON etc');
298 etcDump = pkgs.runCommand "etc-dump" { } "${build-composefs-dump} ${etcJson} > $out";
299 in
300 pkgs.runCommand "etc-metadata.erofs" {
301 nativeBuildInputs = [ pkgs.composefs pkgs.erofs-utils ];
302 } ''
303 mkcomposefs --from-file ${etcDump} $out
304 fsck.erofs $out
305 '';
306
307 };
308
309}