python312Packages.google-auth-oauthlib: fix build in darwin sandbox (#373724)
[NixPkgs.git] / nixos / modules / system / etc / etc.nix
blob9e04467fd8024e6c453ff73a1a4c528bbe5d2be0
1 # Management of static files in /etc.
3   config,
4   lib,
5   pkgs,
6   ...
7 }:
8 let
10   etc' = lib.filter (f: f.enable) (lib.attrValues config.environment.etc);
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
21         makeEtcEntry() {
22           src="$1"
23           target="$2"
24           mode="$3"
25           user="$4"
26           group="$5"
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
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
45                 continue
46               fi
47             fi
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"
53             fi
54           fi
55         }
57         mkdir -p "$out/etc"
58         ${lib.concatMapStringsSep "\n" (
59           etcEntry:
60           lib.escapeShellArgs [
61             "makeEtcEntry"
62             # Force local source paths to be added to the store
63             "${etcEntry.source}"
64             etcEntry.target
65             etcEntry.mode
66             etcEntry.user
67             etcEntry.group
68           ]
69         ) etc'}
70       '';
72   etcHardlinks = lib.filter (f: f.mode != "symlink" && f.mode != "direct-symlink") etc';
78   imports = [ ../build.nix ];
80   ###### interface
82   options = {
84     system.etc.overlay = {
85       enable = lib.mkOption {
86         type = lib.types.bool;
87         default = false;
88         description = ''
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.
93         '';
94       };
96       mutable = lib.mkOption {
97         type = lib.types.bool;
98         default = true;
99         description = ''
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.
104         '';
105       };
106     };
108     environment.etc = lib.mkOption {
109       default = { };
110       example = lib.literalExpression ''
111         { example-configuration-file =
112             { source = "/nix/store/.../etc/dir/file.conf.example";
113               mode = "0440";
114             };
115           "default/useradd".text = "GROUP=100 ...";
116         }
117       '';
118       description = ''
119         Set of files that have to be linked in {file}`/etc`.
120       '';
122       type =
123         with lib.types;
124         attrsOf (
125           submodule (
126             {
127               name,
128               config,
129               options,
130               ...
131             }:
132             {
133               options = {
135                 enable = lib.mkOption {
136                   type = lib.types.bool;
137                   default = true;
138                   description = ''
139                     Whether this /etc file should be generated.  This
140                     option allows specific /etc files to be disabled.
141                   '';
142                 };
144                 target = lib.mkOption {
145                   type = lib.types.str;
146                   description = ''
147                     Name of symlink (relative to
148                     {file}`/etc`).  Defaults to the attribute
149                     name.
150                   '';
151                 };
153                 text = lib.mkOption {
154                   default = null;
155                   type = lib.types.nullOr lib.types.lines;
156                   description = "Text of the file.";
157                 };
159                 source = lib.mkOption {
160                   type = lib.types.path;
161                   description = "Path of the source file.";
162                 };
164                 mode = lib.mkOption {
165                   type = lib.types.str;
166                   default = "symlink";
167                   example = "0600";
168                   description = ''
169                     If set to something else than `symlink`,
170                     the file is copied instead of symlinked, with the given
171                     file mode.
172                   '';
173                 };
175                 uid = lib.mkOption {
176                   default = 0;
177                   type = lib.types.int;
178                   description = ''
179                     UID of created file. Only takes effect when the file is
180                     copied (that is, the mode is not 'symlink').
181                   '';
182                 };
184                 gid = lib.mkOption {
185                   default = 0;
186                   type = lib.types.int;
187                   description = ''
188                     GID of created file. Only takes effect when the file is
189                     copied (that is, the mode is not 'symlink').
190                   '';
191                 };
193                 user = lib.mkOption {
194                   default = "+${toString config.uid}";
195                   type = lib.types.str;
196                   description = ''
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`.
200                   '';
201                 };
203                 group = lib.mkOption {
204                   default = "+${toString config.gid}";
205                   type = lib.types.str;
206                   description = ''
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`.
210                   '';
211                 };
213               };
215               config = {
216                 target = lib.mkDefault name;
217                 source = lib.mkIf (config.text != null) (
218                   let
219                     name' = "etc-" + lib.replaceStrings [ "/" ] [ "-" ] name;
220                   in
221                   lib.mkDerivedConfig options.text (pkgs.writeText name')
222                 );
223               };
225             }
226           )
227         );
229     };
231   };
233   ###### implementation
235   config = {
237     system.build.etc = etc;
238     system.build.etcActivationCommands =
239       let
240         etcOverlayOptions = lib.concatStringsSep "," (
241           [
242             "relatime"
243             "redirect_dir=on"
244             "metacopy=on"
245           ]
246           ++ lib.optionals config.system.etc.overlay.mutable [
247             "upperdir=/.rw-etc/upper"
248             "workdir=/.rw-etc/work"
249           ]
250         );
251       in
252       if config.system.etc.overlay.enable then
253         #bash
254         ''
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.
261           #
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
265           # changed.
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
275             ''}
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} \
285                 /etc
286             else
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} \
294                 $tmpEtcMount
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
300                   continue
301                 fi
303                 tmpMountPoint="$tmpEtcMount/''${mountPoint:5}"
304                   ${
305                     if config.system.etc.overlay.mutable then
306                       ''
307                         if [[ -f "$mountPoint" ]]; then
308                           touch "$tmpMountPoint"
309                         elif [[ -d "$mountPoint" ]]; then
310                           mkdir -p "$tmpMountPoint"
311                         fi
312                       ''
313                     else
314                       ''
315                         if [[ ! -e "$tmpMountPoint" ]]; then
316                           echo "Skipping undeclared mountpoint in environment.etc: $mountPoint"
317                           continue
318                         fi
319                       ''
320                   }
321                 mount --bind "$mountPoint" "$tmpMountPoint"
322               done
324               # Move the new temporary /etc mount underneath the current /etc mount.
325               #
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"
336               rmdir "$tmpEtcMount"
337             fi
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"
347                 rmdir "$mountPoint"
348               fi
349             done
350           fi
351         ''
352       else
353         ''
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
357         '';
359     system.build.etcBasedir = pkgs.runCommandLocal "etc-lowerdir" { } ''
360       set -euo pipefail
362       makeEtcEntry() {
363         src="$1"
364         target="$2"
366         mkdir -p "$out/$(dirname "$target")"
367         cp "$src" "$out/$target"
368       }
370       mkdir -p "$out"
371       ${lib.concatMapStringsSep "\n" (
372         etcEntry:
373         lib.escapeShellArgs [
374           "makeEtcEntry"
375           # Force local source paths to be added to the store
376           "${etcEntry.source}"
377           etcEntry.target
378         ]
379       ) etcHardlinks}
380     '';
382     system.build.etcMetadataImage =
383       let
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
387         '';
388       in
389       pkgs.runCommand "etc-metadata.erofs"
390         {
391           nativeBuildInputs = with pkgs.buildPackages; [
392             composefs
393             erofs-utils
394           ];
395         }
396         ''
397           mkcomposefs --from-file ${etcDump} $out
398           fsck.erofs $out
399         '';
401   };