#!/bin/bash #----------------------------------------------------------------------------- # # pkgchangelog # # Pkgchangelog generates changelogs of packages pending for being upgraded. # It provides a single changelog for all packages sharing the same source # package and therefore have the same changelog. # # A package changelog is generated between the installed version and the # version pending for being upgraded. If there are multiple versions # of the same package installed on the system, then the newest package is # used as the source version. However it's also possible to specify a # specific version or architecture as the source package. # #----------------------------------------------------------------------------- # # Author: (c) 2019,2024 - Invoca Systems # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 2 of the license or, at your # option, any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, see . # #----------------------------------------------------------------------------- # Do not set `$VERSION' if it is already set in library mode if ! (( ${VERSION+1} )); then declare -r VERSION="2.2.29" fi # Check BASH version declare -i MINMAJOR=3 if (( BASH_VERSINFO[0] < MINMAJOR )); then echo "Error: unable to run with bash version < ${MINMAJOR}, exiting" >&2 exit 1 fi unset -v MINMAJOR # Check that needed binaries exist pkg_check_bin() { while (( $# )); do if ! command -v "$1" > /dev/null 2>&1; then echo "Error: ${FUNCNAME[0]}() binary \`${1}' not found or not executable" >&2 exit 1 fi shift done } # Produce changelog from the source version to the upgradable version # for the selected package. # # In case of an error, RPM still sends messages to STDOUT and only sets an # error as exit level. Therefore we read RPM data only in case there was no # error. Some DNF/YUM tools like `repoquery' have a `--quiet' switch which # works as expected, others like `yumdownloader' behave like RPM. rpm_changelog() { local RPMPKG="$1" local NAME= local VEROLD= local ARCH= local SRCNAME= local VERNEW= local MATCH local URL= local CHLOG= local QUERY local RES local -i HEADER local -i NOCHLOG if ! [[ "$RPMPKG" ]]; then echo "Error: ${FUNCNAME[0]}() no package name given" >&2 return 1 fi QUERY="%{name} %{version}-%{release} %{arch} %{sourcerpm}\n" IFS=" " read -rs NAME VEROLD ARCH SRCNAME < <(RES="$(rpm -q --qf "$QUERY" "$RPMPKG")" && echo "$RES" | tail -n 1) SRCNAME="${SRCNAME%.src.rpm}" SRCNAME="${SRCNAME%-*}" SRCNAME="${SRCNAME%-*}" if ! [[ "$NAME" ]]; then echo "Error: ${FUNCNAME[0]}() \$NAME no \`${QUERY}' found for \`${RPMPKG}'" >&2 return 1 elif ! [[ "$VEROLD" ]]; then echo "Error: ${FUNCNAME[0]}() \$VEROLD no \`${QUERY}' found for \`${RPMPKG}'" >&2 return 1 elif ! [[ "$ARCH" ]]; then echo "Error: ${FUNCNAME[0]}() \$ARCH no \`${QUERY}' found for \`${RPMPKG}'" >&2 return 1 elif ! [[ "$SRCNAME" ]]; then echo "Error: ${FUNCNAME[0]}() \$SRCNAME no \`${QUERY}' found for \`${RPMPKG}'" >&2 return 1 fi # Try using the cache file to save a lot of time if [[ -s "$PKGLIST" ]]; then IFS=" " read -rs _ VERNEW _ < <(grep -se "^${NAME}\.${ARCH}" "$PKGLIST") else VERNEW= fi if ! [[ "$VERNEW" ]]; then if [[ "$PKG_PROG" == "yum" ]]; then QUERY="updates" else QUERY="--upgrades" fi IFS=" " read -rs _ VERNEW _ < <(RES="$("$PKG_PROG" --quiet list "$QUERY" "${NAME}.${ARCH}")" && echo "$RES" | tail -n 1) fi # Strip EPOCH from version VERNEW="${VERNEW#*:}" if ! [[ "$VERNEW" ]]; then echo "Error: ${FUNCNAME[0]}() \$VERNEW no \`${QUERY}' found for \`${NAME}.${ARCH}'" >&2 return 1 fi # We do search for matching changelog entries beginning with `*' MATCH="$(rpm -q --changelog "${NAME}-${VEROLD}.${ARCH}" | grep -e "^\*")" if ! [[ "$MATCH" ]]; then echo "Error: ${FUNCNAME[0]}() \$MATCH no changelog found for \`${NAME}-${VEROLD}.${ARCH}'" >&2 return 1 fi IFS= read -rs URL < <(RES="$(yumdownloader --quiet --urls "${NAME}-${VERNEW}.${ARCH}")" && echo "$RES" | tail -n 1) if ! [[ "$URL" ]]; then echo "Error: ${FUNCNAME[0]}() \$URL no url found for \`${NAME}-${VERNEW}.${ARCH}'" >&2 return 1 fi if [[ "$VEROLD" == "$VERNEW" ]]; then echo "Error: ${FUNCNAME[0]}() no upgradable version found for \`${NAME}-${VEROLD}.${ARCH}'" >&2 return 1 fi HEADER=1 NOCHLOG=1 while IFS= read -rs CHLOG; do if (( HEADER )); then HEADER=0 echo "Changelog between source versions:" echo " --> ${SRCNAME}-${VERNEW}" echo " <-- ${SRCNAME}-${VEROLD}" echo fi if [[ "$CHLOG" =~ ^Changelog ]]; then continue elif [[ "$CHLOG" =~ ^\\* ]]; then if [[ "$MATCH" != "${MATCH/"$CHLOG"}" ]]; then break else NOCHLOG=0 fi fi echo "$CHLOG" done < <(rpm -qp --changelog "$URL" 2> /dev/null || echo "Error: ${FUNCNAME[0]}() reading changelog for \`${NAME}-${VERNEW}.${ARCH}' failed" >&2) # Changelogs were processed but there were no changes detected if (( NOCHLOG )); then echo "(none)" echo fi return $HEADER } # Read list of packages from parameters and from STDIN and process them rpm_batchmode() { local ITEM= local RPMPKG= local RPMCHLOG local QUERY local SRCNAME= local LASTSRC local SHARED= local PKG local RES { for ITEM in $@; do if [[ "$ITEM" != "-s" ]] && [[ "$ITEM" != "--stdin" ]]; then echo "$ITEM" else while read -rs ITEM SHARED; do echo "$ITEM" done fi done } | \ while read -rs RPMPKG SHARED; do if ! [[ "$RPMPKG" ]]; then continue elif [[ "$RPMPKG" != "${RPMPKG%.src}" ]]; then echo "Error: ${FUNCNAME[0]}() query for source package \`${RPMPKG}' not supported" >&2 continue fi QUERY="%{sourcerpm}\n" IFS=" " read -rs SRCNAME < <(RES="$(rpm -q --qf "$QUERY" "$RPMPKG")" && echo "$RES" | tail -n 1) SRCNAME="${SRCNAME%.src.rpm}" SRCNAME="${SRCNAME%-*}" SRCNAME="${SRCNAME%-*}" if ! [[ "$SRCNAME" ]]; then echo "Error: ${FUNCNAME[0]}() \$SRCNAME no \`${QUERY}' found for \`${RPMPKG}'" >&2 continue fi echo "$SRCNAME $RPMPKG" done | \ sort -bdu | \ { LASTSRC= while read -rs SRCNAME RPMPKG; do if [[ "$SRCNAME" != "$LASTSRC" ]]; then if [[ "$LASTSRC" ]]; then echo fi echo -n "$SRCNAME" LASTSRC="$SRCNAME" fi echo -n " $RPMPKG" done echo } | \ while read -rs SRCNAME RPMPKG SHARED; do if ! [[ "$RPMPKG" ]]; then continue fi echo "$LINESEP" echo echo "Packages sharing source \`${SRCNAME}':" # Out of the packages sharing the same `$SRCNAME', `$RPMPKG' is the first # package in alphabetical order. In certain situations, we should not use # this package to generate the changelog as can be seen below. RPMCHLOG="$RPMPKG" for PKG in $RPMPKG $SHARED; do echo " $PKG" echo " $(rpm -q --qf "%{summary}" "$PKG")" # If the `$SRCNAME' package is installed, use it to produce the changelog. # On EL9, there is `bpftool-7.2.0-362.18.1.el9_3' built from # `kernel-5.14.0-362.18.1.el9_3'. Wthout this fix, the changelog would # show version `7.2.0-362.18.1.el9_3' instead of `5.14.0-362.18.1.el9_3'! if [[ "$PKG" == "$SRCNAME" ]]; then RPMCHLOG="$SRCNAME" fi done echo rpm_changelog "$RPMCHLOG" done } pkg_help() { cat << EOF Usage: $BASENAME [OPTION] []... Options: -s, --stdin reads a list of packages from STDIN -V, --version show version information and exit -h, --help display this help and exit Examples: pkgchangelog kernel-3.10.0-1127.el7.x86_64 yum --quiet check-update | pkgchangelog --stdin pkgchangelog --stdin postfix spamassassin cyrus-imapd \\ bind < <(dnf --quiet check-upgrade | grep -Ee "kernel|glibc") EOF } pkg_version() { echo "$BASENAME version $VERSION" } # Main program starts here # Option `-l' means we're in library mode and don't execute anything if [[ "$1" == "-l" ]]; then if ! [[ "$LINESEP" ]]; then declare LINESEP="$(printf "=%.0s" {1..80})" fi return 0 fi # Treat unset variables and parameters other than the special # parameters `@' and `*' as an error when performing parameter expansion. set -o nounset # Detect line width of STDOUT, defaults to 80 if not running in a terminal declare -i WIDTH=80 if [[ -t 1 ]]; then declare STRING= exec 3> /dev/stdout read -rs _ STRING < <(stty -F /dev/fd/3 size 2> /dev/null) exec 3>&- if [[ "$STRING" ]]; then WIDTH=$STRING fi unset -v STRING fi # Set sane defaults declare -x LC_ALL="C" declare BASENAME="${0##*/}" declare LINESEP="$(eval printf "=%.0s" "{1..${WIDTH}}")" declare OPT for OPT in "$@"; do case "$OPT" in "-V"|"--version") pkg_version exit 0 ;; "-h"|"--help") pkg_help exit 0 ;; esac done if ! (( $# )); then pkg_version pkg_help >&2 exit 1 else # Variables already set in library mode declare -r PKGLIST="/var/cache/pkgmonitor/pkglist" declare -r PKG_PROG="$(command -v "dnf" > /dev/null 2>&1 && echo "dnf" || echo "yum")" pkg_check_bin repoquery yumdownloader "$PKG_PROG" rpm_batchmode "$@" fi