#!/usr/bin/env bash # Reproduces every executed claim in # https://www.heatware.net/linux/rpm-installation-dependencies/ # # Builds two throwaway RPMs and installs them into a SCRATCH rpm database created with # --dbpath inside a temp directory. It never touches the system rpm database, never uses # sudo, and never contacts a repository. The packages' only payload is a text file under # /tmp/hwrpmdemo, which is removed on exit along with everything else. # # The dnf/yum/dnf5/alien statements in the article are documentation, not executable here; # this script reports them as SKIP rather than pretending otherwise. # # Usage: bash verify-rpm-installation-dependencies.sh # Runtime: under 20 seconds. set -u W="$(mktemp -d -t hwrpm)" PAYLOAD_DIR="$W/payload" trap 'cd /; rm -rf "$W" "$PAYLOAD_ROOT" 2>/dev/null' EXIT PASS=0; FAIL=0; SKIP=0; CONTENT=0 pass() { PASS=$((PASS+1)); printf 'PASS %s\n' "$1"; } fail() { FAIL=$((FAIL+1)); printf 'FAIL %s | expected: %s | actual: %s\n' "$1" "$2" "$3"; } skip() { SKIP=$((SKIP+1)); printf 'SKIP %s (needs %s)\n' "$1" "$2"; } content() { CONTENT=$((CONTENT+1)); printf 'CONTENT %s\n' "$1"; } check() { [ "$2" = "$3" ] && pass "$1" || fail "$1" "$2" "$3"; } has() { case "$3" in *"$2"*) pass "$1";; *) fail "$1" "output containing: $2" "$(printf '%s' "$3" | tr '\n' ' ')";; esac; } if ! command -v rpm >/dev/null 2>&1 || ! command -v rpmbuild >/dev/null 2>&1; then echo "platform: $(uname -s) $(uname -r) $(uname -m)" skip "the whole rpm dependency walkthrough" "rpm and rpmbuild on PATH (brew install rpm)" echo echo "PASS=$PASS FAIL=$FAIL SKIP=$SKIP CONTENT=$CONTENT" exit 0 fi echo "platform: $(uname -s) $(uname -r) $(uname -m)" echo "rpm: $(rpm --version)" echo "rpmbuild: $(rpmbuild --version)" echo # Payload path is unique per run so two copies of this script cannot collide. PAYLOAD_ROOT="/tmp/hwrpmdemo.$$" mkdir -p "$W"/{SPECS,BUILD,RPMS,SOURCES,SRPMS,db} cat > "$W/SPECS/hwlib1.spec" < %{buildroot}$PAYLOAD_ROOT/lib1.txt %files $PAYLOAD_ROOT/lib1.txt EOS cat > "$W/SPECS/hwdemo.spec" <= 1.0 Requires: hwlib2 %description Depends on hwlib1 (built here) and hwlib2 (deliberately never built). %install mkdir -p %{buildroot}$PAYLOAD_ROOT echo app > %{buildroot}$PAYLOAD_ROOT/app.txt %files $PAYLOAD_ROOT/app.txt EOS for s in hwlib1 hwdemo; do rpmbuild --define "_topdir $W" -bb "$W/SPECS/$s.spec" > "$W/$s.build.log" 2>&1 || { echo "rpmbuild failed for $s; last lines:"; tail -15 "$W/$s.build.log" skip "the rpm dependency walkthrough" "a working rpmbuild on this platform" echo; echo "PASS=$PASS FAIL=$FAIL SKIP=$SKIP CONTENT=$CONTENT"; exit 0 } done LIB1=$(find "$W/RPMS" -name 'hwlib1-*.rpm' | head -1) DEMO=$(find "$W/RPMS" -name 'hwdemo-*.rpm' | head -1) [ -n "$LIB1" ] && [ -n "$DEMO" ] || { echo "packages not produced"; exit 1; } pass "rpmbuild produced both scratch packages" DB="$W/db" R() { rpm --dbpath "$DB" "$@"; } # ---------------------------------------------------------------- claim 1 # -qp reads requirements and provides straight out of a package FILE. OUT=$(R -qp --requires "$DEMO" 2>&1) has "rpm -qp --requires lists 'hwlib1 >= 1.0'" "hwlib1 >= 1.0" "$OUT" has "rpm -qp --requires lists the unbuilt 'hwlib2'" "hwlib2" "$OUT" has "rpm -qp --requires also lists rpmlib() internal requirements" "rpmlib(" "$OUT" OUT=$(R -qp --provides "$LIB1" 2>&1) has "rpm -qp --provides shows hwlib1 = 1.0" "hwlib1 = 1.0" "$OUT" OUT=$(R -qp -l "$DEMO" 2>&1) has "rpm -qlp lists the package's files without installing it" "app.txt" "$OUT" # ---------------------------------------------------------------- claim 2 # rpm 6 enforces signature checking by default (rpm 4 did not). LEVEL=$(rpm -E '%{?_pkgverify_level}' 2>/dev/null) echo " _pkgverify_level on this rpm: [$LEVEL]" R --initdb >/dev/null 2>&1 UNSIGNED=$(R -i "$LIB1" 2>&1); URC=$? case "$UNSIGNED" in *"no signature"*|*"NOKEY"*|*"not signed"*) pass "this rpm refuses an unsigned package by default (exit $URC): $(printf '%s' "$UNSIGNED" | tr -d '\t' | tr '\n' ' ')" ;; *) if [ "$URC" -eq 0 ]; then skip "signature enforcement by default" "rpm 6.x (this rpm installed the unsigned package)" R -e hwlib1 >/dev/null 2>&1 else fail "an unsigned package is refused with a signature error" "a 'no signature' message" "$(printf '%s' "$UNSIGNED" | tr '\n' ' ')" fi ;; esac # From here on --nosignature is used, because these scratch packages have no key. N() { rpm --dbpath "$DB" --nosignature "$@"; } # ---------------------------------------------------------------- claim 3 # rpm -i reports unmet dependencies and refuses; it does not resolve them. OUT=$(N -i "$DEMO" 2>&1); RC=$? [ "$RC" -ne 0 ] && pass "rpm -i refuses hwdemo with unmet dependencies (exit $RC)" \ || fail "rpm -i refuses hwdemo with unmet dependencies" "non-zero" "$RC" has "the error is 'Failed dependencies'" "Failed dependencies" "$OUT" has "it names hwlib1 >= 1.0" "hwlib1 >= 1.0 is needed by" "$OUT" has "it names hwlib2" "hwlib2 is needed by" "$OUT" # ---------------------------------------------------------------- claim 4 # Installing one dependency leaves the other unmet — rpm never goes looking. N -i "$LIB1" >/dev/null 2>&1; RC=$? check "hwlib1 installs cleanly on its own" "0" "$RC" OUT=$(N -i "$DEMO" 2>&1); RC=$? [ "$RC" -ne 0 ] && pass "hwdemo still refuses: rpm did not fetch the remaining dependency" \ || fail "hwdemo still refuses after hwlib1 is installed" "non-zero" "$RC" case "$OUT" in *"hwlib1"*) fail "the remaining error mentions only hwlib2" "no hwlib1 in the message" "$(printf '%s' "$OUT" | tr '\n' ' ')" ;; *"hwlib2"*) pass "the remaining error mentions only hwlib2, so the first dependency was genuinely satisfied" ;; *) fail "the remaining error mentions hwlib2" "hwlib2 is needed by" "$(printf '%s' "$OUT" | tr '\n' ' ')" ;; esac # ---------------------------------------------------------------- claim 5 # --nodeps installs anyway and leaves an unsatisfiable requirement in the db. N --nodeps -i "$DEMO" >/dev/null 2>&1; RC=$? check "rpm -i --nodeps installs despite the missing dependency" "0" "$RC" OUT=$(R -qa 2>&1) has "hwdemo is now recorded as installed" "hwdemo-1.0-1" "$OUT" OUT=$(R -q --requires hwdemo 2>&1) has "the database still records the hwlib2 requirement" "hwlib2" "$OUT" OUT=$(R -q --whatprovides hwlib2 2>&1) has "and nothing on the system provides it" "no package provides" "$OUT" # ---------------------------------------------------------------- claim 6 # Reverse dependencies block erasure; --nodeps forces it and breaks the tree. OUT=$(N -e hwlib1 2>&1); RC=$? [ "$RC" -ne 0 ] && pass "rpm -e refuses to remove hwlib1 while hwdemo requires it (exit $RC)" \ || fail "rpm -e refuses to remove a required package" "non-zero" "$RC" has "the message marks the conflict as (installed)" "(installed)" "$OUT" N -e --nodeps hwlib1 >/dev/null 2>&1; RC=$? check "rpm -e --nodeps removes it anyway" "0" "$RC" OUT=$(R -qa 2>&1) case "$OUT" in *hwlib1*) fail "hwlib1 is gone from the database" "no hwlib1" "$OUT" ;; *hwdemo*) pass "hwdemo is left installed with a dependency that is no longer present" ;; *) fail "hwdemo is left installed" "hwdemo in rpm -qa" "$OUT" ;; esac # ---------------------------------------------------------------- claim 7 (merged from install-rpm-guide) # rpm -K / --checksig on an unsigned package: must NOT say "md5 gpg OK" (old FAQ claim). SIGOUT=$(rpm -K "$LIB1" 2>&1) case "$SIGOUT" in *"md5 gpg OK"*) fail "rpm -K on an unsigned package" "no 'md5 gpg OK' string" "$SIGOUT" ;; *"NOT OK"*) pass "rpm -K on an unsigned package reports SIGNATURES NOT OK, not the old FAQ's 'md5 gpg OK'" ;; *) fail "rpm -K on an unsigned package" "output containing NOT OK" "$SIGOUT" ;; esac CSOUT=$(rpm --checksig "$LIB1" 2>&1) has "rpm --checksig agrees with rpm -K" "NOT OK" "$CSOUT" # ---------------------------------------------------------------- claim 8 (merged from install-rpm-guide) # rpm --querytags: at least one tag, and the article's own environment produced 267. TAGCOUNT=$(rpm --querytags 2>/dev/null | wc -l | tr -d ' ') if [ "${TAGCOUNT:-0}" -gt 0 ]; then pass "rpm --querytags lists $TAGCOUNT tags (this rewrite's environment measured 267; count may vary by rpm build)" else fail "rpm --querytags" "at least one tag" "$TAGCOUNT" fi # ---------------------------------------------------------------- not executable here skip "dnf/yum resolving the same dependency from a repository" "a Fedora/RHEL host with dnf and enabled repos" skip "alien converting an RPM on Ubuntu" "an Ubuntu or Debian host" # ---------------------------------------------------------------- content regression guards SCRIPT_DIR=$(cd "$(dirname "$0")" 2>/dev/null && pwd) || SCRIPT_DIR="$(pwd)" ARTICLE="$SCRIPT_DIR/rpm-installation-dependencies.html" if [ -f "$ARTICLE" ]; then BODY="$(awk 'BEGIN{skip=0} /^/){skip=0}; next} {print}' "$ARTICLE")" if printf '%s' "$BODY" | grep -qF "md5 gpg OK"; then PRE_BLOCKS="$(printf '%s' "$BODY" | awk 'BEGIN{RS=""} {print}' | grep -oE '
.*' 2>/dev/null)"
    if printf '%s' "$PRE_BLOCKS" | grep -qF "md5 gpg OK"; then
      fail "'md5 gpg OK' should not appear inside a code block as if it were real output" "no such string inside 
" "found"
    else
      content "'md5 gpg OK' named only in corrective prose, never shown as real output"
    fi
  else
    content "'md5 gpg OK' not present at all"
  fi

  REALHOST="$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo __no_host__)"
  if [ -n "$REALHOST" ] && printf '%s' "$BODY" | grep -qiF -- "$REALHOST"; then
    fail "article should not contain this machine's real hostname" "no hostname" "found $REALHOST"
  else
    content "article does not contain this machine's real hostname"
  fi
else
  skip "article-text checks" "rpm-installation-dependencies.html next to this script"
fi

# Clean the scratch payload rpm wrote outside the temp dir.
N -e --nodeps hwdemo >/dev/null 2>&1
rm -rf "$PAYLOAD_ROOT"
if [ ! -e "$PAYLOAD_ROOT" ]; then pass "scratch payload directory $PAYLOAD_ROOT removed"; fi

echo
echo "PASS=$PASS FAIL=$FAIL SKIP=$SKIP CONTENT=$CONTENT"
[ "$FAIL" -eq 0 ]