Add mock configuration for building against the local dnf repository
[utils/utils.git] / rpm-audit
1 #!/bin/bash
2 # ~ 2017-11-12
3
4 WANTS_FILE=/usr/local/etc/rpm-wants
5
6 set -e
7 set -o pipefail
8 # XXX: $BASH_COMMAND may be wrong with pipefail, but it's better than what we had before.
9 trap 'echo "Unexpected error in command: $BASH_COMMAND"; if [ -n "$tmpdir" ] && [ -r "$tmpdir/dnf.log" ]; then cat "$tmpdir/dnf.log"; fi ' ERR
10
11 if [ $EUID != 0 ]; then
12         echo >&2 'Unfortunately, rpm-audit can only run as root because it calls'
13         echo >&2 '"dnf install --assumeno".'
14         exit 1
15 fi
16
17 tmpdir="$(mktemp -d --tmpdir rpm-audit.XXXXXX)"
18 trap 'rm -rf "$tmpdir"' EXIT 
19
20 uncommented_entries=($(<"$WANTS_FILE" sed -re 's/(^|[^[:space:]])[[:space:]]*#.*$/\1/' | egrep -v '^$'))
21 wants=()
22 modules=()
23 for e in "${uncommented_entries[@]}"; do
24         case "$e" in
25         (module:*) modules+=("${e#module:}");;
26         (*) wants+=("$e");;
27         esac
28 done
29
30 function rpmq {
31         # Set query format to match "dnf repoquery".
32         #
33         # Looks like rpm exits with the number of unmatched arguments, so we
34         # have to swallow any exit code. :( ~ Matt 2018-06-01
35         rpm -q --qf '%{NAME}-%|EPOCH?{%{EPOCH}}:{0}|:%{VERSION}-%{RELEASE}.%{ARCH}\n' "$@" || true
36 }
37 function strip_zero_epoch {
38         sed -e 's/-0:/-/'
39 }
40 function indent {
41         sed -e 's/^/  /'
42 }
43 function filter_packages {
44         # Exempt kernel-{core,devel,modules{,-extra}} from audit because of multiple
45         # installed versions: not worth trying to do something better.
46         grep -Ev '^(gpg-pubkey|kernel-core|kernel-devel|kernel-modules|kernel-modules-extra)-[^-]+-[^-]+$'
47 }
48
49 # Check 1: installed want providers vs. userinstalled
50
51 # "dnf repoquery --installed --whatprovides" doesn't seem to accept multiple
52 # arguments, and running it once per entry would be unacceptably slow.  See how
53 # long we can get away with this before looking for another solution.
54 #
55 # "rpm -q --whatprovides glibc.i686" does not work.  Take all wants that look
56 # like they have an architecture and use "rpm -q" instead.  This may fail if
57 # someone writes an actual provide of a package-like name with an architecture.
58 # ~ Matt 2017-11-16
59 pkg_arch_wants=()
60 normal_wants=()
61 for w in "${wants[@]}"; do
62         case "$w" in
63         (*.i686|*.x86_64)
64                 pkg_arch_wants+=("$w");;
65         (*)
66                 normal_wants+=("$w");;
67         esac
68 done
69 # Since F29, kernel packages keep getting unmarked as userinstalled.  Not
70 # investigating; just exclude them here. ~ Matt 2019-01-22
71 { rpmq --whatprovides "${normal_wants[@]}" && rpmq "${pkg_arch_wants[@]}"; } | strip_zero_epoch | filter_packages | sort | uniq >"$tmpdir/wants-installed"
72 dnf repoquery --userinstalled | strip_zero_epoch | filter_packages | sort >"$tmpdir/userinstalled"
73
74 if ! cmp -s "$tmpdir/wants-installed" "$tmpdir/userinstalled"; then
75         echo "Installed wants that are not marked as userinstalled:"
76         comm -2 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent
77         echo "Userinstalled packages that are not wants:"
78         comm -1 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent
79         echo "To correct, edit the wants file or use 'dnf mark {install|remove} PACKAGE_NAME'."
80         exit 1
81 fi
82
83 # Check 2: fresh solution of wants vs. installed (should catch different choice of provider, packages needing update, orphans, and problems)
84
85 platform_id="$(sed -nre 's,^PLATFORM_ID="(.*)"$,\1,p' /etc/os-release)"
86
87 SANDBOX_DNF=(dnf
88         --installroot="$tmpdir/installroot"
89         --setopt=cachedir=/var/cache/dnf  # Share main cache to save time
90         --disableplugin=qubes-hooks  # Qubes plugin takes unwanted actions when using an installroot
91         --releasever=/ --setopt=module_platform_id=$platform_id
92 )
93
94 "${SANDBOX_DNF[@]}" --assumeyes module enable "${modules[@]}" &>"$tmpdir/dnf.log"
95
96 # --verbose for "Package ... will be installed" output.  The human-readable list
97 # of packages to install is harder to scrape because of line wrapping.
98 #
99 # "dnf --assumeno" exits 1.  I don't see an obvious way to distinguish this from
100 # real errors.  However, real errors are likely to generate a diff anyway.
101 { "${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"
102 # Looks like "dnf repoquery --installed" doesn't catch extras.
103 rpmq -a | filter_packages | strip_zero_epoch | sort >"$tmpdir/installed"
104
105 if ! cmp -s "$tmpdir/solved" "$tmpdir/installed"; then
106         echo "Packages in fresh solution that are not installed:"
107         comm -2 -3 "$tmpdir/solved" "$tmpdir/installed" | indent
108         echo "Installed packages that are not in fresh solution:"
109         comm -1 -3 "$tmpdir/solved" "$tmpdir/installed" | indent
110         echo "To correct, install or remove packages to match the canonical solution,"
111         echo "or override the default choice of providers by adding the desired"
112         echo "providers to the wants file."
113         exit 1
114 fi
115
116 echo "RPM package set audit passed!"