summary refs log tree commit diff
diff options
context:
space:
mode:
authorRobert Hensing <roberth@users.noreply.github.com>2023-07-08 16:06:25 +0200
committerGitHub <noreply@github.com>2023-07-08 16:06:25 +0200
commit3fd4ac8e82f62e55715be77a30a8b6cfa2e3be55 (patch)
tree0a5b3ec92b8d6f402f7b6b581682bdd4005fb9b4
parentbaecfdc0794b045bb568e2b64f84a85ae0788ec6 (diff)
parent772d6076e825dc999a04245f7dfa3cb19082ec28 (diff)
downloadnixpkgs-3fd4ac8e82f62e55715be77a30a8b6cfa2e3be55.tar
nixpkgs-3fd4ac8e82f62e55715be77a30a8b6cfa2e3be55.tar.gz
nixpkgs-3fd4ac8e82f62e55715be77a30a8b6cfa2e3be55.tar.bz2
nixpkgs-3fd4ac8e82f62e55715be77a30a8b6cfa2e3be55.tar.lz
nixpkgs-3fd4ac8e82f62e55715be77a30a8b6cfa2e3be55.tar.xz
nixpkgs-3fd4ac8e82f62e55715be77a30a8b6cfa2e3be55.tar.zst
nixpkgs-3fd4ac8e82f62e55715be77a30a8b6cfa2e3be55.zip
Merge pull request #237040 from roberth/flexible-activation
nixos/system: Support pre-activated images
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/system/activation/activatable-system.nix92
-rw-r--r--nixos/modules/system/activation/activation-script.nix21
-rwxr-xr-xnixos/modules/system/activation/switch-to-configuration.pl34
-rw-r--r--nixos/modules/system/activation/top-level.nix62
-rw-r--r--nixos/tests/switch-test.nix47
6 files changed, 183 insertions, 74 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index c2f51f4d10a..aa8c2e67073 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1340,6 +1340,7 @@
   ./services/x11/xbanish.nix
   ./services/x11/xfs.nix
   ./services/x11/xserver.nix
+  ./system/activation/activatable-system.nix
   ./system/activation/activation-script.nix
   ./system/activation/specialisation.nix
   ./system/activation/bootspec.nix
diff --git a/nixos/modules/system/activation/activatable-system.nix b/nixos/modules/system/activation/activatable-system.nix
new file mode 100644
index 00000000000..7f6154794bd
--- /dev/null
+++ b/nixos/modules/system/activation/activatable-system.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    mkOption
+    optionalString
+    types
+    ;
+
+  perlWrapped = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
+
+  systemBuilderArgs = {
+    activationScript = config.system.activationScripts.script;
+    dryActivationScript = config.system.dryActivationScript;
+  };
+
+  systemBuilderCommands = ''
+    echo "$activationScript" > $out/activate
+    echo "$dryActivationScript" > $out/dry-activate
+    substituteInPlace $out/activate --subst-var-by out ''${!toplevelVar}
+    substituteInPlace $out/dry-activate --subst-var-by out ''${!toplevelVar}
+    chmod u+x $out/activate $out/dry-activate
+    unset activationScript dryActivationScript
+
+    mkdir $out/bin
+    substitute ${./switch-to-configuration.pl} $out/bin/switch-to-configuration \
+      --subst-var out \
+      --subst-var-by toplevel ''${!toplevelVar} \
+      --subst-var-by coreutils "${pkgs.coreutils}" \
+      --subst-var-by distroId ${lib.escapeShellArg config.system.nixos.distroId} \
+      --subst-var-by installBootLoader ${lib.escapeShellArg config.system.build.installBootLoader} \
+      --subst-var-by localeArchive "${config.i18n.glibcLocales}/lib/locale/locale-archive" \
+      --subst-var-by perl "${perlWrapped}" \
+      --subst-var-by shell "${pkgs.bash}/bin/sh" \
+      --subst-var-by su "${pkgs.shadow.su}/bin/su" \
+      --subst-var-by systemd "${config.systemd.package}" \
+      --subst-var-by utillinux "${pkgs.util-linux}" \
+      ;
+
+    chmod +x $out/bin/switch-to-configuration
+    ${optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
+      if ! output=$(${perlWrapped}/bin/perl -c $out/bin/switch-to-configuration 2>&1); then
+        echo "switch-to-configuration syntax is not valid:"
+        echo "$output"
+        exit 1
+      fi
+    ''}
+  '';
+
+in
+{
+  options = {
+    system.activatable = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to add the activation script to the system profile.
+
+        The default, to have the script available all the time, is what we normally
+        do, but for image based systems, this may not be needed or not be desirable.
+      '';
+    };
+    system.build.separateActivationScript = mkOption {
+      type = types.package;
+      description = ''
+        A separate activation script package that's not part of the system profile.
+
+        This is useful for configurations where `system.activatable` is `false`.
+        Otherwise, you can just use `system.build.toplevel`.
+      '';
+    };
+  };
+  config = {
+    system.systemBuilderCommands = lib.mkIf config.system.activatable systemBuilderCommands;
+    system.systemBuilderArgs = lib.mkIf config.system.activatable
+      (systemBuilderArgs // {
+        toplevelVar = "out";
+      });
+
+    system.build.separateActivationScript =
+      pkgs.runCommand
+        "separate-activation-script"
+        (systemBuilderArgs // {
+          toplevelVar = "toplevel";
+          toplevel = config.system.build.toplevel;
+        })
+        ''
+          mkdir $out
+          ${systemBuilderCommands}
+        '';
+  };
+}
diff --git a/nixos/modules/system/activation/activation-script.nix b/nixos/modules/system/activation/activation-script.nix
index f23d4809e35..c8407dd6779 100644
--- a/nixos/modules/system/activation/activation-script.nix
+++ b/nixos/modules/system/activation/activation-script.nix
@@ -204,6 +204,27 @@ in
         `/usr/bin/env`.
       '';
     };
