1# generate the script used to activate the configuration.
2{ config, lib, pkgs, ... }:
3
4with lib;
5
6let
7
8 addAttributeName = mapAttrs (a: v: v // {
9 text = ''
10 #### Activation script snippet ${a}:
11 _localstatus=0
12 ${v.text}
13
14 if (( _localstatus > 0 )); then
15 printf "Activation script snippet '%s' failed (%s)\n" "${a}" "$_localstatus"
16 fi
17 '';
18 });
19
20 systemActivationScript = set: onlyDry: let
21 set' = mapAttrs (_: v: if isString v then (noDepEntry v) // { supportsDryActivation = false; } else v) set;
22 withHeadlines = addAttributeName set';
23 # When building a dry activation script, this replaces all activation scripts
24 # that do not support dry mode with a comment that does nothing. Filtering these
25 # activation scripts out so they don't get generated into the dry activation script
26 # does not work because when an activation script that supports dry mode depends on
27 # an activation script that does not, the dependency cannot be resolved and the eval
28 # fails.
29 withDrySnippets = mapAttrs (a: v: if onlyDry && !v.supportsDryActivation then v // {
30 text = "#### Activation script snippet ${a} does not support dry activation.";
31 } else v) withHeadlines;
32 in
33 ''
34 #!${pkgs.runtimeShell}
35
36 systemConfig='@out@'
37
38 export PATH=/empty
39 for i in ${toString path}; do
40 PATH=$PATH:$i/bin:$i/sbin
41 done
42
43 _status=0
44 trap "_status=1 _localstatus=\$?" ERR
45
46 # Ensure a consistent umask.
47 umask 0022
48
49 ${textClosureMap id (withDrySnippets) (attrNames withDrySnippets)}
50
51 '' + optionalString (!onlyDry) ''
52 # Make this configuration the current configuration.
53 # The readlink is there to ensure that when $systemConfig = /system
54 # (which is a symlink to the store), /run/current-system is still
55 # used as a garbage collection root.
56 ln -sfn "$(readlink -f "$systemConfig")" /run/current-system
57
58 exit $_status
59 '';
60
61 path = with pkgs; map getBin
62 [ coreutils
63 gnugrep
64 findutils
65 getent
66 stdenv.cc.libc # nscd in update-users-groups.pl
67 shadow
68 nettools # needed for hostname
69 util-linux # needed for mount and mountpoint
70 ];
71
72 scriptType = withDry: with types;
73 let scriptOptions =
74 { deps = mkOption
75 { type = types.listOf types.str;
76 default = [ ];
77 description = "List of dependencies. The script will run after these.";
78 };
79 text = mkOption
80 { type = types.lines;
81 description = "The content of the script.";
82 };
83 } // optionalAttrs withDry {
84 supportsDryActivation = mkOption
85 { type = types.bool;
86 default = false;
87 description = ''
88 Whether this activation script supports being dry-activated.
89 These activation scripts will also be executed on dry-activate
90 activations with the environment variable
91 `NIXOS_ACTION` being set to `dry-activate`.
92 it's important that these activation scripts don't
93 modify anything about the system when the variable is set.
94 '';
95 };
96 };
97 in either str (submodule { options = scriptOptions; });
98
99in
100
101{
102
103 ###### interface
104
105 options = {
106
107 system.activationScripts = mkOption {
108 default = {};
109
110 example = literalExpression ''
111 { stdio.text =
112 '''
113 # Needed by some programs.
114 ln -sfn /proc/self/fd /dev/fd
115 ln -sfn /proc/self/fd/0 /dev/stdin
116 ln -sfn /proc/self/fd/1 /dev/stdout
117 ln -sfn /proc/self/fd/2 /dev/stderr
118 ''';
119 }
120 '';
121
122 description = ''
123 A set of shell script fragments that are executed when a NixOS
124 system configuration is activated. Examples are updating
125 /etc, creating accounts, and so on. Since these are executed
126 every time you boot the system or run
127 {command}`nixos-rebuild`, it's important that they are
128 idempotent and fast.
129 '';
130
131 type = types.attrsOf (scriptType true);
132 apply = set: set // {
133 script = systemActivationScript set false;
134 };
135 };
136
137 system.dryActivationScript = mkOption {
138 description = "The shell script that is to be run when dry-activating a system.";
139 readOnly = true;
140 internal = true;
141 default = systemActivationScript (removeAttrs config.system.activationScripts [ "script" ]) true;
142 defaultText = literalMD "generated activation script";
143 };
144
145 system.userActivationScripts = mkOption {
146 default = {};
147
148 example = literalExpression ''
149 { plasmaSetup = {
150 text = '''
151 ''${pkgs.libsForQt5.kservice}/bin/kbuildsycoca5"
152 ''';
153 deps = [];
154 };
155 }
156 '';
157
158 description = ''
159 A set of shell script fragments that are executed by a systemd user
160 service when a NixOS system configuration is activated. Examples are
161 rebuilding the .desktop file cache for showing applications in the menu.
162 Since these are executed every time you run
163 {command}`nixos-rebuild`, it's important that they are
164 idempotent and fast.
165 '';
166
167 type = with types; attrsOf (scriptType false);
168
169 apply = set: {
170 script = ''
171 unset PATH
172 for i in ${toString path}; do
173 PATH=$PATH:$i/bin:$i/sbin
174 done
175
176 _status=0
177 trap "_status=1 _localstatus=\$?" ERR
178
179 ${
180 let
181 set' = mapAttrs (n: v: if isString v then noDepEntry v else v) set;
182 withHeadlines = addAttributeName set';
183 in textClosureMap id (withHeadlines) (attrNames withHeadlines)
184 }
185
186 exit $_status
187 '';
188 };
189
190 };
191
192 environment.usrbinenv = mkOption {
193 default = "${pkgs.coreutils}/bin/env";
194 defaultText = literalExpression ''"''${pkgs.coreutils}/bin/env"'';
195 example = literalExpression ''"''${pkgs.busybox}/bin/env"'';
196 type = types.nullOr types.path;
197 visible = false;
198 description = ''
199 The env(1) executable that is linked system-wide to
200 `/usr/bin/env`.
201 '';
202 };
203
204 system.build.installBootLoader = mkOption {
205 internal = true;
206 # "; true" => make the `$out` argument from switch-to-configuration.pl
207 # go to `true` instead of `echo`, hiding the useless path
208 # from the log.
209 default = "echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2; true";
210 description = ''
211 A program that writes a bootloader installation script to the path passed in the first command line argument.
212
213 See `nixos/modules/system/activation/switch-to-configuration.pl`.
214 '';
215 type = types.unique {
216 message = ''
217 Only one bootloader can be enabled at a time. This requirement has not
218 been checked until NixOS 22.05. Earlier versions defaulted to the last
219 definition. Change your configuration to enable only one bootloader.
220 '';
221 } (types.either types.str types.package);
222 };
223
224 };
225
226
227 ###### implementation
228
229 config = {
230
231 system.activationScripts.stdio = ""; # obsolete
232 system.activationScripts.var = ""; # obsolete
233
234 systemd.tmpfiles.rules = [
235 # Prevent the current configuration from being garbage-collected.
236 "d /nix/var/nix/gcroots -"
237 "L+ /nix/var/nix/gcroots/current-system - - - - /run/current-system"
238 "D /var/empty 0555 root root -"
239 "h /var/empty - - - - +i"
240 ];
241
242 system.activationScripts.usrbinenv = if config.environment.usrbinenv != null
243 then ''
244 mkdir -p /usr/bin
245 chmod 0755 /usr/bin
246 ln -sfn ${config.environment.usrbinenv} /usr/bin/.env.tmp
247 mv /usr/bin/.env.tmp /usr/bin/env # atomically replace /usr/bin/env
248 ''
249 else ''
250 rm -f /usr/bin/env
251 rmdir --ignore-fail-on-non-empty /usr/bin /usr
252 '';
253
254 system.activationScripts.specialfs =
255 ''
256 specialMount() {
257 local device="$1"
258 local mountPoint="$2"
259 local options="$3"
260 local fsType="$4"
261
262 if mountpoint -q "$mountPoint"; then
263 local options="remount,$options"
264 else
265 mkdir -p "$mountPoint"
266 chmod 0755 "$mountPoint"
267 fi
268 mount -t "$fsType" -o "$options" "$device" "$mountPoint"
269 }
270 source ${config.system.build.earlyMountScript}
271 '';
272
273 systemd.user = {
274 services.nixos-activation = {
275 description = "Run user-specific NixOS activation";
276 script = config.system.userActivationScripts.script;
277 unitConfig.ConditionUser = "!@system";
278 serviceConfig.Type = "oneshot";
279 wantedBy = [ "default.target" ];
280 };
281 };
282 };
283
284}