From c3b97e672854711c3d7b6206f93bbc430a0b15c7 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Mon, 5 Sep 2022 18:12:09 +0200 Subject: [PATCH] tests: add `shellcheckServices` --- test/lib/make-test.nix | 6 +- test/lib/shellcheck-services.nix | 128 +++++++++++++++++++++++++++++++ test/lib/test-lib.nix | 4 + test/shellcheck.sh | 2 + 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 test/lib/shellcheck-services.nix diff --git a/test/lib/make-test.nix b/test/lib/make-test.nix index 4c95a68..50cfceb 100644 --- a/test/lib/make-test.nix +++ b/test/lib/make-test.nix @@ -9,8 +9,9 @@ name: testConfig: vm = makeVM { name = "nix-bitcoin-${name}"; - nodes.machine = { + nodes.machine = { config, ... }: { imports = [ testConfig ]; + virtualisation = { # Needed because duplicity requires 270 MB of free temp space, regardless of backup size diskSize = 1024; @@ -20,6 +21,9 @@ name: testConfig: cores = lib.mkDefault 2; }; + + # Run shellcheck on all nix-bitcoin services during machine build time + system.extraDependencies = [ config.test.shellcheckServices ]; }; testScript = nodes: let diff --git a/test/lib/shellcheck-services.nix b/test/lib/shellcheck-services.nix new file mode 100644 index 0000000..bc6d5ba --- /dev/null +++ b/test/lib/shellcheck-services.nix @@ -0,0 +1,128 @@ +{ config, pkgs, lib, extendModules, ... }: +with lib; +let + options = { + test.shellcheckServices = mkOption { + readOnly = true; + description = '' + A derivation that runs shellcheck on all bash scripts included + in nix-bitcoin services. + ''; + default = shellcheckServices; + }; + }; + + # TODO-EXTERNAL: + # This can be removed when https://github.com/NixOS/nixpkgs/pull/189836 is merged. + # + # A list of all systemd service definitions and their locations, with format + # [ + # { + # file = ...; + # value = { postgresql = ...; }; + # } + # ... + # ] + systemdServiceDefs = + (extendModules { + modules = [ + { + # Currently, NixOS modules only allow accessing option definition locations + # via type.merge. + # Override option `systemd.services` and use it to return the list of service defs. + options.systemd.services = lib.mkOption { + type = lib.types.anything // { + merge = loc: defs: defs; + }; + }; + + # Disable all modules that define options.systemd.services so that these + # defs don't collide with our definition + disabledModules = [ + "system/boot/systemd.nix" + # These files amend option systemd.services + "testing/service-runner.nix" + "security/systemd-confinement.nix" + ]; + + config._module.check = false; + } + ]; + }).config.systemd.services; + + # A list of all service names that are defined by nix-bitcoin. + # [ "bitcoind", "clightning", ... ] + # + # Algorithm: Parse `systemdServiceDefs` and return all services that + # only have definitions located in the nix-bitcoin source. + nix-bitcoin-services = let + nix-bitcoin-source = toString ../..; + nbServices = collectServices true; + nonNbServices = collectServices false; + # Return set of services ({ service1 = true; service2 = true; ... }) + # which are either defined or not defined by nix-bitcoin, depending + # on `fromNixBitcoin`. + collectServices = fromNixBitcoin: lib.listToAttrs (builtins.concatLists (map (def: + let + isNbSource = lib.hasPrefix nix-bitcoin-source def.file; + in + # Nix has nor boolean XOR, so use `if` + lib.optionals (if fromNixBitcoin then isNbSource else !isNbSource) ( + (map (service: { name = service; value = true; }) (builtins.attrNames def.value)) + ) + ) systemdServiceDefs)); + in + # Set difference: nbServices - nonNbServices + builtins.filter (nbService: ! nonNbServices ? ${nbService}) (builtins.attrNames nbServices); + + # The concatenated list of values of ExecStart, ExecStop, ... (`scriptAttrs`) of all `nix-bitcoin-services`. + serviceCmds = let + scriptAttrs = [ + "ExecStartPre" + "ExecStart" + "ExecStartPost" + "ExecStop" + "ExecStopPost" + "ExecCondition" + "ExecReload" + ]; + services = config.systemd.services; + in + builtins.concatMap (serviceName: let + serviceCfg = services.${serviceName}.serviceConfig; + in + builtins.concatMap (attr: + lib.optionals (serviceCfg ? ${attr}) ( + let + cmd = serviceCfg.${attr}; + in + if builtins.typeOf cmd == "list" then cmd else [ cmd ] + ) + ) scriptAttrs + ) nix-bitcoin-services; + + # A list of all binaries included in `serviceCmds` + serviceBinaries = map (cmd: builtins.head ( + # Extract the first component (the binary). + # cmd can start with extra modifiers like `+` + builtins.match "[^/]*([^[:space:]]+).*" (toString cmd) + )) serviceCmds; + + shellcheckServices = pkgs.runCommand "shellcheck-services" { + inherit serviceBinaries; + # The `builtins.match` in `serviceBinaries` discards the string context, so we + # also have to add `serviceCmds` to the derivation. This ensures that all + # referenced nix paths are available to the builder. + inherit serviceCmds; + } '' + echo "Checked binaries:" + # Find and check all binaries that have a bash shebang + grep -Pl '\A#! *\S+bash\b' $serviceBinaries | while IFS= read -r script; do + echo "$script" + ${pkgs.shellcheck}/bin/shellcheck --shell bash "$script" + done | tee "$out" + ''; +in +{ + inherit options; +} diff --git a/test/lib/test-lib.nix b/test/lib/test-lib.nix index 1de2b18..9a67f51 100644 --- a/test/lib/test-lib.nix +++ b/test/lib/test-lib.nix @@ -1,6 +1,10 @@ { config, lib, ... }: with lib; { + imports = [ + ./shellcheck-services.nix + ]; + options = { test = { noConnections = mkOption { diff --git a/test/shellcheck.sh b/test/shellcheck.sh index f6b697e..887cc2e 100755 --- a/test/shellcheck.sh +++ b/test/shellcheck.sh @@ -2,6 +2,8 @@ set -euo pipefail . "${BASH_SOURCE[0]%/*}/../helper/run-in-nix-env" "shellcheck findutils gnugrep" "$@" +# Shellcheck all bash sources in this repo + cd "${BASH_SOURCE[0]%/*}/.." { # Skip .git dir in all find commands