summary refs log tree commit diff
diff options
context:
space:
mode:
authorRobert Hensing <robert@roberthensing.nl>2023-06-28 14:06:28 +0200
committerRobert Hensing <robert@roberthensing.nl>2023-06-28 14:06:28 +0200
commit772d6076e825dc999a04245f7dfa3cb19082ec28 (patch)
tree1c9fbc01eb3ff0d078ac69b54a358c8b8d55b793
parent89664199e18d45e1e629b011aaff0336987694b7 (diff)
downloadnixpkgs-772d6076e825dc999a04245f7dfa3cb19082ec28.tar
nixpkgs-772d6076e825dc999a04245f7dfa3cb19082ec28.tar.gz
nixpkgs-772d6076e825dc999a04245f7dfa3cb19082ec28.tar.bz2
nixpkgs-772d6076e825dc999a04245f7dfa3cb19082ec28.tar.lz
nixpkgs-772d6076e825dc999a04245f7dfa3cb19082ec28.tar.xz
nixpkgs-772d6076e825dc999a04245f7dfa3cb19082ec28.tar.zst
nixpkgs-772d6076e825dc999a04245f7dfa3cb19082ec28.zip
nixos: Add system.activatable flag for images that are pre-activated
-rw-r--r--nixos/modules/system/activation/activatable-system.nix121
-rwxr-xr-xnixos/modules/system/activation/switch-to-configuration.pl34
-rw-r--r--nixos/tests/switch-test.nix41
3 files changed, 132 insertions, 64 deletions
diff --git a/nixos/modules/system/activation/activatable-system.nix b/nixos/modules/system/activation/activatable-system.nix
index b179fce417e..7f6154794bd 100644
--- a/nixos/modules/system/activation/activatable-system.nix
+++ b/nixos/modules/system/activation/activatable-system.nix
@@ -1,59 +1,92 @@
-/*
-  This module adds the activation script to toplevel, so that any previously
-  built configuration can be activated again, as long as they're available in
-  the store, e.g. through the profile's older generations.
-
-  Alternate applications of the NixOS modules may omit this module, e.g. to
-  build images that are pre-activated and omit the activation script and its
-  dependencies.
- */
 { 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
 {
-  config = {
-    system.systemBuilderArgs = {
-      activationScript = config.system.activationScripts.script;
-      dryActivationScript = config.system.dryActivationScript;
+  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.systemBuilderCommands = ''
-      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
-
-      mkdir $out/bin
-      substitute ${./switch-to-configuration.pl} $out/bin/switch-to-configuration \
-        --subst-var out \
-        --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
-      ''}
-    '';
+    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/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index de6e43dd30d..a41e67a2294 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/tests/switch-test.nix b/nixos/tests/switch-test.nix
index 8cc4e68e78a..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";
@@ -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")