Files
fips/testing/openwrt/package-test.sh
T
Johnathan Corgan 7671f0d59a Version release-candidate .ipk packages so opkg sorts them below the release
A release candidate is tagged vX.Y.Z-rcN, and the OpenWrt workflow
wrote that tag unchanged as the .ipk control Version. opkg compares
versions with dpkg's algorithm and reads the text after the last '-' as
a revision, so v0.5.3-rc1 sorted above v0.5.3 and a router that had
installed the candidate was never upgraded by the release.

The "Derive package version" step now also emits ipk_version, which
maps a candidate tag's -alphaN/-betaN/-preN/-rcN suffix to '~' (the
same mapping the .deb uses), giving v0.5.3~rc1. The leading 'v' is
kept: every released .ipk carries it, and under opkg 0.5.4 would sort
below v0.5.3, so dropping it would stop future releases upgrading.
Release tags and branch builds are unchanged.

build-ipk.sh takes the control Version from IPK_VERSION, falling back
to PKG_VERSION, and still names the file from PKG_VERSION so the asset
name keeps the tag's -rcN (a GitHub release renames assets with '~').

check-package-versions.sh gains an .ipk section that runs the step's
text for a release tag, an rc and a beta candidate tag and a branch,
checks the ordering against v0.5.2, v0.5.3 and v0.5.4 and of the beta
below the rc, and checks the job output and build step wiring.
package-test.sh builds the .ipk with IPK_VERSION removed from the
build's environment and with it set, and reads the control Version
back. It removes the first package before the second build and records
a missing file as a failed check, so a build-ipk.sh that named its file
from IPK_VERSION cannot pass by reading the first build's package, and
its exit trap also removes a package whose name carries the '~' form.
2026-10-01 22:41:14 +00:00

386 lines
16 KiB
Bash
Executable File

