Skip to content

Commit bd1e5ce

Browse files
committed
Enable system activation scripts
refs #221
1 parent 3219a92 commit bd1e5ce

File tree

6 files changed

+314
-0
lines changed

6 files changed

+314
-0
lines changed

examples/example.nix

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,9 @@
102102
mode = "0755";
103103
};
104104
};
105+
106+
system.activationScripts.test = ''
107+
echo "This is a test activation script"
108+
'';
105109
};
106110
}

nix/modules/default.nix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@
228228
${system-manager}/bin/system-manager deactivate "$@"
229229
'';
230230

231+
systemActivationScript = pkgs.writeShellScript "systemActivationScript" config.system.activationScripts.script;
232+
231233
preActivationAssertionScript =
232234
let
233235
mkAssertion =
@@ -271,6 +273,7 @@
271273
exit 0
272274
fi
273275
'';
276+
274277
};
275278

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

nix/modules/upstream/nixpkgs/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
[
99
./nginx.nix
1010
./nix.nix
11+
./activation-script.nix
1112
]
1213
++
1314
# List of imported NixOS modules

src/activate.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
106106
};
107107
final_state.write_to_file(state_file)?;
108108

109+
log::info!("Running system activation script...");
110+
match run_system_activation_script(store_path) {
111+
Ok(status) if status.success() => {
112+
log::info!("System activation script executed successfully.");
113+
}
114+
Ok(status) => {
115+
log::error!("System activation script failed with status: {status}");
116+
}
117+
Err(e) => {
118+
log::error!("Error running system activation script: {e}");
119+
}
120+
}
121+
109122
if let Err(e) = tmp_result {
110123
return Err(e.into());
111124
}
@@ -218,6 +231,19 @@ fn run_preactivation_assertions(store_path: &StorePath) -> Result<process::ExitS
218231
Ok(status)
219232
}
220233

234+
fn run_system_activation_script(store_path: &StorePath) -> Result<process::ExitStatus> {
235+
let status = process::Command::new(
236+
store_path
237+
.store_path
238+
.join("bin")
239+
.join("systemActivationScript"),
240+
)
241+
.stderr(process::Stdio::inherit())
242+
.stdout(process::Stdio::inherit())
243+
.status()?;
244+
Ok(status)
245+
}
246+
221247
fn get_state_file() -> Result<PathBuf> {
222248
let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(STATE_FILE_NAME);
223249
DirBuilder::new()

test/nix/modules/default.nix

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ let
144144
trusted-users = [ "zimbatm" ];
145145
};
146146
};
147+
148+
system.activationScripts = {
149+
"system-manager" = {
150+
text = ''
151+
touch /tmp/file-created-by-system-activation-script
152+
'';
153+
};
154+
};
147155
};
148156
}
149157
)
@@ -232,6 +240,7 @@ forEachUbuntuImage "example" {
232240
vm.fail("test -f /etc/a/nested/example/foo3")
233241
vm.fail("test -f /etc/baz/bar/foo2")
234242
vm.succeed("test -f /etc/foo_new")
243+
vm.succeed("test -f /tmp/file-created-by-system-activation-script")
235244
236245
nix_trusted_users = vm.succeed("${hostPkgs.nix}/bin/nix config show trusted-users").strip()
237246
assert "zimbatm" in nix_trusted_users, f"Expected 'zimbatm' to be in trusted-users, got {nix_trusted_users}"

0 commit comments

Comments
 (0)