Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/example.nix
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,9 @@
mode = "0755";
};
};

system.activationScripts.test = ''
echo "This is a test activation script"
'';
};
}
3 changes: 3 additions & 0 deletions nix/modules/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@
${system-manager}/bin/system-manager deactivate "$@"
'';

systemActivationScript = pkgs.writeShellScript "systemActivationScript" config.system.activationScripts.script;

preActivationAssertionScript =
let
mkAssertion =
Expand Down Expand Up @@ -271,6 +273,7 @@
exit 0
fi
'';

};

# TODO: handle globbing
Expand Down
271 changes: 271 additions & 0 deletions nix/modules/upstream/nixpkgs/activation-script.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
# copied from modules/system/activation/activation-script.nix to avoid the dependency on systemd.user
{
config,
lib,
pkgs,
nixosModulesPath,
...
}:

with lib;

let

addAttributeName = mapAttrs (
a: v:
v
// {
text = ''
#### Activation script snippet ${a}:
_localstatus=0
${v.text}

if (( _localstatus > 0 )); then
printf "Activation script snippet '%s' failed (%s)\n" "${a}" "$_localstatus"
fi
'';
}
);

systemActivationScript =
set: onlyDry:
let
set' = mapAttrs (
_: v: if isString v then (noDepEntry v) // { supportsDryActivation = false; } else v
) set;
withHeadlines = addAttributeName set';
# When building a dry activation script, this replaces all activation scripts
# that do not support dry mode with a comment that does nothing. Filtering these
# activation scripts out so they don't get generated into the dry activation script
# does not work because when an activation script that supports dry mode depends on
# an activation script that does not, the dependency cannot be resolved and the eval
# fails.
withDrySnippets = mapAttrs (
a: v:
if onlyDry && !v.supportsDryActivation then
v
// {
text = "#### Activation script snippet ${a} does not support dry activation.";
}
else
v
) withHeadlines;
in
''
#!${pkgs.runtimeShell}

source ${nixosModulesPath}/system/activation/lib/lib.sh

systemConfig='@out@'

export PATH=/empty
for i in ${toString path}; do
PATH=$PATH:$i/bin:$i/sbin
done

_status=0
trap "_status=1 _localstatus=\$?" ERR

# Ensure a consistent umask.
umask 0022

${textClosureMap id (withDrySnippets) (attrNames withDrySnippets)}

''
+ optionalString (!onlyDry) ''
# Make this configuration the current configuration.
# The readlink is there to ensure that when $systemConfig = /system
# (which is a symlink to the store), /run/current-system is still
# used as a garbage collection root.
ln -sfn "$(readlink -f "$systemConfig")" /run/current-system

exit $_status
'';

path =
with pkgs;
map getBin [
coreutils
gnugrep
findutils
getent
stdenv.cc.libc # nscd in update-users-groups.pl
shadow
util-linux # needed for mount and mountpoint
];

scriptType =
withDry:
with types;
let
scriptOptions =
{
deps = mkOption {
type = types.listOf types.str;
default = [ ];
description = "List of dependencies. The script will run after these.";
};
text = mkOption {
type = types.lines;
description = "The content of the script.";
};
}
// optionalAttrs withDry {
supportsDryActivation = mkOption {
type = types.bool;
default = false;
description = ''
Whether this activation script supports being dry-activated.
These activation scripts will also be executed on dry-activate
activations with the environment variable
`NIXOS_ACTION` being set to `dry-activate`.
it's important that these activation scripts don't
modify anything about the system when the variable is set.
'';
};
};
in
either str (submodule {
options = scriptOptions;
});

in