#!/bin/bash
# ── OpenWrt package contents, checked on the host ───────────────────────────
# Runs the real packaging/openwrt-apk/build-apk.sh against placeholder
# binaries and a stub `apk` that records what `apk mkpkg` was asked to
# package. No docker, no apk-tools and no FIPS build are needed.
#
# What it checks is which maintainer scripts the .apk registers and what each
# one does with apk's upgrade arguments and environment (apk-tools v3 passes
# "<new-version> <old-version>" and a PATH-only environment to pre-upgrade and
# post-upgrade), and which files the .apk and the .ipk install. The .ipk is
# built for real by build-ipk.sh, which needs only tar. Whether a real
# `apk mkpkg` accepts the result is left to the GitHub packaging workflow,
# which builds with the real tool.
#
# Usage: package-test.sh [--keep <dir>]
# --keep <dir> copy the captured apk scripts into <dir> as post-install,
# pre-upgrade, post-upgrade and pre-deinstall, so
# scenarios.sh can run them under ash.
#
# Exit 0 = every check passed. Exit 1 = at least one failed. Exit 2 = the
# harness could not run; never treated as a pass.
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
SCRIPTS_SRC="$PROJECT_ROOT/packaging/openwrt-ipk/scripts"
KEEP=""
while [[ $# -gt 0 ]]; do
case "$1" in
--keep)
[[ $# -ge 2 ]] || { echo "package-test: --keep needs a directory" >&2; exit 2; }
KEEP="$2"
shift 2
;;
*) echo "package-test: unknown argument: $1" >&2; exit 2 ;;
esac
done
if [[ -n "$KEEP" && ! -d "$KEEP" ]]; then
echo "package-test: --keep needs an existing directory" >&2
exit 2
fi
# The version is unique to this run so the cleanup below removes only what
# this run's builders wrote into dist/.
PKG_VERSION="pkgtest.$$"
TMP="$(mktemp -d)" || { echo "package-test: mktemp failed" >&2; exit 2; }
trap 'rm -rf "$TMP"; rm -f "$PROJECT_ROOT/dist/fips_${PKG_VERSION}"[_~]*' EXIT
harness_fail() {
echo "package-test: $*" >&2
exit 2
}
FAILURES=0
CASES=0
ok() {
CASES=$((CASES + 1))
echo " ok $*"
return 0
}
bad() {
CASES=$((CASES + 1))
FAILURES=$((FAILURES + 1))
echo " FAIL $*"
return 0
}
# ── Build the .apk against a stub apk ───────────────────────────────────────
BINS="$TMP/bins"
CAPTURE="$TMP/capture"
mkdir -p "$BINS" "$CAPTURE" || harness_fail "cannot create $TMP subdirectories"
for bin in fips fipsctl fipstop fips-gateway; do
printf 'x' > "$BINS/$bin" || harness_fail "cannot write placeholder $bin"
done
# The stub knows only the arguments build-apk.sh passes today. Anything else
# exits 64, so a new mkpkg argument fails the build rather than going unseen.
cat > "$TMP/apk" <<STUB
#!/bin/bash
set -euo pipefail
cap='$CAPTURE'
[ "\${1-}" = mkpkg ] || { echo "stub apk: only mkpkg is supported, got '\${1-}'" >&2; exit 64; }
shift
files=""
out=""
while [ \$# -gt 0 ]; do
[ \$# -ge 2 ] || { echo "stub apk: \$1 has no value" >&2; exit 64; }
case "\$1" in
--info) ;;
--script)
phase="\${2%%:*}"
cp "\${2#*:}" "\$cap/script.\$phase"
echo "\$phase" >> "\$cap/phases"
;;
--files) files="\$2" ;;
--output) out="\$2" ;;
*) echo "stub apk: unknown argument \$1" >&2; exit 64 ;;
esac
shift 2
done
[ -n "\$files" ] && [ -n "\$out" ] || { echo "stub apk: --files and --output are required" >&2; exit 64; }
(cd "\$files" && find . -mindepth 1 | LC_ALL=C sort) > "\$cap/payload"
: > "\$out"
: > "\$cap/called"
STUB
chmod 0755 "$TMP/apk" || harness_fail "cannot make the stub apk executable"
echo "OpenWrt package checks"
echo "==> build-apk.sh with a stub apk"
if ! PKG_VERSION="$PKG_VERSION" APK_VERSION=0.0.0-r0 APK_BIN="$TMP/apk" \
bash "$PROJECT_ROOT/packaging/openwrt-apk/build-apk.sh" --arch x86_64 --bin-dir "$BINS" \
> "$TMP/build-apk.log" 2>&1; then
cat "$TMP/build-apk.log" >&2
harness_fail "build-apk.sh failed, so nothing was checked"
fi
[[ -f "$CAPTURE/called" ]] || harness_fail "build-apk.sh exited 0 but never ran apk mkpkg"
[[ -f "$CAPTURE/phases" ]] || harness_fail "apk mkpkg ran but no script phase was recorded"
# ── A1. The .apk registers exactly the four lifecycle scripts ───────────────
# apk runs only post-install on a fresh install, only pre-upgrade and
# post-upgrade on an upgrade, and only pre-deinstall on a removal.
WANT_PHASES="post-install post-upgrade pre-deinstall pre-upgrade"
got_phases="$(LC_ALL=C sort "$CAPTURE/phases" | tr '\n' ' ')"
got_phases="${got_phases% }"
if [[ "$got_phases" == "$WANT_PHASES" ]]; then
ok "A1 the .apk registers $WANT_PHASES"
else
missing=""
for phase in $WANT_PHASES; do
grep -qxF "$phase" "$CAPTURE/phases" || missing="$missing $phase"
done
bad "A1 registered phases are '$got_phases', want '$WANT_PHASES'; missing:${missing:- none}"
fi
# ── A2. Every registered script starts with a working #! line ───────────────
# apk execs the script directly, so the kernel reads line 1.
for phase in $WANT_PHASES; do
script="$CAPTURE/script.$phase"
if [[ ! -f "$script" ]]; then
bad "A2 $phase is not registered, so it has no #! line"
elif [[ "$(head -n 1 "$script")" == "#!/bin/sh" ]]; then
ok "A2 $phase starts with #!/bin/sh"
else
bad "A2 $phase starts with '$(head -n 1 "$script")', not #!/bin/sh"
fi
done
# ── A3. Install and removal ship the shared bodies unchanged ────────────────
for pair in post-install:postinst pre-deinstall:prerm; do
phase="${pair%%:*}"
src="$SCRIPTS_SRC/${pair#*:}"
if cmp -s "$CAPTURE/script.$phase" "$src"; then
ok "A3 $phase is the shipped ${pair#*:}, byte for byte"
else
bad "A3 $phase differs from the shipped ${pair#*:}"
fi
done
# ── A4 and A5. The upgrade pair wraps the shared bodies ─────────────────────
# A4 reads the structure: the script ends with the body after its #! line, and
# at least one header line sits between the two. A5 runs the header with apk's
# upgrade argv and environment, so what is judged is what the shell does with
# it, not how it reads.
for pair in pre-upgrade:prerm post-upgrade:postinst; do
phase="${pair%%:*}"
src="$SCRIPTS_SRC/${pair#*:}"
script="$CAPTURE/script.$phase"
if [[ ! -f "$script" ]]; then
bad "A4 $phase is not registered"
bad "A5 $phase is not registered, so its header cannot run"
continue
fi
tail -n +2 "$src" > "$TMP/body" || harness_fail "cannot read $src"
body_lines=$(wc -l < "$TMP/body")
script_lines=$(wc -l < "$script")
header_lines=$((script_lines - body_lines))
if [[ $header_lines -lt 2 ]]; then
bad "A4 $phase has $header_lines header line(s) before the ${pair#*:} body, want at least 2"
elif ! tail -n "$body_lines" "$script" | cmp -s - "$TMP/body"; then
bad "A4 $phase does not end with the ${pair#*:} body"
else
ok "A4 $phase is a $header_lines-line header and the ${pair#*:} body"
fi
if [[ $header_lines -lt 1 ]]; then
bad "A5 $phase has no header to run"
continue
fi
probe="$TMP/probe.$phase"
{
head -n "$header_lines" "$script"
# shellcheck disable=SC2016
printf '%s\n' 'printf '\''%s|%s|%s\n'\'' "${1-}" "${2-}" "${PKG_UPGRADE-}"'
} > "$probe"
chmod 0755 "$probe" || harness_fail "cannot make the $phase probe executable"
got="$(env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin "$probe" 0.6.0-r1 0.5.2-r1)"
rc=$?
if [[ $rc -eq 126 || $rc -eq 127 ]]; then
harness_fail "the $phase probe could not be executed (exit $rc)"
fi
IFS='|' read -r f1 f2 f3 <<< "$got"
case "$phase" in
pre-upgrade)
if [[ $rc -eq 0 && "$f1|$f2" == "upgrade|0.6.0-r1" ]]; then
ok "A5 pre-upgrade hands the body 'upgrade 0.6.0-r1'"
else
bad "A5 pre-upgrade hands the body '$f1 $f2' (exit $rc), want 'upgrade 0.6.0-r1'"
fi
;;
post-upgrade)
if [[ $rc -eq 0 && "$f3" == "1" ]]; then
ok "A5 post-upgrade runs the body with PKG_UPGRADE=1"
else
bad "A5 post-upgrade runs the body with PKG_UPGRADE='$f3' (exit $rc), want 1"
fi
;;
esac
done
# ── Build the .ipk ──────────────────────────────────────────────────────────
echo "==> build-ipk.sh"
# The first build is V1's "IPK_VERSION unset" case, so it must not inherit an
# IPK_VERSION from the caller's environment.
if ! env -u IPK_VERSION PKG_VERSION="$PKG_VERSION" \
bash "$PROJECT_ROOT/packaging/openwrt-ipk/build-ipk.sh" --arch x86_64 --bin-dir "$BINS" \
> "$TMP/build-ipk.log" 2>&1; then
cat "$TMP/build-ipk.log" >&2
harness_fail "build-ipk.sh failed, so the .ipk was not checked"
fi
IPK="$PROJECT_ROOT/dist/fips_${PKG_VERSION}_x86_64.ipk"
[[ -f "$IPK" ]] || harness_fail "build-ipk.sh exited 0 but wrote no $IPK"
tar -xzf "$IPK" -O ./data.tar.gz | tar -tzf - > "$TMP/ipk-data" \
|| harness_fail "cannot list data.tar.gz in $IPK"
tar -xzf "$IPK" -O ./control.tar.gz | tar -tzf - > "$TMP/ipk-control" \
|| harness_fail "cannot list control.tar.gz in $IPK"
# Print the Version field of an .ipk's control file.
ipk_version() {
tar -xzf "$1" -O ./control.tar.gz | tar -xzOf - ./control \
| sed -n 's/^Version: //p'
}
# ── V1 and V2. The control Version comes from IPK_VERSION ───────────────────
# The workflow passes a candidate's opkg-sortable version (vX.Y.Z~rcN) as
# IPK_VERSION and keeps the tag's form in PKG_VERSION for the file name, since
# a GitHub release renames an asset with '~' in its name. Unset, IPK_VERSION
# falls back to PKG_VERSION, as a local build expects.
got="$(ipk_version "$IPK")" || harness_fail "cannot read the control file in $IPK"
if [[ "$got" == "$PKG_VERSION" ]]; then
ok "V1 with IPK_VERSION unset the control Version is PKG_VERSION ($got)"
else
bad "V1 with IPK_VERSION unset the control Version is '$got', want $PKG_VERSION"
fi
# The first build's package is removed so a second build that names its file
# from anything but PKG_VERSION cannot be passed by reading the stale one.
RC_VERSION="${PKG_VERSION}~rc1"
rm -f "$IPK" || harness_fail "cannot remove $IPK before the second build"
if ! PKG_VERSION="$PKG_VERSION" IPK_VERSION="$RC_VERSION" \
bash "$PROJECT_ROOT/packaging/openwrt-ipk/build-ipk.sh" --arch x86_64 --bin-dir "$BINS" \
> "$TMP/build-ipk-rc.log" 2>&1; then
cat "$TMP/build-ipk-rc.log" >&2
harness_fail "build-ipk.sh failed with IPK_VERSION set"
fi
if [[ ! -f "$IPK" ]]; then
bad "V2 with IPK_VERSION set build-ipk.sh wrote no $IPK; the file name must come from PKG_VERSION"
else
got="$(ipk_version "$IPK")" || harness_fail "cannot read the control file in $IPK"
if [[ "$got" == "$RC_VERSION" ]]; then
ok "V2 with IPK_VERSION set the control Version is IPK_VERSION ($got), file named from PKG_VERSION"
else
bad "V2 with IPK_VERSION set the control Version is '$got', want $RC_VERSION"
fi
fi
# ── P1 and P2. Neither package ships the dnsmasq drop-in ────────────────────
# OpenWrt's dnsmasq builds its config from UCI and reads no directory under
# /etc, so .fips forwarding comes from the UCI entry 90-fips-setup adds. The
# match is on the directory prefix: tar lists the directory with a trailing
# slash and the stub's find without one.
for pair in "P1:apk:$CAPTURE/payload" "P2:ipk:$TMP/ipk-data"; do
id="${pair%%:*}"
rest="${pair#*:}"
kind="${rest%%:*}"
listing="${rest#*:}"
hits="$(grep -F './etc/dnsmasq.d' "$listing" | tr '\n' ' ')"
if [[ -z "$hits" ]]; then
ok "$id the .$kind installs nothing under /etc/dnsmasq.d"
else
bad "$id the .$kind still installs: $hits"
fi
done
# ── P3. Positive control for P1 and P2 ──────────────────────────────────────
# An empty or unreadable listing would pass P1 and P2, so each listing must
# show files that are known to ship.
for pair in "apk:$CAPTURE/payload" "ipk:$TMP/ipk-data"; do
kind="${pair%%:*}"
listing="${pair#*:}"
for path in ./etc/init.d/fips-gateway ./etc/uci-defaults/90-fips-setup; do
if grep -qxF "$path" "$listing"; then
ok "P3 the .$kind payload lists $path"
else
bad "P3 the .$kind payload does not list $path, so P1/P2 saw no real listing"
fi
done
done
for path in ./postinst ./prerm; do
if grep -qxF "$path" "$TMP/ipk-control"; then
ok "P3 the .ipk control archive lists $path"
else
bad "P3 the .ipk control archive does not list $path"
fi
done
# ── P4. No source still names the drop-in ───────────────────────────────────
# With P5, one of the two checks on the SDK feed Makefile, which nothing here
# builds.
# grep exits 1 when nothing matches and 2 when it could not read a path; only
# the first is a pass.
(cd "$PROJECT_ROOT" && grep -rlF 'dnsmasq.d/fips.conf' \
packaging/openwrt-ipk packaging/openwrt-apk .github/workflows/package-openwrt.yml) \
> "$TMP/refs"
rc=$?
[[ $rc -le 1 ]] || harness_fail "the drop-in reference search failed (grep exit $rc)"
refs="$(tr '\n' ' ' < "$TMP/refs")"
if [[ -z "$refs" ]]; then
ok "P4 no OpenWrt packaging file names the dnsmasq drop-in"
else
bad "P4 the dnsmasq drop-in is still named in: $refs"
fi
if [[ -e "$PROJECT_ROOT/packaging/openwrt-ipk/files/etc/dnsmasq.d/fips.conf" ]]; then
bad "P4 packaging/openwrt-ipk/files/etc/dnsmasq.d/fips.conf still exists"
else
ok "P4 the drop-in source file is gone"
fi
# ── P5. The SDK feed Makefile ships the same maintainer scripts ─────────────
# Read, not built: building it needs the OpenWrt SDK. Each script must be the
# file in scripts/ that the scenarios run, and fips.yaml must be a conffile.
MAKEFILE="$PROJECT_ROOT/packaging/openwrt-ipk/Makefile"
[[ -r "$MAKEFILE" ]] || harness_fail "cannot read $MAKEFILE"
for script in preinst postinst prerm; do
if awk -v want="define Package/fips/$script" -v body="\$(file < \$(CURDIR)/scripts/$script)" '
$0 == want { inside = 1; next }
inside && $0 == body { found = 1 }
inside && $0 == "endef" { inside = 0 }
END { exit !found }' "$MAKEFILE"; then
ok "P5 the SDK Makefile's $script is scripts/$script"
else
bad "P5 the SDK Makefile does not define Package/fips/$script as scripts/$script"
fi
done
if awk '$0 == "define Package/fips/conffiles" { inside = 1; next }
inside && $0 == "/etc/fips/fips.yaml" { found = 1 }
inside && $0 == "endef" { inside = 0 }
END { exit !found }' "$MAKEFILE"; then
ok "P5 the SDK Makefile lists /etc/fips/fips.yaml as a conffile"
else
bad "P5 the SDK Makefile does not list /etc/fips/fips.yaml as a conffile"
fi
# ── Hand the scripts to the ash scenarios ───────────────────────────────────
if [[ -n "$KEEP" ]]; then
for phase in $WANT_PHASES; do
[[ -f "$CAPTURE/script.$phase" ]] || continue
install -m 0755 "$CAPTURE/script.$phase" "$KEEP/$phase" \
|| harness_fail "cannot copy $phase into $KEEP"
done
fi
echo ""
if [[ $FAILURES -eq 0 ]]; then
echo "package-test: all $CASES checks passed"
exit 0
fi
echo "package-test: $FAILURES of $CASES checks failed"
exit 1