#!/bin/bash # ~ 2017-11-12 WANTS_FILE=/usr/local/etc/rpm-wants set -e set -o pipefail # XXX: $BASH_COMMAND may be wrong with pipefail, but it's better than what we had before. trap 'echo "Unexpected error in command: $BASH_COMMAND"; if [ -n "$tmpdir" ] && [ -r "$tmpdir/dnf.log" ]; then cat "$tmpdir/dnf.log"; fi ' ERR if [ $EUID != 0 ]; then echo >&2 'Unfortunately, rpm-audit can only run as root because it calls' echo >&2 '"dnf install --assumeno".' exit 1 fi tmpdir="$(mktemp -d --tmpdir rpm-audit.XXXXXX)" trap 'rm -rf "$tmpdir"' EXIT uncommented_entries=($(<"$WANTS_FILE" sed -re 's/(^|[^[:space:]])[[:space:]]*#.*$/\1/' | egrep -v '^$')) wants=() modules=() for e in "${uncommented_entries[@]}"; do case "$e" in (module:*) modules+=("${e#module:}");; (*) wants+=("$e");; esac done function rpmq { # Set query format to match "dnf repoquery". # # Looks like rpm exits with the number of unmatched arguments, so we # have to swallow any exit code. :( ~ Matt 2018-06-01 rpm -q --qf '%{NAME}-%|EPOCH?{%{EPOCH}}:{0}|:%{VERSION}-%{RELEASE}.%{ARCH}\n' "$@" || true } function strip_zero_epoch { sed -e 's/-0:/-/' } function indent { sed -e 's/^/ /' } function filter_packages { # Exempt kernel-{core,devel,modules{,-extra}} from audit because of multiple # installed versions: not worth trying to do something better. grep -Ev '^(gpg-pubkey|kernel-core|kernel-devel|kernel-modules|kernel-modules-extra)-[^-]+-[^-]+$' } # Check 1: installed want providers vs. userinstalled # "dnf repoquery --installed --whatprovides" doesn't seem to accept multiple # arguments, and running it once per entry would be unacceptably slow. See how # long we can get away with this before looking for another solution. # # "rpm -q --whatprovides glibc.i686" does not work. Take all wants that look # like they have an architecture and use "rpm -q" instead. This may fail if # someone writes an actual provide of a package-like name with an architecture. # ~ Matt 2017-11-16 pkg_arch_wants=() normal_wants=() for w in "${wants[@]}"; do case "$w" in (*.i686|*.x86_64) pkg_arch_wants+=("$w");; (*) normal_wants+=("$w");; esac done # Since F29, kernel packages keep getting unmarked as userinstalled. Not # investigating; just exclude them here. ~ Matt 2019-01-22 { rpmq --whatprovides "${normal_wants[@]}" && rpmq "${pkg_arch_wants[@]}"; } | strip_zero_epoch | filter_packages | sort | uniq >"$tmpdir/wants-installed" dnf repoquery --userinstalled | strip_zero_epoch | filter_packages | sort >"$tmpdir/userinstalled" if ! cmp -s "$tmpdir/wants-installed" "$tmpdir/userinstalled"; then echo "Installed wants that are not marked as userinstalled:" comm -2 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent echo "Userinstalled packages that are not wants:" comm -1 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent echo "To correct, edit the wants file or use 'dnf mark {install|remove} PACKAGE_NAME'." exit 1 fi # Check 2: fresh solution of wants vs. installed (should catch different choice of provider, packages needing update, orphans, and problems) platform_id="$(sed -nre 's,^PLATFORM_ID="(.*)"$,\1,p' /etc/os-release)" SANDBOX_DNF=(dnf --installroot="$tmpdir/installroot" --setopt=cachedir=/var/cache/dnf # Share main cache to save time --disableplugin=qubes-hooks # Qubes plugin takes unwanted actions when using an installroot --releasever=/ --setopt=module_platform_id=$platform_id ) "${SANDBOX_DNF[@]}" --assumeyes module enable "${modules[@]}" &>"$tmpdir/dnf.log" # --verbose for "Package ... will be installed" output. The human-readable list # of packages to install is harder to scrape because of line wrapping. # # "dnf --assumeno" exits 1. I don't see an obvious way to distinguish this from # real errors. However, real errors are likely to generate a diff anyway. { "${SANDBOX_DNF[@]}" --verbose --assumeno install --best "${wants[@]}" 2>&1 || [ $? == 1 ]; } | tee -a "$tmpdir/dnf.log" | sed -nre 's/^---> Package ([^ ]+)\.([^ .]+) ([^ ]+) will be installed$/\1-\3.\2/p' | filter_packages | sort >"$tmpdir/solved" # Looks like "dnf repoquery --installed" doesn't catch extras. rpmq -a | filter_packages | strip_zero_epoch | sort >"$tmpdir/installed" if ! cmp -s "$tmpdir/solved" "$tmpdir/installed"; then echo "Packages in fresh solution that are not installed:" comm -2 -3 "$tmpdir/solved" "$tmpdir/installed" | indent echo "Installed packages that are not in fresh solution:" comm -1 -3 "$tmpdir/solved" "$tmpdir/installed" | indent echo "To correct, install or remove packages to match the canonical solution," echo "or override the default choice of providers by adding the desired" echo "providers to the wants file." exit 1 fi echo "RPM package set audit passed!"