--- /dev/null
+#!/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!"