Commit 64275009 authored by Robert Hensing's avatar Robert Hensing
Browse files

nixos/toplevel: Add bin/apply

parent 2fcea20c
Loading
Loading
Loading
Loading
+146 −0
Original line number Diff line number Diff line
#!@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
+51 −0
Original line number Diff line number Diff line
# 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
        '';
      };
}
+176 −0
Original line number Diff line number Diff line
#!/usr/bin/env bash
# shellcheck disable=SC2317 disable=SC2031
# False positives:
# SC2317: Unreachable code: TEST_*
# SC2031: <variable> 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
+25 −2
Original line number Diff line number Diff line
@@ -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 \
+1 −0
Original line number Diff line number Diff line
@@ -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 {};