diff --git a/examples/example.nix b/examples/example.nix index 4a78aa30..270555c5 100644 --- a/examples/example.nix +++ b/examples/example.nix @@ -102,5 +102,9 @@ mode = "0755"; }; }; + + system.activationScripts.test = '' + echo "This is a test activation script" + ''; }; } diff --git a/nix/modules/default.nix b/nix/modules/default.nix index b1cc3dc9..4ab17ccb 100644 --- a/nix/modules/default.nix +++ b/nix/modules/default.nix @@ -228,6 +228,8 @@ ${system-manager}/bin/system-manager deactivate "$@" ''; + systemActivationScript = pkgs.writeShellScript "systemActivationScript" config.system.activationScripts.script; + preActivationAssertionScript = let mkAssertion = @@ -271,6 +273,7 @@ exit 0 fi ''; + }; # TODO: handle globbing diff --git a/nix/modules/upstream/nixpkgs/activation-script.nix b/nix/modules/upstream/nixpkgs/activation-script.nix new file mode 100644 index 00000000..3679b35b --- /dev/null +++ b/nix/modules/upstream/nixpkgs/activation-script.nix @@ -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); + }; + + }; +} diff --git a/nix/modules/upstream/nixpkgs/default.nix b/nix/modules/upstream/nixpkgs/default.nix index 3985e2f6..2f216809 100644 --- a/nix/modules/upstream/nixpkgs/default.nix +++ b/nix/modules/upstream/nixpkgs/default.nix @@ -8,6 +8,7 @@ [ ./nginx.nix ./nix.nix + ./activation-script.nix ] ++ # List of imported NixOS modules diff --git a/src/activate.rs b/src/activate.rs index 3df263d6..644d3bff 100644 --- a/src/activate.rs +++ b/src/activate.rs @@ -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()); } @@ -218,6 +231,19 @@ fn run_preactivation_assertions(store_path: &StorePath) -> Result Result { + 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 { let state_file = Path::new(SYSTEM_MANAGER_STATE_DIR).join(STATE_FILE_NAME); DirBuilder::new() diff --git a/test/nix/modules/default.nix b/test/nix/modules/default.nix index 2cc677f7..c754eb01 100644 --- a/test/nix/modules/default.nix +++ b/test/nix/modules/default.nix @@ -144,6 +144,14 @@ let trusted-users = [ "zimbatm" ]; }; }; + + system.activationScripts = { + "system-manager" = { + text = '' + touch /tmp/file-created-by-system-activation-script + ''; + }; + }; }; } ) @@ -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}"