+
+    system.build.installBootLoader = mkOption {
+      internal = true;
+      # "; true" => make the `$out` argument from switch-to-configuration.pl
+      #             go to `true` instead of `echo`, hiding the useless path
+      #             from the log.
+      default = "echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2; true";
+      description = lib.mdDoc ''
+        A program that writes a bootloader installation script to the path passed in the first command line argument.
+
+        See `nixos/modules/system/activation/switch-to-configuration.pl`.
+      '';
+      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/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index a77a6ed52eb..cfad6403986 100755
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -31,8 +31,10 @@ use Cwd qw(abs_path);
 ## no critic(ValuesAndExpressions::ProhibitNoisyQuotes, ValuesAndExpressions::ProhibitMagicNumbers, ValuesAndExpressions::ProhibitEmptyQuotes, ValuesAndExpressions::ProhibitInterpolationOfLiterals)
 ## no critic(RegularExpressions::ProhibitEscapedMetacharacters)
 
-# System closure path to switch to
+# Location of activation scripts
 my $out = "@out@";
+# System closure path to switch to
+my $toplevel = "@toplevel@";
 # Path to the directory containing systemd tools of the old system
 my $cur_systemd = abs_path("/run/current-system/sw/bin");
 # Path to the systemd store path of the new system
@@ -96,7 +98,7 @@ if ($action eq "switch" || $action eq "boot") {
     chomp(my $install_boot_loader = <<'EOFBOOTLOADER');
 @installBootLoader@
 EOFBOOTLOADER
-    system("$install_boot_loader $out") == 0 or exit 1;
+    system("$install_boot_loader $toplevel") == 0 or exit 1;
 }
 
 # Just in case the new configuration hangs the system, do a sync now.
@@ -110,7 +112,7 @@ if ($action eq "boot") {
 
 # Check if we can activate the new configuration.
 my $cur_init_interface_version = read_file("/run/current-system/init-interface-version", err_mode => "quiet") // "";
-my $new_init_interface_version = read_file("$out/init-interface-version");
+my $new_init_interface_version = read_file("$toplevel/init-interface-version");
 
 if ($new_init_interface_version ne $cur_init_interface_version) {
     print STDERR <<'EOF';
@@ -477,7 +479,7 @@ sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutin
                             $units_to_stop->{$socket} = 1;
                             # Only restart sockets that actually
                             # exist in new configuration:
-                            if (-e "$out/etc/systemd/system/$socket") {
+                            if (-e "$toplevel/etc/systemd/system/$socket") {
                                 $units_to_start->{$socket} = 1;
                                 if ($units_to_start eq $units_to_restart) {
                                     record_unit($restart_list_file, $socket);
@@ -539,13 +541,13 @@ while (my ($unit, $state) = each(%{$active_cur})) {
     my $base_unit = $unit;
 
     my $cur_unit_file = "/etc/systemd/system/$base_unit";
-    my $new_unit_file = "$out/etc/systemd/system/$base_unit";
+    my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
 
     # Detect template instances.
     if (!-e $cur_unit_file && !-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
       $base_unit = "$1\@.$2";
       $cur_unit_file = "/etc/systemd/system/$base_unit";
-      $new_unit_file = "$out/etc/systemd/system/$base_unit";
+      $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
     }
 
     my $base_name = $base_unit;
@@ -626,7 +628,7 @@ sub path_to_unit_name {
 # we generated units for all mounts; then we could unify this with the
 # unit checking code above.
 my ($cur_fss, $cur_swaps) = parse_fstab("/etc/fstab");
-my ($new_fss, $new_swaps) = parse_fstab("$out/etc/fstab");
+my ($new_fss, $new_swaps) = parse_fstab("$toplevel/etc/fstab");
 foreach my $mount_point (keys(%{$cur_fss})) {
     my $cur = $cur_fss->{$mount_point};
     my $new = $new_fss->{$mount_point};
@@ -670,7 +672,7 @@ foreach my $device (keys(%{$cur_swaps})) {
 my $cur_pid1_path = abs_path("/proc/1/exe") // "/unknown";
 my $cur_systemd_system_config = abs_path("/etc/systemd/system.conf") // "/unknown";
 my $new_pid1_path = abs_path("$new_systemd/lib/systemd/systemd") or die;
-my $new_systemd_system_config = abs_path("$out/etc/systemd/system.conf") // "/unknown";
+my $new_systemd_system_config = abs_path("$toplevel/etc/systemd/system.conf") // "/unknown";
 
 my $restart_systemd = $cur_pid1_path ne $new_pid1_path;
 if ($cur_systemd_system_config ne $new_systemd_system_config) {
@@ -709,12 +711,12 @@ if ($action eq "dry-activate") {
     foreach (split(/\n/msx, read_file($dry_restart_by_activation_file, err_mode => "quiet") // "")) {
         my $unit = $_;
         my $base_unit = $unit;
-        my $new_unit_file = "$out/etc/systemd/system/$base_unit";
+        my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
 
         # Detect template instances.
         if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
           $base_unit = "$1\@.$2";
-          $new_unit_file = "$out/etc/systemd/system/$base_unit";
+          $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
         }
 
         my $base_name = $base_unit;
@@ -757,7 +759,7 @@ if ($action eq "dry-activate") {
 }
 
 
-syslog(LOG_NOTICE, "switching to system configuration $out");
+syslog(LOG_NOTICE, "switching to system configuration $toplevel");
 
 if (scalar(keys(%units_to_stop)) > 0) {
     if (scalar(@units_to_stop_filtered)) {
@@ -781,12 +783,12 @@ system("$out/activate", "$out") == 0 or $res = 2;
 foreach (split(/\n/msx, read_file($restart_by_activation_file, err_mode => "quiet") // "")) {
     my $unit = $_;
     my $base_unit = $unit;
-    my $new_unit_file = "$out/etc/systemd/system/$base_unit";
+    my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
 
     # Detect template instances.
     if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
       $base_unit = "$1\@.$2";
-      $new_unit_file = "$out/etc/systemd/system/$base_unit";
+      $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
     }
 
     my $base_name = $base_unit;
@@ -857,7 +859,7 @@ if (scalar(keys(%units_to_reload)) > 0) {
     for my $unit (keys(%units_to_reload)) {
         if (!unit_is_active($unit)) {
             # Figure out if we need to start the unit
-            my %unit_info = parse_unit("$out/etc/systemd/system/$unit");
+            my %unit_info = parse_unit("$toplevel/etc/systemd/system/$unit");
             if (!(parse_systemd_bool(\%unit_info, "Unit", "RefuseManualStart", 0) || parse_systemd_bool(\%unit_info, "Unit", "X-OnlyManualStart", 0))) {
                 $units_to_start{$unit} = 1;
                 record_unit($start_list_file, $unit);
@@ -940,9 +942,9 @@ if (scalar(@failed) > 0) {
 }
 
 if ($res == 0) {
-    syslog(LOG_NOTICE, "finished switching to system configuration $out");
+    syslog(LOG_NOTICE, "finished switching to system configuration $toplevel");
 } else {
-    syslog(LOG_ERR, "switching to system configuration $out failed (status $res)");
+    syslog(LOG_ERR, "switching to system configuration $toplevel failed (status $res)");
 }
 
 exit($res);
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index 68eb0469d8e..07c2e05ce37 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -36,13 +36,6 @@ let
         ln -s ${config.hardware.firmware}/lib/firmware $out/firmware
       ''}
 
-      echo "$activationScript" > $out/activate
-      echo "$dryActivationScript" > $out/dry-activate
-      substituteInPlace $out/activate --subst-var out
-      substituteInPlace $out/dry-activate --subst-var out
-      chmod u+x $out/activate $out/dry-activate
-      unset activationScript dryActivationScript
-
       ${if config.boot.initrd.systemd.enable then ''
         cp ${config.system.build.bootStage2} $out/prepare-root
         substituteInPlace $out/prepare-root --subst-var-by systemConfig $out
@@ -63,19 +56,6 @@ let
       echo -n "$nixosLabel" > $out/nixos-version
       echo -n "${config.boot.kernelPackages.stdenv.hostPlatform.system}" > $out/system
 
-      mkdir $out/bin
-      export localeArchive="${config.i18n.glibcLocales}/lib/locale/locale-archive"
-      export distroId=${config.system.nixos.distroId};
-      substituteAll ${./switch-to-configuration.pl} $out/bin/switch-to-configuration
-      chmod +x $out/bin/switch-to-configuration
-      ${optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
-        if ! output=$($perl/bin/perl -c $out/bin/switch-to-configuration 2>&1); then
-          echo "switch-to-configuration syntax is not valid:"
-          echo "$output"
-          exit 1
-        fi
-      ''}
-
       ${config.system.systemBuilderCommands}
 
       cp "$extraDependenciesPath" "$out/extra-dependencies"
@@ -93,7 +73,7 @@ let
   # symlinks to the various parts of the built configuration (the
   # kernel, systemd units, init scripts, etc.) as well as a script
   # `switch-to-configuration' that activates the configuration and
-  # makes it bootable.
+  # makes it bootable. See `activatable-system.nix`.
   baseSystem = pkgs.stdenvNoCC.mkDerivation ({
     name = "nixos-system-${config.system.name}-${config.system.nixos.label}";
     preferLocalBuild = true;
@@ -101,22 +81,12 @@ let
     passAsFile = [ "extraDependencies" ];
     buildCommand = systemBuilder;
 
-    inherit (pkgs) coreutils;
     systemd = config.systemd.package;
-    shell = "${pkgs.bash}/bin/sh";
-    su = "${pkgs.shadow.su}/bin/su";
-    utillinux = pkgs.util-linux;
 
     kernelParams = config.boot.kernelParams;
-    installBootLoader = config.system.build.installBootLoader;
-    activationScript = config.system.activationScripts.script;
-    dryActivationScript = config.system.dryActivationScript;
     nixosLabel = config.system.nixos.label;
 
     inherit (config.system) extraDependencies;
-
-    # Needed by switch-to-configuration.
-    perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
   } // config.system.systemBuilderArgs);
 
   # Handle assertions and warnings
@@ -178,26 +148,6 @@ in
     };
 
     system.build = {
-      installBootLoader = mkOption {
-        internal = true;
-        # "; true" => make the `$out` argument from switch-to-configuration.pl
-        #             go to `true` instead of `echo`, hiding the useless path
-        #             from the log.
-        default = "echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2; true";
-        description = lib.mdDoc ''
-          A program that writes a bootloader installation script to the path passed in the first command line argument.
-
-          See `nixos/modules/system/activation/switch-to-configuration.pl`.
-        '';
-        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);
-      };
-
       toplevel = mkOption {
         type = types.package;
         readOnly = true;
@@ -380,6 +330,16 @@ in
         '';
 
     system.systemBuilderArgs = {
+
+      # Legacy environment variables. These were used by the activation script,
+      # but some other script might still depend on them, although unlikely.
+      installBootLoader = config.system.build.installBootLoader;
+      localeArchive = "${config.i18n.glibcLocales}/lib/locale/locale-archive";
+      distroId = config.system.nixos.distroId;
+      perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
+      # End if legacy environment variables
+
+
       # Not actually used in the builder. `passedChecks` is just here to create
       # the build dependencies. Checks are similar to build dependencies in the
       # sense that if they fail, the system build fails. However, checks do not
diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix
index f891a2cb2f4..f44dede7fef 100644
--- a/nixos/tests/switch-test.nix
+++ b/nixos/tests/switch-test.nix
@@ -70,6 +70,19 @@ in {
           };
         };
 
+        simpleServiceSeparateActivationScript.configuration = {
+          system.activatable = false;
+          systemd.services.test = {
+            wantedBy = [ "multi-user.target" ];
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+              ExecStart = "${pkgs.coreutils}/bin/true";
+              ExecReload = "${pkgs.coreutils}/bin/true";
+            };
+          };
+        };
+
         simpleServiceDifferentDescription.configuration = {
           imports = [ simpleService.configuration ];
           systemd.services.test.description = "Test unit";
@@ -482,9 +495,9 @@ in {
   };
 
   testScript = { nodes, ... }: let
-    originalSystem = nodes.machine.config.system.build.toplevel;
-    otherSystem = nodes.other.config.system.build.toplevel;
-    machine = nodes.machine.config.system.build.toplevel;
+    originalSystem = nodes.machine.system.build.toplevel;
+    otherSystem = nodes.other.system.build.toplevel;
+    machine = nodes.machine.system.build.toplevel;
 
     # Ensures failures pass through using pipefail, otherwise failing to
     # switch-to-configuration is hidden by the success of `tee`.
@@ -497,11 +510,15 @@ in {
   in /* python */ ''
     def switch_to_specialisation(system, name, action="test", fail=False):
         if name == "":
-            stc = f"{system}/bin/switch-to-configuration"
+            switcher = f"{system}/bin/switch-to-configuration"
         else:
-            stc = f"{system}/specialisation/{name}/bin/switch-to-configuration"
-        out = machine.fail(f"{stc} {action} 2>&1") if fail \
-            else machine.succeed(f"{stc} {action} 2>&1")
+            switcher = f"{system}/specialisation/{name}/bin/switch-to-configuration"
+        return run_switch(switcher, action, fail)
+
+    # like above but stc = switcher
+    def run_switch(switcher, action="test", fail=False):
+        out = machine.fail(f"{switcher} {action} 2>&1") if fail \
+            else machine.succeed(f"{switcher} {action} 2>&1")
         assert_lacks(out, "switch-to-configuration line")  # Perl warnings
         return out
 
@@ -639,6 +656,22 @@ in {
         assert_lacks(out, "the following new units were started:")
         assert_contains(out, "would start the following units: test.service\n")
 
+        out = switch_to_specialisation("${machine}", "", action="test")
+
+        # Ensure the service can be started when the activation script isn't in toplevel
+        # This is a lot like "Start a simple service", except activation-only deps could be gc-ed
+        out = run_switch("${nodes.machine.specialisation.simpleServiceSeparateActivationScript.configuration.system.build.separateActivationScript}/bin/switch-to-configuration");
+        assert_lacks(out, "installing dummy bootloader")  # test does not install a bootloader
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_contains(out, "reloading the following units: dbus.service\n")  # huh
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_contains(out, "the following new units were started: test.service\n")
+        machine.succeed("! test -e /run/current-system/activate")
+        machine.succeed("! test -e /run/current-system/dry-activate")
+        machine.succeed("! test -e /run/current-system/bin/switch-to-configuration")
+
         # Ensure \ works in unit names
         out = switch_to_specialisation("${machine}", "unitWithBackslash")
         assert_contains(out, "stopping the following units: test.service\n")