1 # Management of static files in /etc.
10 etc' = lib.filter (f: f.enable) (lib.attrValues config.environment.etc);
13 pkgs.runCommandLocal "etc"
15 # This is needed for the systemd module
16 passthru.targets = map (x: x.target) etc';
28 if [[ "$src" = *'*'* ]]; then
29 # If the source name contains '*', perform globbing.
30 mkdir -p "$out/etc/$target"
32 ln -s "$fn" "$out/etc/$target/"
36 mkdir -p "$out/etc/$(dirname "$target")"
37 if ! [ -e "$out/etc/$target" ]; then
38 ln -s "$src" "$out/etc/$target"
40 echo "duplicate entry $target -> $src"
41 if [ "$(readlink "$out/etc/$target")" != "$src" ]; then
42 echo "mismatched duplicate entry $(readlink "$out/etc/$target") <-> $src"
49 if [ "$mode" != symlink ]; then
50 echo "$mode" > "$out/etc/$target.mode"
51 echo "$user" > "$out/etc/$target.uid"
52 echo "$group" > "$out/etc/$target.gid"
58 ${lib.concatMapStringsSep "\n" (
62 # Force local source paths to be added to the store
72 etcHardlinks = lib.filter (f: f.mode != "symlink" && f.mode != "direct-symlink") etc';
78 imports = [ ../build.nix ];
84 system.etc.overlay = {
85 enable = lib.mkOption {
86 type = lib.types.bool;
89 Mount `/etc` as an overlayfs instead of generating it via a perl script.
91 Note: This is currently experimental. Only enable this option if you're
92 confident that you can recover your system if it breaks.
96 mutable = lib.mkOption {
97 type = lib.types.bool;
100 Whether to mount `/etc` mutably (i.e. read-write) or immutably (i.e. read-only).
102 If this is false, only the immutable lowerdir is mounted. If it is
103 true, a writable upperdir is mounted on top.
108 environment.etc = lib.mkOption {
110 example = lib.literalExpression ''
111 { example-configuration-file =
112 { source = "/nix/store/.../etc/dir/file.conf.example";
115 "default/useradd".text = "GROUP=100 ...";
119 Set of files that have to be linked in {file}`/etc`.
135 enable = lib.mkOption {
136 type = lib.types.bool;
139 Whether this /etc file should be generated. This
140 option allows specific /etc files to be disabled.
144 target = lib.mkOption {
145 type = lib.types.str;
147 Name of symlink (relative to
148 {file}`/etc`). Defaults to the attribute
153 text = lib.mkOption {
155 type = lib.types.nullOr lib.types.lines;
156 description = "Text of the file.";
159 source = lib.mkOption {
160 type = lib.types.path;
161 description = "Path of the source file.";
164 mode = lib.mkOption {
165 type = lib.types.str;
169 If set to something else than `symlink`,
170 the file is copied instead of symlinked, with the given
177 type = lib.types.int;
179 UID of created file. Only takes effect when the file is
180 copied (that is, the mode is not 'symlink').
186 type = lib.types.int;
188 GID of created file. Only takes effect when the file is
189 copied (that is, the mode is not 'symlink').
193 user = lib.mkOption {
194 default = "+${toString config.uid}";
195 type = lib.types.str;
197 User name of created file.
198 Only takes effect when the file is copied (that is, the mode is not 'symlink').
199 Changing this option takes precedence over `uid`.
203 group = lib.mkOption {
204 default = "+${toString config.gid}";
205 type = lib.types.str;
207 Group name of created file.
208 Only takes effect when the file is copied (that is, the mode is not 'symlink').
209 Changing this option takes precedence over `gid`.
216 target = lib.mkDefault name;
217 source = lib.mkIf (config.text != null) (
219 name' = "etc-" + lib.replaceStrings [ "/" ] [ "-" ] name;
221 lib.mkDerivedConfig options.text (pkgs.writeText name')
233 ###### implementation
237 system.build.etc = etc;
238 system.build.etcActivationCommands =
240 etcOverlayOptions = lib.concatStringsSep "," (
246 ++ lib.optionals config.system.etc.overlay.mutable [
247 "upperdir=/.rw-etc/upper"
248 "workdir=/.rw-etc/work"
252 if config.system.etc.overlay.enable then
255 # This script atomically remounts /etc when switching configuration.
256 # On a (re-)boot this should not run because /etc is mounted via a
257 # systemd mount unit instead.
258 # The activation script can also be called in cases where we didn't have
259 # an initrd though, like for instance when using nixos-enter,
260 # so we cannot assume that /etc has already been mounted.
262 # To a large extent this mimics what composefs does. Because
263 # it's relatively simple, however, we avoid the composefs dependency.
264 # Since this script is not idempotent, it should not run when etc hasn't
266 if [[ ! $IN_NIXOS_SYSTEMD_STAGE1 ]] && [[ "${config.system.build.etc}/etc" != "$(readlink -f /run/current-system/etc)" ]]; then
267 echo "remounting /etc..."
269 ${lib.optionalString config.system.etc.overlay.mutable ''
270 # These directories are usually created in initrd,
271 # but we need to create them here when we're called directly,
272 # for instance by nixos-enter
273 mkdir --parents /.rw-etc/upper /.rw-etc/work
274 chmod 0755 /.rw-etc /.rw-etc/upper /.rw-etc/work
277 tmpMetadataMount=$(TMPDIR="/run" mktemp --directory -t nixos-etc-metadata.XXXXXXXXXX)
278 mount --type erofs -o ro ${config.system.build.etcMetadataImage} $tmpMetadataMount
280 # There was no previous /etc mounted. This happens when we're called
281 # directly without an initrd, like with nixos-enter.
282 if ! mountpoint -q /etc; then
283 mount --type overlay overlay \
284 --options lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \
287 # Mount the new /etc overlay to a temporary private mount.
288 # This needs the indirection via a private bind mount because you
289 # cannot move shared mounts.
290 tmpEtcMount=$(TMPDIR="/run" mktemp --directory -t nixos-etc.XXXXXXXXXX)
291 mount --bind --make-private $tmpEtcMount $tmpEtcMount
292 mount --type overlay overlay \
293 --options lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \
296 # Before moving the new /etc overlay under the old /etc, we have to
297 # move mounts on top of /etc to the new /etc mountpoint.
298 findmnt /etc --submounts --list --noheading --kernel --output TARGET | while read -r mountPoint; do
299 if [[ "$mountPoint" = "/etc" ]]; then
303 tmpMountPoint="$tmpEtcMount/''${mountPoint:5}"
305 if config.system.etc.overlay.mutable then
307 if [[ -f "$mountPoint" ]]; then
308 touch "$tmpMountPoint"
309 elif [[ -d "$mountPoint" ]]; then
310 mkdir -p "$tmpMountPoint"
315 if [[ ! -e "$tmpMountPoint" ]]; then
316 echo "Skipping undeclared mountpoint in environment.etc: $mountPoint"
321 mount --bind "$mountPoint" "$tmpMountPoint"
324 # Move the new temporary /etc mount underneath the current /etc mount.
326 # This should eventually use util-linux to perform this move beneath,
327 # however, this functionality is not yet in util-linux. See this
328 # tracking issue: https://github.com/util-linux/util-linux/issues/2604
329 ${pkgs.move-mount-beneath}/bin/move-mount --move --beneath $tmpEtcMount /etc
331 # Unmount the top /etc mount to atomically reveal the new mount.
332 umount --lazy --recursive /etc
334 # Unmount the temporary mount
335 umount --lazy "$tmpEtcMount"
339 # Unmount old metadata mounts
340 # For some reason, `findmnt /tmp --submounts` does not show the nested
341 # mounts. So we'll just find all mounts of type erofs and filter on the
342 # name of the mountpoint.
343 findmnt --type erofs --list --kernel --output TARGET | while read -r mountPoint; do
344 if [[ ("$mountPoint" =~ ^/run/nixos-etc-metadata\..{10}$ || "$mountPoint" =~ ^/run/nixos-etc-metadata$ ) &&
345 "$mountPoint" != "$tmpMetadataMount" ]]; then
346 umount --lazy "$mountPoint"
354 # Set up the statically computed bits of /etc.
355 echo "setting up /etc..."
356 ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc
359 system.build.etcBasedir = pkgs.runCommandLocal "etc-lowerdir" { } ''
366 mkdir -p "$out/$(dirname "$target")"
367 cp "$src" "$out/$target"
371 ${lib.concatMapStringsSep "\n" (
373 lib.escapeShellArgs [
375 # Force local source paths to be added to the store
382 system.build.etcMetadataImage =
384 etcJson = pkgs.writeText "etc-json" (builtins.toJSON etc');
385 etcDump = pkgs.runCommand "etc-dump" { } ''
386 ${lib.getExe pkgs.buildPackages.python3} ${./build-composefs-dump.py} ${etcJson} > $out
389 pkgs.runCommand "etc-metadata.erofs"
391 nativeBuildInputs = with pkgs.buildPackages; [
397 mkcomposefs --from-file ${etcDump} $out