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 # Prevent the current configuration from being garbage-collected.
59 ln -sfn /run/current-system /nix/var/nix/gcroots/current-system
60
61 exit $_status
62 '';
63
64 path = with pkgs; map getBin
65 [ coreutils
66 gnugrep
67 findutils
68 getent
69 stdenv.cc.libc # nscd in update-users-groups.pl
70 shadow
71 nettools # needed for hostname
72 util-linux # needed for mount and mountpoint
73 ];
74
75 scriptType = withDry: with types;
76 let scriptOptions =
77 { deps = mkOption
78 { type = types.listOf types.str;
79 default = [ ];
80 description = "List of dependencies. The script will run after these.";
81 };
82 text = mkOption
83 { type = types.lines;
84 description = "The content of the script.";
85 };
86 } // optionalAttrs withDry {
87 supportsDryActivation = mkOption
88 { type = types.bool;
89 default = false;
90 description = ''
91 Whether this activation script supports being dry-activated.
92 These activation scripts will also be executed on dry-activate
93 activations with the environment variable
94 <literal>NIXOS_ACTION</literal> being set to <literal>dry-activate
95 </literal>. it's important that these activation scripts don't
96 modify anything about the system when the variable is set.
97 '';
98 };
99 };
100 in either str (submodule { options = scriptOptions; });
101
102in
103
104{
105
106 ###### interface
107
108 options = {
109
110 system.activationScripts = mkOption {
111 default = {};
112
113 example = literalExpression ''
114 { stdio.text =
115 '''
116 # Needed by some programs.
117 ln -sfn /proc/self/fd /dev/fd
118 ln -sfn /proc/self/fd/0 /dev/stdin
119 ln -sfn /proc/self/fd/1 /dev/stdout
120 ln -sfn /proc/self/fd/2 /dev/stderr
121 ''';
122 }
123 '';
124
125 description = ''
126 A set of shell script fragments that are executed when a NixOS
127 system configuration is activated. Examples are updating
128 /etc, creating accounts, and so on. Since these are executed
129 every time you boot the system or run
130 <command>nixos-rebuild</command>, it's important that they are
131 idempotent and fast.
132 '';
133
134 type = types.attrsOf (scriptType true);
135 apply = set: set // {
136 script = systemActivationScript set false;
137 };
138 };
139
140 system.dryActivationScript = mkOption {
141 description = "The shell script that is to be run when dry-activating a system.";
142 readOnly = true;
143 internal = true;
144 default = systemActivationScript (removeAttrs config.system.activationScripts [ "script" ]) true;
145 };
146
147 system.userActivationScripts = mkOption {
148 default = {};
149
150 example = literalExpression ''
151 { plasmaSetup = {
152 text = '''
153 ${pkgs.libsForQt5.kservice}/bin/kbuildsycoca5"
154 ''';
155 deps = [];
156 };
157 }
158 '';
159
160 description = ''
161 A set of shell script fragments that are executed by a systemd user
162 service when a NixOS system configuration is activated. Examples are
163 rebuilding the .desktop file cache for showing applications in the menu.
164 Since these are executed every time you run
165 <command>nixos-rebuild</command>, it's important that they are
166 idempotent and fast.
167 '';
168
169 type = with types; attrsOf (scriptType false);
170
171 apply = set: {
172 script = ''
173 unset PATH
174 for i in ${toString path}; do
175 PATH=$PATH:$i/bin:$i/sbin
176 done
177
178 _status=0
179 trap "_status=1 _localstatus=\$?" ERR
180
181 ${
182 let
183 set' = mapAttrs (n: v: if isString v then noDepEntry v else v) set;
184 withHeadlines = addAttributeName set';
185 in textClosureMap id (withHeadlines) (attrNames withHeadlines)
186 }
187
188 exit $_status
189 '';
190 };
191
192 };
193
194 environment.usrbinenv = mkOption {
195 default = "${pkgs.coreutils}/bin/env";
196 defaultText = literalExpression ''"''${pkgs.coreutils}/bin/env"'';
197 example = literalExpression ''"''${pkgs.busybox}/bin/env"'';
198 type = types.nullOr types.path;
199 visible = false;
200 description = ''
201 The env(1) executable that is linked system-wide to
202 <literal>/usr/bin/env</literal>.
203 '';
204 };
205 };
206
207
208 ###### implementation
209
210 config = {
211
212 system.activationScripts.stdio = ""; # obsolete
213
214 system.activationScripts.var =
215 ''
216 # Various log/runtime directories.
217
218 mkdir -m 1777 -p /var/tmp
219
220 # Empty, immutable home directory of many system accounts.
221 mkdir -p /var/empty
222 # Make sure it's really empty
223 ${pkgs.e2fsprogs}/bin/chattr -f -i /var/empty || true
224 find /var/empty -mindepth 1 -delete
225 chmod 0555 /var/empty
226 chown root:root /var/empty
227 ${pkgs.e2fsprogs}/bin/chattr -f +i /var/empty || true
228 '';
229
230 system.activationScripts.usrbinenv = if config.environment.usrbinenv != null
231 then ''
232 mkdir -m 0755 -p /usr/bin
233 ln -sfn ${config.environment.usrbinenv} /usr/bin/.env.tmp
234 mv /usr/bin/.env.tmp /usr/bin/env # atomically replace /usr/bin/env
235 ''
236 else ''
237 rm -f /usr/bin/env
238 rmdir --ignore-fail-on-non-empty /usr/bin /usr
239 '';
240
241 system.activationScripts.specialfs =
242 ''
243 specialMount() {
244 local device="$1"
245 local mountPoint="$2"
246 local options="$3"
247 local fsType="$4"
248
249 if mountpoint -q "$mountPoint"; then
250 local options="remount,$options"
251 else
252 mkdir -m 0755 -p "$mountPoint"
253 fi
254 mount -t "$fsType" -o "$options" "$device" "$mountPoint"
255 }
256 source ${config.system.build.earlyMountScript}
257 '';
258
259 systemd.user = {
260 services.nixos-activation = {
261 description = "Run user-specific NixOS activation";
262 script = config.system.userActivationScripts.script;
263 unitConfig.ConditionUser = "!@system";
264 serviceConfig.Type = "oneshot";
265 wantedBy = [ "default.target" ];
266 };
267 };
268 };
269
270}