diff --git a/doc/build-helpers/testers.chapter.md b/doc/build-helpers/testers.chapter.md index a10e60de8c6d..ec659e75bdb5 100644 --- a/doc/build-helpers/testers.chapter.md +++ b/doc/build-helpers/testers.chapter.md @@ -116,6 +116,55 @@ It has two modes: : The `lychee` package to use. +## `shellcheck` {#tester-shellcheck} + +Runs files through `shellcheck`, a static analysis tool for shell scripts. + +:::{.example #ex-shellcheck} +# Run `testers.shellcheck` + +A single script + +```nix +testers.shellcheck { + name = "shellcheck"; + src = ./script.sh; +} +``` + +Multiple files + +```nix +let + inherit (lib) fileset; +in +testers.shellcheck { + name = "shellcheck"; + src = fileset.toSource { + root = ./.; + fileset = fileset.unions [ + ./lib.sh + ./nixbsd-activate + ]; + }; +} +``` + +::: + +### Inputs {#tester-shellcheck-inputs} + +[`src` (path or string)]{#tester-shellcheck-param-src} + +: The path to the shell script(s) to check. + This can be a single file or a directory containing shell files. + All files in `src` will be checked, so you may want to provide `fileset`-based source instead of a whole directory. + +### Return value {#tester-shellcheck-return} + +A derivation that runs `shellcheck` on the given script(s). +The build will fail if `shellcheck` finds any issues. + ## `testVersion` {#tester-testVersion} Checks that the output from running a command contains the specified version string in it as a whole word. diff --git a/nixos/modules/config/nix-channel.nix b/nixos/modules/config/nix-channel.nix index 3cdd1d1b6366..2703a60f858f 100644 --- a/nixos/modules/config/nix-channel.nix +++ b/nixos/modules/config/nix-channel.nix @@ -12,6 +12,7 @@ let mkDefault mkIf mkOption + stringAfter types ; @@ -97,5 +98,8 @@ in systemd.tmpfiles.rules = lib.mkIf cfg.channel.enable [ ''f /root/.nix-channels - - - - ${config.system.defaultChannel} nixos\n'' ]; + + system.activationScripts.no-nix-channel = mkIf (!cfg.channel.enable) + (stringAfter [ "etc" "users" ] (builtins.readFile ./nix-channel/activation-check.sh)); }; } diff --git a/nixos/modules/config/nix-channel/activation-check.sh b/nixos/modules/config/nix-channel/activation-check.sh new file mode 100644 index 000000000000..42b1b712d702 --- /dev/null +++ b/nixos/modules/config/nix-channel/activation-check.sh @@ -0,0 +1,21 @@ +# shellcheck shell=bash + +explainChannelWarning=0 +if [[ -e "/root/.nix-defexpr/channels" ]]; then + warn '/root/.nix-defexpr/channels exists, but channels have been disabled.' + explainChannelWarning=1 +fi +if [[ -e "/nix/var/nix/profiles/per-user/root/channels" ]]; then + warn "/nix/var/nix/profiles/per-user/root/channels exists, but channels have been disabled." + explainChannelWarning=1 +fi +while IFS=: read -r _ _ _ _ _ home _ ; do + if [[ -n "$home" && -e "$home/.nix-defexpr/channels" ]]; then + warn "$home/.nix-defexpr/channels exists, but channels have been disabled." 1>&2 + explainChannelWarning=1 + fi +done < <(getent passwd) +if [[ $explainChannelWarning -eq 1 ]]; then + echo "Due to https://github.com/NixOS/nix/issues/9574, Nix may still use these channels when NIX_PATH is unset." 1>&2 + echo "Delete the above directory or directories to prevent this." 1>&2 +fi diff --git a/nixos/modules/config/nix-channel/test.nix b/nixos/modules/config/nix-channel/test.nix new file mode 100644 index 000000000000..4b00cf9db3c4 --- /dev/null +++ b/nixos/modules/config/nix-channel/test.nix @@ -0,0 +1,19 @@ +# Run: +# nix-build -A nixosTests.nix-channel +{ lib, testers }: +let + inherit (lib) fileset; + + runShellcheck = testers.shellcheck { + src = fileset.toSource { + root = ./.; + fileset = fileset.unions [ + ./activation-check.sh + ]; + }; + }; + +in +lib.recurseIntoAttrs { + inherit runShellcheck; +} diff --git a/nixos/modules/system/activation/activation-script.nix b/nixos/modules/system/activation/activation-script.nix index fc29aa3cb2f7..195ad31b1e56 100644 --- a/nixos/modules/system/activation/activation-script.nix +++ b/nixos/modules/system/activation/activation-script.nix @@ -33,6 +33,8 @@ let '' #!${pkgs.runtimeShell} + source ${./lib/lib.sh} + systemConfig='@out@' export PATH=/empty diff --git a/nixos/modules/system/activation/lib/lib.sh b/nixos/modules/system/activation/lib/lib.sh new file mode 100644 index 000000000000..5ecf94e81604 --- /dev/null +++ b/nixos/modules/system/activation/lib/lib.sh @@ -0,0 +1,5 @@ +# shellcheck shell=bash + +warn() { + printf "\033[1;35mwarning:\033[0m %s\n" "$*" >&2 +} diff --git a/nixos/modules/system/activation/lib/test.nix b/nixos/modules/system/activation/lib/test.nix new file mode 100644 index 000000000000..39886d305195 --- /dev/null +++ b/nixos/modules/system/activation/lib/test.nix @@ -0,0 +1,36 @@ +# Run: +# nix-build -A nixosTests.activation-lib +{ lib, stdenv, testers }: +let + inherit (lib) fileset; + + runTests = stdenv.mkDerivation { + name = "tests-activation-lib"; + src = fileset.toSource { + root = ./.; + fileset = fileset.unions [ + ./lib.sh + ./test.sh + ]; + }; + buildPhase = ":"; + doCheck = true; + postUnpack = '' + patchShebangs --build . + ''; + checkPhase = '' + ./test.sh + ''; + installPhase = '' + touch $out + ''; + }; + + runShellcheck = testers.shellcheck { + src = runTests.src; + }; + +in +lib.recurseIntoAttrs { + inherit runTests runShellcheck; +} diff --git a/nixos/modules/system/activation/lib/test.sh b/nixos/modules/system/activation/lib/test.sh new file mode 100755 index 000000000000..9b146383ad4b --- /dev/null +++ b/nixos/modules/system/activation/lib/test.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# Run: +# ./test.sh +# or: +# nix-build -A nixosTests.activation-lib + +cd "$(dirname "${BASH_SOURCE[0]}")" +set -euo pipefail + +# report failure +onerr() { + set +e + # find failed statement + echo "call trace:" + local i=0 + while t="$(caller $i)"; do + line="${t%% *}" + file="${t##* }" + echo " $file:$line" >&2 + ((i++)) + done + # red + printf "\033[1;31mtest failed\033[0m\n" >&2 + exit 1 +} +trap onerr ERR + +source ./lib.sh + +(warn hi, this works >/dev/null) 2>&1 | grep -E $'.*warning:.* hi, this works' >/dev/null + +# green +printf "\033[1;32mok\033[0m\n" diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 18beed47d4c2..96cc6215118c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -301,6 +301,7 @@ in { esphome = handleTest ./esphome.nix {}; etc = pkgs.callPackage ../modules/system/etc/test.nix { inherit evalMinimalConfig; }; activation = pkgs.callPackage ../modules/system/activation/test.nix { }; + activation-lib = pkgs.callPackage ../modules/system/activation/lib/test.nix { }; activation-var = runTest ./activation/var.nix; activation-nix-channel = runTest ./activation/nix-channel.nix; activation-etc-overlay-mutable = runTest ./activation/etc-overlay-mutable.nix; @@ -624,6 +625,7 @@ in { nbd = handleTest ./nbd.nix {}; ncdns = handleTest ./ncdns.nix {}; ndppd = handleTest ./ndppd.nix {}; + nix-channel = pkgs.callPackage ../modules/config/nix-channel/test.nix { }; nebula = handleTest ./nebula.nix {}; netbird = handleTest ./netbird.nix {}; nimdow = handleTest ./nimdow.nix {}; diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix index 49c50a6f5a3a..d57866c9f52c 100644 --- a/nixos/tests/installer.nix +++ b/nixos/tests/installer.nix @@ -463,7 +463,32 @@ let """) with subtest("Switch to flake based config"): - target.succeed("nixos-rebuild switch --flake /root/my-config#xyz") + target.succeed("nixos-rebuild switch --flake /root/my-config#xyz 2>&1 | tee activation-log >&2") + + target.succeed(""" + cat -n activation-log >&2 + """) + + target.succeed(""" + grep -F '/root/.nix-defexpr/channels exists, but channels have been disabled.' activation-log + """) + target.succeed(""" + grep -F '/nix/var/nix/profiles/per-user/root/channels exists, but channels have been disabled.' activation-log + """) + target.succeed(""" + grep -F '/root/.nix-defexpr/channels exists, but channels have been disabled.' activation-log + """) + target.succeed(""" + grep -F 'Due to https://github.com/NixOS/nix/issues/9574, Nix may still use these channels when NIX_PATH is unset.' activation-log + """) + target.succeed("rm activation-log") + + # Perform the suggested cleanups we've just seen in the log + # TODO after https://github.com/NixOS/nix/issues/9574: don't remove them yet + target.succeed(""" + rm -rf /root/.nix-defexpr/channels /nix/var/nix/profiles/per-user/root/channels /root/.nix-defexpr/channels + """) + target.shutdown() @@ -474,10 +499,20 @@ let # Note that the channel profile is still present on disk, but configured # not to be used. - with subtest("builtins.nixPath is now empty"): - target.succeed(""" - [[ "[ ]" == "$(nix-instantiate builtins.nixPath --eval --expr)" ]] - """) + # TODO after issue https://github.com/NixOS/nix/issues/9574: re-enable this assertion + # I believe what happens is + # - because of the issue, we've removed the `nix-path =` line from nix.conf + # - the "backdoor" shell is not a proper session and does not have `NIX_PATH=""` set + # - seeing no nix path settings at all, Nix loads its hardcoded default value, + # which is unfortunately non-empty + # Or maybe it's the new default NIX_PATH?? :( + # with subtest("builtins.nixPath is now empty"): + # target.succeed(""" + # ( + # set -x; + # [[ "[ ]" == "$(nix-instantiate builtins.nixPath --eval --expr)" ]]; + # ) + # """) with subtest(" does not resolve"): target.succeed(""" @@ -491,12 +526,16 @@ let target.succeed(""" ( exec 1>&2 - rm -v /root/.nix-channels + rm -vf /root/.nix-channels rm -vrf ~/.nix-defexpr rm -vrf /nix/var/nix/profiles/per-user/root/channels* ) """) - target.succeed("nixos-rebuild switch --flake /root/my-config#xyz") + target.succeed("nixos-rebuild switch --flake /root/my-config#xyz | tee activation-log >&2") + target.succeed("cat -n activation-log >&2") + target.succeed("! grep -F '/root/.nix-defexpr/channels' activation-log") + target.succeed("! grep -F 'but channels have been disabled' activation-log") + target.succeed("! grep -F 'https://github.com/NixOS/nix/issues/9574' activation-log") target.shutdown() ''; diff --git a/pkgs/build-support/testers/default.nix b/pkgs/build-support/testers/default.nix index 20d2957134c3..d0d88115003f 100644 --- a/pkgs/build-support/testers/default.nix +++ b/pkgs/build-support/testers/default.nix @@ -140,4 +140,6 @@ hasPkgConfigModules = callPackage ./hasPkgConfigModules/tester.nix { }; testMetaPkgConfig = callPackage ./testMetaPkgConfig/tester.nix { }; + + shellcheck = callPackage ./shellcheck/tester.nix { }; } diff --git a/pkgs/build-support/testers/shellcheck/example.sh b/pkgs/build-support/testers/shellcheck/example.sh new file mode 100644 index 000000000000..7e89bf37d3cc --- /dev/null +++ b/pkgs/build-support/testers/shellcheck/example.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo $@ diff --git a/pkgs/build-support/testers/shellcheck/tester.nix b/pkgs/build-support/testers/shellcheck/tester.nix new file mode 100644 index 000000000000..66f048f23095 --- /dev/null +++ b/pkgs/build-support/testers/shellcheck/tester.nix @@ -0,0 +1,28 @@ +# Dependencies (callPackage) +{ lib, stdenv, shellcheck }: + +# testers.shellcheck function +# Docs: doc/build-helpers/testers.chapter.md +# Tests: ./tests.nix +{ src }: +let + inherit (lib) fileset pathType isPath; +in +stdenv.mkDerivation { + name = "run-shellcheck"; + src = + if isPath src && pathType src == "regular" # note that for strings this would have been IFD, which we prefer to avoid + then fileset.toSource { root = dirOf src; fileset = src; } + else src; + nativeBuildInputs = [ shellcheck ]; + doCheck = true; + dontConfigure = true; + dontBuild = true; + checkPhase = '' + find . -type f -print0 \ + | xargs -0 shellcheck + ''; + installPhase = '' + touch $out + ''; +} diff --git a/pkgs/build-support/testers/shellcheck/tests.nix b/pkgs/build-support/testers/shellcheck/tests.nix new file mode 100644 index 000000000000..855aa14afead --- /dev/null +++ b/pkgs/build-support/testers/shellcheck/tests.nix @@ -0,0 +1,38 @@ +# Run: +# nix-build -A tests.testers.shellcheck + +{ lib, testers, runCommand }: +let + inherit (lib) fileset; +in +lib.recurseIntoAttrs { + + example-dir = runCommand "test-testers-shellcheck-example-dir" { + failure = testers.testBuildFailure + (testers.shellcheck { + src = fileset.toSource { + root = ./.; + fileset = fileset.unions [ + ./example.sh + ]; + }; + }); + } '' + log="$failure/testBuildFailure.log" + echo "Checking $log" + grep SC2068 "$log" + touch $out + ''; + + example-file = runCommand "test-testers-shellcheck-example-file" { + failure = testers.testBuildFailure + (testers.shellcheck { + src = ./example.sh; + }); + } '' + log="$failure/testBuildFailure.log" + echo "Checking $log" + grep SC2068 "$log" + touch $out + ''; +} diff --git a/pkgs/build-support/testers/test/default.nix b/pkgs/build-support/testers/test/default.nix index 8e8886342f1e..48855df91627 100644 --- a/pkgs/build-support/testers/test/default.nix +++ b/pkgs/build-support/testers/test/default.nix @@ -16,6 +16,8 @@ lib.recurseIntoAttrs { hasPkgConfigModules = pkgs.callPackage ../hasPkgConfigModules/tests.nix { }; + shellcheck = pkgs.callPackage ../shellcheck/tests.nix { }; + runNixOSTest-example = pkgs-with-overlay.testers.runNixOSTest ({ lib, ... }: { name = "runNixOSTest-test"; nodes.machine = { pkgs, ... }: {