diff --git a/doc/build-helpers/testers.chapter.md b/doc/build-helpers/testers.chapter.md index 34cfc00a4953c..9b2dbcd8365da 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 6498ce6c469ca..2703a60f858fb 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 ; @@ -94,10 +95,11 @@ in NIX_PATH = cfg.nixPath; }; - nix.settings.nix-path = mkIf (! cfg.channel.enable) (mkDefault ""); - 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 0000000000000..42b1b712d702b --- /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 0000000000000..4b00cf9db3c47 --- /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 fc29aa3cb2f71..195ad31b1e56c 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 0000000000000..5ecf94e81604c --- /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 0000000000000..39886d305195a --- /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 0000000000000..9b146383ad4b0 --- /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 d16b747bfa95e..61596fb8708b2 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -296,6 +296,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; @@ -609,6 +610,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 bb6ad79615fa3..3a31df775c850 100644 --- a/nixos/tests/installer.nix +++ b/nixos/tests/installer.nix @@ -350,7 +350,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() @@ -361,10 +386,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(""" @@ -378,12 +413,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 dbf9a6d6cb05b..f70b544611416 100644 --- a/pkgs/build-support/testers/default.nix +++ b/pkgs/build-support/testers/default.nix @@ -151,4 +151,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 0000000000000..7e89bf37d3ccf --- /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 0000000000000..66f048f230959 --- /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 0000000000000..855aa14afead6 --- /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 a815fe63e416e..d719b0f349e45 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, ... }: {