#!/bin/sh -e # checkapk - find ABI breakages in package upgrades # Copyright (c) 2012 Natanael Copa # # Distributed under GPL-2 # program_version=@VERSION@ datadir=@datadir@ if ! [ -f "$datadir/functions.sh" ]; then echo "$datadir/functions.sh: not found" >&2 exit 1 fi . "$datadir/functions.sh" case "$(tar --version)" in *GNU*) tar_flavor=GNU;; *libarchive*) tar_flavor=libarchive;; *busybox*) tar_flavor=busybox;; *) die 'unknown tar flavor';; esac : ${APK:="/sbin/apk"} if command -v abipkgdiff >/dev/null 2>&1; then abigail=1 fi subpackage_types_has() { local i for i in $subpackages; do local _name="${i%%:*}" [ "$1" = "${_name##*-}" ] && return 0 done return 1 } list_has() { local needle="$1" local i shift for i in $@; do [ "$needle" = "$i" ] && return 0 [ "$needle" = "!$i" ] && return 1 done return 1 } usage() { cat >&2 <<-__EOF__ $program $program_version - find ABI breakages in package upgrades Usage: $program Run in the directory of a built package. __EOF__ } fetch_old() { if ! $APK fetch --stdout --repositories-file "$tmpdir/repositories" "$1" > old.apk; then warning "could not download $1 (network error or new package)" return 1 else [ -e old.apk ] || die "can't find old apk old.apk" return 0 fi } fetch_new() { local i for i in "$PKGDEST/$1" "$REPODEST/$repo/$CARCH/$1" "$startdir/$1"; do i="$i-$pkgver-r$pkgrel.apk" if [ -f "$i" ]; then echo "$i" return 0 fi done [ -f "$i" ] || die "can't find new apk $1" } pkginfo() { local apk="$1" local filename="$2" tar -zxf "$apk" .PKGINFO mv .PKGINFO .PKGINFO.orig grep -E \ '^(depend|provides|provider|replaces|triggers|install_if)' \ .PKGINFO.orig | sort > "$filename" touch -r .PKGINFO.orig "$filename" rm .PKGINFO.orig } filelist() { local apk="$1" local filename="$2" local tar_fmt case "$tar_flavor" in GNU) # -rw-r--r-- root/root 737 2019-06-14 20:45 .PKGINFO # lrwxrwxrwx root/root 0 2019-09-13 06:01 usr/lib/libixion-0.15.so.0 -> libixion-0.15.so.0.0.0 # hrwxr-xr-x root/root 0 2019-04-22 10:26 usr/bin/python3.6 link to usr/bin/python3.6m tar_fmt='{ printf "%s %s ", $1, $2 } /^l/ { printf "%s -> ", $(NF-2) } /^h/ { printf "%s -> ", $(NF-3) } { print $NF }';; busybox) # -rw-r--r-- root/root 737 2019-06-14 20:45:29 .PKGINFO # lrwxrwxrwx root/root 0 2019-09-13 06:01:49 usr/lib/libixion-0.15.so.0 -> libixion-0.15.so.0.0.0 # -rwxr-xr-x root/root 0 2019-04-22 10:26:45 usr/bin/python3.6 -> usr/bin/python3.6m tar_fmt='{ printf "%s %s ", $1, $2 } / -> / { printf "%s -> ", $(NF-2) } { print $NF }';; libarchive) # -rw-r--r-- 0 root root 737 Jun 14 15:45 .PKGINFO # lrwxrwxrwx 0 root root 0 Sep 13 01:01 usr/lib/libixion-0.15.so.0 -> libixion-0.15.so.0.0.0 # hrwxr-xr-x 0 root root 0 Oct 1 22:40 usr/bin/python3.6m link to usr/bin/python3.6 tar_fmt='{ printf "%s %s/%s ", $1, $3, $4 } /^l/ { printf "%s -> ", $(NF-2) } /^h/ { printf "%s -> ", $(NF-3) } { print $NF }';; esac tar -ztvf "$apk" | grep -ve ' \.SIGN\.' \ | awk "$tar_fmt" \ | sort -k 3,3 > "$filename" } abigail_pre() { [ -n "$abigail" ] || return 0 # This assumes one of each, which should generally be the case. for i in $subpackages; do case "$i" in *-dbg) dbg2="$i";; *-dev) dev2="$i";; esac done if [ -n "$dbg2" ]; then if fetch_old "$dbg2"; then dbg1="$dbg2.old.apk" mv old.apk "$dbg1" fi dbg2="$(fetch_new "$dbg2")" fi if [ -n "$dev2" ]; then if fetch_old "$dev2"; then dev1="$dev2.old.apk" mv old.apk "$dev1" fi dev2="$(fetch_new "$dev2")" fi } abigail() { [ -n "$abigail" ] || return 0 [ -n "$oldapk" ] || return 0 case "$newapk" in "$dbg2"|"$dev2") return 0;; esac set +e abipkgdiff \ ${dbg1:+--debug-info-pkg1 "$dbg1"} \ ${dbg2:+--debug-info-pkg2 "$dbg2"} \ ${dev1:+--devel-pkg1 "$dev1"} \ ${dev2:+--devel-pkg2 "$dev2"} \ --leaf-changes-only \ --impacted-interfaces \ --show-bytes \ "$oldapk" "$newapk" rc=$? set -e if [ "$((rc & 4))" -eq 4 ]; then warning "Abigail detected a possibly incompatible change." elif [ "$((rc & 8))" -eq 8 ]; then error "Abigail detected an incompatible change." fi } check_soname() { local soname sover_old basename soname_new sover_new local rdeps real_rdeps i self j soname="$1" sover_old="${soname#*=}" soname="${soname%=*}" basename="${soname%%.so*}.so" soname_new="$(grep -F "provides = $basename" \ "pkginfo-$_pkgname-new" | cut -d ' ' -f 3)" sover_new="${soname_new#*=}" soname_new="${soname_new%=*}" if [ -z "$soname_new" ]; then warning "SONAME moved or deleted: $soname" elif [ "$soname" != "$soname_new" ]; then warning "SONAME changed: $soname=$sover_old" warning2 "-> $soname_new=$sover_new" elif [ "$sover_old" != "$sover_new" ]; then warning "SOVER changed: $soname=$sover_old" warning2 "-> $soname_new=$sover_new" fi rdeps="$($APK search --repositories-file "$tmpdir/repositories" \ --rdepends --quiet --exact "$soname" | sort -u)" real_rdeps= for i in $rdeps; do self=0 for j in $pkgname $subpackages; do [ "$i" = "$j" ] || continue self=1 break done [ "$self" -eq 0 ] && real_rdeps="$real_rdeps $i" done if [ -n "$real_rdeps" ]; then warning "dependents on $soname:" # (warning2) >>> WARNING: printf '%s\n' "$real_rdeps" | sed '/^$/d; s/^/ /' >&2 else msg "No dependents on $soname." fi } if [ $# -gt 0 ]; then usage exit 2 fi if ! [ -f "$ABUILD_CONF" ] && ! [ -f "$ABUILD_USERCONF" ]; then die "no abuild.conf found" fi if ! [ -f APKBUILD ]; then die 'must be run in the directory of a built package' fi if ! [ -n "$CARCH" ]; then die "failed to detect CARCH" fi . ./APKBUILD if [ -n "$DEFAULT_DBG" ] \ && ! subpackage_types_has "dbg" \ && ! list_has "!dbg" $options \ && [ "$arch" != "noarch" ]; then subpackages="$pkgname-dbg $subpackages" fi startdir="$PWD" repodir="${startdir%/*}" repo="${repodir##*/}" tmpdir=$(mktemp -d -t checkpkg-script.XXXXXX) trap "rm -rf '$tmpdir'" INT EXIT cd "$tmpdir" || die "failed to create temp dir" # generate a temp repositories file with only the http(s) repos grep -E "^https?:" /etc/apk/repositories > "$tmpdir/repositories" abigail_pre for i in $pkgname $subpackages; do _pkgname=${i%%:*} newapk="$(fetch_new "$_pkgname")" case "$newapk" in "$dbg2") oldapk="$dbg1" ;; "$dev2") oldapk="$dev1" ;; *) oldapk=old.apk if ! fetch_old "$_pkgname"; then oldapk= fi ;; esac if [ -n "$oldapk" ]; then pkginfo "$oldapk" "pkginfo-$_pkgname-old" filelist "$oldapk" "filelist-$_pkgname-old" touch -r "pkginfo-$_pkgname-old" "filelist-$_pkgname-old" else touch "filelist-$_pkgname-old" touch "pkginfo-$_pkgname-old" fi pkginfo "$newapk" "pkginfo-$_pkgname-new" filelist "$newapk" "filelist-$_pkgname-new" touch -r "pkginfo-$_pkgname-new" "filelist-$_pkgname-new" diff -u "filelist-$_pkgname-old" "filelist-$_pkgname-new" || true diff -u "pkginfo-$_pkgname-old" "pkginfo-$_pkgname-new" | tee pkginfo-diff soname= for soname in $(awk '/^-provides = so:/ { print $3 }' pkginfo-diff); do check_soname "$soname" done [ -n "$soname" ] || msg "No soname differences for $_pkgname." if grep -q '^provides = so:' "pkginfo-$_pkgname-new"; then abigail fi rm -f old.apk done