Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/system: Support pre-activated images #237040

Merged
merged 8 commits into from
Jul 8, 2023
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,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
Expand Down
92 changes: 92 additions & 0 deletions nixos/modules/system/activation/activatable-system.nix
Original file line number Diff line number Diff line change
@@ -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}
'';
};
}
21 changes: 21 additions & 0 deletions nixos/modules/system/activation/activation-script.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};

};


Expand Down
34 changes: 18 additions & 16 deletions nixos/modules/system/activation/switch-to-configuration.pl
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
## 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
Expand Down Expand Up @@ -96,7 +98,7 @@
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.
Expand All @@ -110,7 +112,7 @@

# 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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -539,13 +541,13 @@ sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutin
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;
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -670,7 +672,7 @@ sub path_to_unit_name {
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) {
Expand Down Expand Up @@ -709,12 +711,12 @@ sub filter_units {
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;
Expand Down Expand Up @@ -757,7 +759,7 @@ sub filter_units {
}


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)) {
Expand All @@ -781,12 +783,12 @@ sub filter_units {
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;
Expand Down Expand Up @@ -857,7 +859,7 @@ sub filter_units {
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);
Expand Down Expand Up @@ -940,9 +942,9 @@ sub filter_units {
}

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);
62 changes: 11 additions & 51 deletions nixos/modules/system/activation/top-level.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -93,30 +73,20 @@ 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;
allowSubstitutes = false;
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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading