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