From 642750098948bc77303044df4e1c248536ccde98 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 25 Sep 2024 11:05:48 +0200 Subject: [PATCH] nixos/toplevel: Add bin/apply --- .../modules/system/activation/apply/apply.sh | 146 +++++++++++++++ .../system/activation/apply/checks.nix | 51 +++++ nixos/modules/system/activation/apply/test.sh | 176 ++++++++++++++++++ .../system/activation/switchable-system.nix | 27 ++- nixos/tests/all-tests.nix | 1 + 5 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 nixos/modules/system/activation/apply/apply.sh create mode 100644 nixos/modules/system/activation/apply/checks.nix create mode 100755 nixos/modules/system/activation/apply/test.sh diff --git a/nixos/modules/system/activation/apply/apply.sh b/nixos/modules/system/activation/apply/apply.sh new file mode 100644 index 000000000000..f3dd9a658b2e --- /dev/null +++ b/nixos/modules/system/activation/apply/apply.sh @@ -0,0 +1,146 @@ +#!@bash@ + + +# This is the NixOS apply script, typically located at +# +# ${config.system.build.toplevel}/bin/apply +# +# This script is responsible for managing the profile link and calling the +# appropriate scripts for its subcommands, such as switch, boot, and test. + + +set -euo pipefail + +toplevel=@toplevel@ + +subcommand= + +installBootloader= +specialisation= +profile=/nix/var/nix/profiles/system + +log() { + echo "$@" >&2 +} + +die() { + log "NixOS apply error: $*" + exit 1 +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + switch|boot|test|dry-activate) + subcommand="$1" + ;; + --install-bootloader) + installBootloader=1 + ;; + --profile) + if [[ $# -lt 2 ]]; then + die "missing argument for --profile" + fi + profile="$2" + shift + ;; + # --rollback is not an `apply` responsibility, and it should be + # implemented by the caller of `apply` instead. + --specialisation) + if [[ $# -lt 2 ]]; then + die "missing argument for --specialisation" + fi + specialisation="$2" + shift + ;; + *) + if [[ -n "$subcommand" ]]; then + die "unexpected argument or flag: $1" + else + die "unexpected subcommand or flag: $1" + fi + ;; + esac + shift + done + + if [ -z "$subcommand" ]; then + die "no subcommand specified" + fi +} + +main() { + local cmd activity + + case "$subcommand" in + boot|switch) + nix-env -p "$profile" --set "$toplevel" + ;; + esac + + # Using systemd-run here to protect against PTY failures/network + # disconnections during rebuild. + # See: https://github.com/NixOS/nixpkgs/issues/39118 + cmd=( + "systemd-run" + "-E" "LOCALE_ARCHIVE" # Will be set to new value early in switch-to-configuration script, but interpreter starts out with old value + "-E" "NIXOS_INSTALL_BOOTLOADER=$installBootloader" + "--collect" + "--no-ask-password" + "--pipe" + "--quiet" + "--same-dir" + "--service-type=exec" + "--unit=nixos-rebuild-switch-to-configuration" + "--wait" + ) + # Check if we have a working systemd-run. In chroot environments we may have + # a non-working systemd, so we fallback to not using systemd-run. + if ! "${cmd[@]}" true; then + log "Skipping systemd-run to switch configuration since it is not working in target host." + cmd=( + "env" + "-i" + "LOCALE_ARCHIVE=${LOCALE_ARCHIVE:-}" + "NIXOS_INSTALL_BOOTLOADER=$installBootloader" + ) + fi + if [[ -z "$specialisation" ]]; then + cmd+=("$toplevel/bin/switch-to-configuration") + else + cmd+=("$toplevel/specialisation/$specialisation/bin/switch-to-configuration") + + if ! [[ -f "${cmd[-1]}" ]]; then + log "error: specialisation not found: $specialisation" + exit 1 + fi + fi + + if ! "${cmd[@]}" "$subcommand"; then + case "$subcommand" in + switch) + activity="switching to the new configuration" + ;; + boot) + activity="switching the boot entry to the new configuration" + ;; + test) + activity="switching to the new configuration (in test mode)" + ;; + dry-activate) + activity="switching to the new configuration (in dry-activate mode)" + ;; + *) # Should never happen + activity="running $subcommand" + ;; + esac + log "warning: error(s) occurred while $activity" + exit 1 + fi +} + +if ! type test_run_tests &>/dev/null; then + # We're not loaded into the test.sh, so we run main. + parse_args "$@" + main +fi diff --git a/nixos/modules/system/activation/apply/checks.nix b/nixos/modules/system/activation/apply/checks.nix new file mode 100644 index 000000000000..bb509b9210cb --- /dev/null +++ b/nixos/modules/system/activation/apply/checks.nix @@ -0,0 +1,51 @@ +# Run: +# nix-build -A nixosTests.apply +# +# These are not all tests. See also nixosTests. + +{ + lib, + stdenvNoCC, + testers, + ... +}: + +let + fileset = lib.fileset.unions [ + ./test.sh + ./apply.sh + ]; +in + +{ + unitTests = stdenvNoCC.mkDerivation { + name = "nixos-apply-unit-tests"; + src = lib.fileset.toSource { + root = ./.; + inherit fileset; + }; + dontBuild = true; + checkPhase = '' + ./test.sh + ''; + installPhase = '' + touch $out + ''; + }; + + shellcheck = + (testers.shellcheck { + src = lib.fileset.toSource { + # This makes the error messages include the full path + root = ../../../../..; + inherit fileset; + }; + }).overrideAttrs + { + postUnpack = '' + for f in $(find . -type f); do + substituteInPlace $f --replace @bash@ /usr/bin/bash + done + ''; + }; +} diff --git a/nixos/modules/system/activation/apply/test.sh b/nixos/modules/system/activation/apply/test.sh new file mode 100755 index 000000000000..02ab01f69bea --- /dev/null +++ b/nixos/modules/system/activation/apply/test.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2317 disable=SC2031 +# False positives: +# SC2317: Unreachable code: TEST_* +# SC2031: was modified in a subshell. That change might be lost. +# We have a lot of that, and that's expected. + +# This is a unit test script for the NixOS apply script. +# It can be run quickly with the following command: +# +# ./test.sh +# +# Alternatively, run the following to run all tests and checks +# +# TODO +# + +set -euo pipefail +# set -x + +apply="${BASH_SOURCE[0]%/*}/apply.sh" +# source_apply() { + +run_parse_args() { + bash -c "source $apply;"' parse_args "$@"' -- "$@" +} + +TEST_parse_args_none() { + if errout="$(run_parse_args 2>&1)"; then + test_fail "apply without arguments should fail" + elif [[ $? -ne 1 ]]; then + test_fail "apply without arguments should exit with code 1" + fi + grep -F "no subcommand specified" <<<"$errout" >/dev/null +} + +TEST_parse_args_switch() { + ( + # shellcheck source=nixos/modules/system/activation/apply/apply.sh + source "$apply"; + parse_args switch; + [[ $subcommand == switch ]] + [[ $specialisation == "" ]] + [[ $profile == "" ]] + ) +} + +TEST_parse_args_boot() { + ( + # shellcheck source=nixos/modules/system/activation/apply/apply.sh + source "$apply"; + parse_args boot; + [[ $subcommand == boot ]] + [[ $specialisation == "" ]] + [[ $profile == "" ]] + ) +} + +TEST_parse_args_test() { + ( + # shellcheck source=nixos/modules/system/activation/apply/apply.sh + source "$apply"; + parse_args test; + [[ $subcommand == test ]] + [[ $specialisation == "" ]] + [[ $profile == "" ]] + ) +} + +TEST_parse_args_dry_activate() { + ( + # shellcheck source=nixos/modules/system/activation/apply/apply.sh + source "$apply"; + parse_args dry-activate; + [[ $subcommand == dry-activate ]] + [[ $specialisation == "" ]] + [[ $profile == "" ]] + ) +} + +TEST_parse_args_unknown() { + if errout="$(run_parse_args foo 2>&1)"; then + test_fail "apply with unknown subcommand should fail" + fi + grep -F "unexpected argument or flag: foo" <<<"$errout" >/dev/null +} + +TEST_parse_args_switch_specialisation_no_arg() { + if errout="$(run_parse_args switch --specialisation 2>&1)"; then + test_fail "apply with --specialisation without argument should fail" + fi + grep -F "missing argument for --specialisation" <<<"$errout" >/dev/null +} + +TEST_parse_args_switch_specialisation() { + ( + # shellcheck source=nixos/modules/system/activation/apply/apply.sh + source "$apply"; + parse_args switch --specialisation low-power; + [[ $subcommand == switch ]] + [[ $specialisation == low-power ]] + [[ $profile == "" ]] + ) +} + +TEST_parse_args_switch_profile() { + ( + # shellcheck source=nixos/modules/system/activation/apply/apply.sh + source "$apply"; + parse_args switch --profile /nix/var/nix/profiles/system; + [[ $subcommand == switch ]] + [[ $specialisation == "" ]] + [[ $profile == /nix/var/nix/profiles/system ]] + ) +} + + + +# Support code + +test_fail() { + echo "TEST FAILURE: $*" >&2 + exit 1 +} + +test_print_trace() { + local frame=${1:0} + local caller + # shellcheck disable=SC2207 disable=SC2086 + while caller=( $(caller $frame) ); do + echo " in ${caller[1]} at ${caller[2]}:${caller[0]}" + frame=$((frame+1)); + done +} +test_on_err() { + echo "ERROR running: ${BASH_COMMAND}" >&2 + test_print_trace 1 >&2 +} + +test_init() { + trap 'test_on_err' ERR +} + +test_find() { + declare -F | grep -o 'TEST_.*' | sort +} + +test_run_tests() { + local status=0 + for test in $(test_find); do + set +e + ( + set -eEuo pipefail + trap 'test_on_err' ERR + $test + ) + r=$? + set -e + if [[ $r == 0 ]]; then + echo "ok: $test" + else + echo "TEST FAIL: $test"; status=1; + fi + done + if [[ $status == 0 ]]; then + echo "All good" + else + echo + echo "TEST SUITE FAILED" + fi + exit $status +} + +# Main +test_init +test_run_tests diff --git a/nixos/modules/system/activation/switchable-system.nix b/nixos/modules/system/activation/switchable-system.nix index d1326a18e5fe..bb3f7a7d4a78 100644 --- a/nixos/modules/system/activation/switchable-system.nix +++ b/nixos/modules/system/activation/switchable-system.nix @@ -40,7 +40,30 @@ in }; }; + options.system.apply.enable = lib.mkOption { + type = lib.types.bool; + default = config.system.switch.enable; + internal = true; + description = '' + Whether to include the `bin/apply` script. + + Disabling puts `nixos-rebuild` in a legacy mode that won't be maintained + and removes cheap and useful functionality. It's also slower over ssh. + This should only be used for testing the `nixos-rebuild` command, to + pretend that the configuration is an old NixOS. + ''; + }; + config = lib.mkMerge [ + (lib.mkIf config.system.apply.enable { + system.activatableSystemBuilderCommands = '' + mkdir -p $out/bin + substitute ${./apply/apply.sh} $out/bin/apply \ + --subst-var-by bash ${lib.getExe pkgs.bash} \ + --subst-var-by toplevel ''${!toplevelVar} + chmod +x $out/bin/apply + ''; + }) (lib.mkIf (config.system.switch.enable && !config.system.switch.enableNg) { warnings = [ '' @@ -54,7 +77,7 @@ in ]; system.activatableSystemBuilderCommands = '' - mkdir $out/bin + mkdir -p $out/bin substitute ${./switch-to-configuration.pl} $out/bin/switch-to-configuration \ --subst-var out \ --subst-var-by toplevel ''${!toplevelVar} \ @@ -86,7 +109,7 @@ in ( source ${pkgs.buildPackages.makeWrapper}/nix-support/setup-hook - mkdir $out/bin + mkdir -p $out/bin ln -sf ${lib.getExe pkgs.switch-to-configuration-ng} $out/bin/switch-to-configuration wrapProgram $out/bin/switch-to-configuration \ --set OUT $out \ diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 8b674bfb7342..4f81bf6e5600 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -129,6 +129,7 @@ in { apfs = runTest ./apfs.nix; appliance-repart-image = runTest ./appliance-repart-image.nix; appliance-repart-image-verity-store = runTest ./appliance-repart-image-verity-store.nix; + apply = pkgs.callPackage ../modules/system/activation/apply/checks.nix { }; apparmor = handleTest ./apparmor.nix {}; archi = handleTest ./archi.nix {}; aria2 = handleTest ./aria2.nix {};