{

###### interface

options = {

system.activationScripts = mkOption {
default = { };

example = literalExpression ''
{
stdio = {
# Run after /dev has been mounted
deps = [ "specialfs" ];
text =
'''
# Needed by some programs.
ln -sfn /proc/self/fd /dev/fd
ln -sfn /proc/self/fd/0 /dev/stdin
ln -sfn /proc/self/fd/1 /dev/stdout
ln -sfn /proc/self/fd/2 /dev/stderr
''';
};
}
'';

description = ''
A set of shell script fragments that are executed when a NixOS
system configuration is activated. Examples are updating
/etc, creating accounts, and so on. Since these are executed
every time you boot the system or run
{command}`nixos-rebuild`, it's important that they are
idempotent and fast.
'';

type = types.attrsOf (scriptType true);
apply =
set:
set
// {
script = systemActivationScript set false;
};
};

system.dryActivationScript = mkOption {
description = "The shell script that is to be run when dry-activating a system.";
readOnly = true;
internal = true;
default = systemActivationScript (removeAttrs config.system.activationScripts [ "script" ]) true;
defaultText = literalMD "generated activation script";
};

system.userActivationScripts = mkOption {
default = { };

example = literalExpression ''
{ plasmaSetup = {
text = '''
''${pkgs.libsForQt5.kservice}/bin/kbuildsycoca5"
''';
deps = [];
};
}
'';

description = ''
A set of shell script fragments that are executed by a systemd user
service when a NixOS system configuration is activated. Examples are
rebuilding the .desktop file cache for showing applications in the menu.
Since these are executed every time you run
{command}`nixos-rebuild`, it's important that they are
idempotent and fast.
'';

type = with types; attrsOf (scriptType false);

apply = set: {
script = ''
export PATH=
for i in ${toString path}; do
PATH=$PATH:$i/bin:$i/sbin
done

_status=0
trap "_status=1 _localstatus=\$?" ERR

${
let
set' = mapAttrs (n: v: if isString v then noDepEntry v else v) set;
withHeadlines = addAttributeName set';
in
textClosureMap id (withHeadlines) (attrNames withHeadlines)
}

exit $_status
'';
};

};

environment.usrbinenv = mkOption {
default = "${pkgs.coreutils}/bin/env";
defaultText = literalExpression ''"''${pkgs.coreutils}/bin/env"'';
example = literalExpression ''"''${pkgs.busybox}/bin/env"'';
type = types.nullOr types.path;
visible = false;
description = ''
The {manpage}`env(1)` executable that is linked system-wide to
`/usr/bin/env`.
'';
};

system.build.installBootLoader = mkOption {
internal = true;
default = pkgs.writeShellScript "no-bootloader" ''
echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2
'';
defaultText = lib.literalExpression ''
pkgs.writeShellScript "no-bootloader" '''
echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2
'''
'';
description = ''
A program that writes a bootloader installation script to the path passed in the first command line argument.

See `pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs`.
'';
type = types.unique {
message = ''
Only one bootloader can be enabled at a time. This requirement has not
been checked until NixOS 22.05. Earlier versions defaulted to the last
definition. Change your configuration to enable only one bootloader.
'';
} (types.either types.str types.package);
};

};
}
1 change: 1 addition & 0 deletions nix/modules/upstream/nixpkgs/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
[
./nginx.nix
./nix.nix
./activation-script.nix
]
++
# List of imported NixOS modules
Expand Down
26 changes: 26 additions & 0 deletions src/activate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
};
final_state.write_to_file(state_file)?;

log::info!("Running system activation script...");
match run_system_activation_script(store_path) {
Ok(status) if status.success() => {
log::info!("System activation script executed successfully.");
}
Ok(status) => {
log::error!("System activation script failed with status: {status}");
}
Err(e) => {
log::error!("Error running system activation script: {e}");
}
}

if let Err(e) = tmp_result {
return Err(e.into());
}
Expand Down Expand Up @@ -218,6 +231,19 @@ fn run_preactivation_assertions(store_path: &StorePath) -> Result<process::ExitS
Ok(status)
}

fn run_system_activation_script(store_path: &StorePath) -> Result<process::ExitStatus> {
let status = process::Command::new(
store_path
.store_path
.join("bin")
.join("systemActivationScript"),
)
.stderr(process::Stdio::inherit())
.stdout(process::Stdio::inherit())
.status()?;
Ok(status)
}

fn get_state_file() -> Result<PathBuf> {
let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(STATE_FILE_NAME);
DirBuilder::new()
Expand Down
9 changes: 9 additions & 0 deletions test/nix/modules/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ let
trusted-users = [ "zimbatm" ];
};
};

system.activationScripts = {
"system-manager" = {
text = ''
touch /tmp/file-created-by-system-activation-script
'';
};
};
};
}
)
Expand Down Expand Up @@ -232,6 +240,7 @@ forEachUbuntuImage "example" {
vm.fail("test -f /etc/a/nested/example/foo3")
vm.fail("test -f /etc/baz/bar/foo2")
vm.succeed("test -f /etc/foo_new")
vm.succeed("test -f /tmp/file-created-by-system-activation-script")

nix_trusted_users = vm.succeed("${hostPkgs.nix}/bin/nix config show trusted-users").strip()
assert "zimbatm" in nix_trusted_users, f"Expected 'zimbatm' to be in trusted-users, got {nix_trusted_users}"
Expand Down