SPS · log · files · refs · tags · releases · people · languages · GitHub · commit
ab9f886
src: add 'check' command to validate recipes without building
Parents
Stat
bin/src | 121 ++++++++++++++++++++++++++++++++++++++++++++++++ man/src.1 | 13 ++++++ tests/test_src_check.sh | 97 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+)
Diff
diff --git a/bin/src b/bin/src
index f91cc16..c7a8531 100755
--- a/bin/src
+++ b/bin/src
@@ -24,6 +24,7 @@ sps_tab=${sps_tab%X}
src_usage()
{
printf '%s\n' 'usage: src update'
+ printf '%s\n' ' src check RECIPE... [--deps]'
printf '%s\n' ' src search PATTERN [--raw]'
printf '%s\n' ' src show PACKAGE [--raw]'
printf '%s\n' ' src which PACKAGE'
@@ -407,11 +408,131 @@ src_query()
-v program=src -f "$SPS_LIBDIR/index.awk" "$sps_index"
}
+# src_check validates one or more recipes with the same parser that src update
+# uses, without downloading sources or running any build phase. It accepts a
+# recipe file or a directory containing a 'recipe' file. A failed recipe is
+# reported and the command exits nonzero, so it is safe to gate automation on.
+src_check()
+{
+ src_check_deps=0
+ src_check_recipes=
+ while [ "$#" -gt 0 ]; do
+ case $1 in
+ --deps)
+ src_check_deps=1
+ ;;
+ --help|-h)
+ printf '%s\n' 'usage: src check RECIPE... [--deps]'
+ exit 0
+ ;;
+ -*)
+ src_usage >&2
+ exit "$SPS_EX_USAGE"
+ ;;
+ *)
+ if [ -d "$1" ] && [ ! -L "$1" ]; then
+ src_check_resolve="$1/recipe"
+ else
+ src_check_resolve=$1
+ fi
+ if [ ! -f "$src_check_resolve" ] ||
+ [ -L "$src_check_resolve" ] ||
+ [ ! -r "$src_check_resolve" ]; then
+ sps_die "$SPS_EX_NOTFOUND" \
+ "no readable recipe file at '$1'"
+ fi
+ src_check_recipes="$src_check_recipes $src_check_resolve"
+ ;;
+ esac
+ shift
+ done
+ [ -n "$src_check_recipes" ] || {
+ printf '%s\n' 'usage: src check RECIPE... [--deps]' >&2
+ exit "$SPS_EX_USAGE"
+ }
+
+ if [ "$src_check_deps" -eq 1 ]; then
+ src_check_work=$(sps_mktemp_dir "$SPS_BUILD/.check") || exit $?
+ src_check_index=$src_check_work/packages.index
+ else
+ src_check_index=/dev/null
+ fi
+ src_check_failed=0
+ src_check_count=0
+ src_check_arch=${SPS_ARCH:-$(uname -m 2>/dev/null || printf unknown)}
+ for src_check_recipe in $src_check_recipes; do
+ [ -n "$src_check_recipe" ] || continue
+ src_check_count=$((src_check_count + 1))
+ src_check_digest=$(sps_definition_sha256 "$src_check_recipe") || {
+ printf 'check: FAILED %s (definition digest)\n' \
+ "$src_check_recipe" >&2
+ src_check_failed=1
+ continue
+ }
+ src_check_name=$(awk -v recipe_path="$src_check_recipe" '
+ /^[ \t]*name[ \t]+/ {
+ line = $0
+ sub(/^[ \t]*name[ \t]+/, "", line)
+ sub(/[ \t\r]+$/, "", line)
+ print line
+ exit
+ }
+ ' "$src_check_recipe")
+ if [ "$src_check_deps" -eq 1 ]; then
+ src_check_target=$src_check_index
+ else
+ src_check_target=/dev/null
+ fi
+ if awk -v recipe_repo=check -v recipe_priority=100 \
+ -v recipe_path="$src_check_recipe" \
+ -v recipe_sha256="$src_check_digest" \
+ -v recipe_default_arch="$src_check_arch" \
+ -f "$SPS_LIBDIR/recipe.awk" "$src_check_recipe" \
+ >> "$src_check_target" 2>/dev/null; then
+ printf 'check: ok %s (%s)\n' "$src_check_recipe" \
+ "$src_check_name"
+ else
+ printf 'check: FAILED %s (%s)\n' "$src_check_recipe" \
+ "$src_check_name" >&2
+ src_check_failed=1
+ fi
+ done
+
+ if [ "$src_check_failed" -ne 0 ]; then
+ rm -f "$src_check_index" 2>/dev/null || :
+ rmdir "$src_check_work" 2>/dev/null || :
+ sps_die "$SPS_EX_PACKAGE" \
+ "$src_check_count recipe(s) checked: at least one failed"
+ fi
+
+ if [ "$src_check_deps" -eq 1 ]; then
+ if ! awk -v action=validate -v program=src \
+ -f "$SPS_LIBDIR/index.awk" "$src_check_index"; then
+ rmdir "$src_check_work" 2>/dev/null || :
+ sps_die "$SPS_EX_PACKAGE" "generated check index is malformed"
+ fi
+ if ! awk -v action=validate -v program=src \
+ -f "$SPS_LIBDIR/version.awk" -f "$SPS_LIBDIR/deps.awk" \
+ "$src_check_index"; then
+ rmdir "$src_check_work" 2>/dev/null || :
+ sps_die "$SPS_EX_DEPENDENCY" "checked dependency graph is invalid"
+ fi
+ rm -f "$src_check_index" 2>/dev/null || :
+ rmdir "$src_check_work" 2>/dev/null || :
+ fi
+
+ printf 'checked %d recipe(s)\n' "$src_check_count"
+}
+
case ${1-} in
update)
shift
src_update "$@"
;;
+ check)
+ shift
+ src_check "$@"
+ ;;
search|show|which)
src_command=$1
shift
diff --git a/man/src.1 b/man/src.1
index a4a1e26..7e5ca88 100644
--- a/man/src.1
+++ b/man/src.1
@@ -4,6 +4,7 @@ src \- update and query SPS source repository indexes
.SH SYNOPSIS
.nf
src update
+src check RECIPE... [--deps]
src search PATTERN [--raw]
src show PACKAGE [--raw]
src which PACKAGE
@@ -23,6 +24,18 @@ fast-forward safe tracking branches, scan configured trees, validate recipes
and the selected dependency graph, and atomically replace the local package
index. Invalid input leaves the previous index in place.
.TP
+.BI check " recipe ..."
+Validate each recipe with the same parser used by
+.B update
+without downloading sources or running any build phase. Each argument is a
+recipe file or a directory containing a
+.B recipe
+file. Failed recipes are reported and the command exits nonzero, so it is safe
+to gate automation on it. With
+.B --deps
+a temporary index of the checked recipes is also validated as a dependency
+graph.
+.TP
.BI search " pattern"
Search indexed names and descriptions.
.TP
diff --git a/tests/test_src_check.sh b/tests/test_src_check.sh
new file mode 100644
index 0000000..aee1ccc
--- /dev/null
+++ b/tests/test_src_check.sh
@@ -0,0 +1,97 @@
+#!/bin/sh
+set -eu
+
+case $0 in */*) test_dir=${0%/*} ;; *) test_dir=. ;; esac
+project_dir=$(CDPATH= cd "$test_dir/.." 2>/dev/null && pwd)
+src=$project_dir/bin/src
+
+fail()
+{
+ printf 'test_src_check: %s\n' "$*" >&2
+ exit 1
+}
+
+expect_status()
+{
+ expected=$1
+ shift
+ set +e
+ "$@" >"$tmp/out" 2>"$tmp/err"
+ got=$?
+ set -e
+ [ "$got" -eq "$expected" ] ||
+ fail "expected status $expected, got $got for: $*"
+}
+
+tmp=$(mktemp -d "${TMPDIR:-/tmp}/sps-src-check.XXXXXX")
+trap 'rm -rf "$tmp"' 0 1 2 3 15
+mkdir -p "$tmp/root" "$tmp/cache" "$tmp/db" "$tmp/build" "$tmp/ok" "$tmp/bad" \
+ "$tmp/missing-dep"
+
+SPS_ROOT=$tmp/root
+SPS_DB=$tmp/db
+SPS_CACHE=$tmp/cache
+SPS_BUILD=$tmp/build
+SPS_CONFIG=/dev/null
+SPS_REPOS_CONFIG=/dev/null
+export SPS_ROOT SPS_DB SPS_CACHE SPS_BUILD SPS_CONFIG SPS_REPOS_CONFIG
+
+# A complete, valid recipe passes.
+cat >"$tmp/ok/recipe" <<'EOF'
+name validpkg
+version 1.0
+release 2
+description A complete, valid test recipe
+source https://example.invalid/validpkg-1.0.tar.gz
+hash sha256:0000000000000000000000000000000000000000000000000000000000000000
+install true
+EOF
+expect_status 0 "$src" check "$tmp/ok"
+$src check "$tmp/ok" >/dev/null 2>&1 || fail "valid recipe rejected"
+$src check "$tmp/ok/recipe" | grep -q '^check: ok ' ||
+ fail "valid recipe was not reported ok"
+
+# A directory argument resolves to its recipe file.
+$src check "$tmp/ok/recipe" >/dev/null 2>&1 || fail "file recipe rejected"
+
+# Missing the required install command is a syntax failure.
+cat >"$tmp/bad/recipe" <<'EOF'
+name badpkg
+version 1.0
+release 1
+description Recipe missing its install command
+EOF
+expect_status 4 "$src" check "$tmp/bad"
+
+# A nonexistent path is reported and exits with the not-found status.
+expect_status 3 "$src" check "$tmp/nonexistent"
+
+# --help exits cleanly.
+expect_status 0 "$src" check --help
+
+# An unknown option is a usage error.
+expect_status 2 "$src" check --no-such-option "$tmp/ok"
+
+# --deps validates a self-consistent graph.
+cat >"$tmp/missing-dep/recipe" <<'EOF'
+name needsnothing
+version 1
+release 1
+description No dependencies to declare
+install true
+EOF
+expect_status 0 "$src" check --deps "$tmp/missing-dep"
+
+# --deps flags an unresolvable declared dependency.
+cat >"$tmp/bad/deps" <<'EOF'
+name needsghost
+version 1
+release 1
+description Declares a dependency that is not in the checked set
+depend ghost-pkg
+install true
+EOF
+mv "$tmp/bad/deps" "$tmp/bad/recipe2"
+expect_status 5 "$src" check --deps "$tmp/bad/recipe2"
+
+printf '%s\n' 'test_src_check: ok'