#!/bin/bash # parabola-dependents - list all packages which are dependents of a specified package # # Copyright (C) 2022-2023 bill-auger # # SPDX-License-Identifier: AGPL-3.0-or-later # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . readonly ABSLIBRE_DIR=/packages/abslibre readonly ABS_PACKAGES_DIR=/packages/arch/packages readonly ABS_COMMUNITY_DIR=/packages/arch/community readonly DEBUG=0 readonly BE_VERBOSE=$( [[ "${1}" == '-v' ]] && echo 1 || echo 0 ) ; (( $BE_VERBOSE )) && shift ; readonly PKGBUILD_SED_RX='s|/PKGBUILD$|| ; s|([^/]*/)?([^/]+)/([^/]+)$|\2 \3|' readonly DEP=$( sed -E "${PKGBUILD_SED_RX}" <<<${1} | cut -d ' ' -f 2 ) # readonly REPOS=( nonprism nonsystemd{-testing,} libre{-testing,} readonly REPOS=( nonprism{-testing,} nonsystemd{-testing,} libre{-testing,} kernels{-testing,} testing core extra community{-testing,} pcr{-testing,} nonprism-multilib{-testing,} nonsystemd-multilib libre-multilib{-testing,} multilib{-testing,} pcr-multilib{-testing,} ) readonly TEMP_DB_NAME=parabola-dependents readonly CBLUE='\033[0;36m' readonly CRED='\033[0;31m' readonly CYELLOW='\033[0;33m' readonly CEND='\033[0m' readonly JOIN_CHAR='=' readonly DUMMY_PKGBUILD_FMT='pkgname=%s pkgver=0.0.0 pkgrel=42 pkgdesc="dummy PKGBUILD for experimentation" arch=(armv7h i686 x86_64) url=https://nowhere.man license=(GPL) provides=(%s) source=(PKGBUILD) sha256sums=(SKIP) package() { echo "created '"'\${pkgname}'"' package" ; }' readonly USAGE="USAGE: parabola-dependents [-v] parabola-dependents [-v] parabola-dependents [-v] List all parabola packages which are dependents of a specified package. This includes direct dependents, transitive dependents, and makedepends dependents. NOTE: The verbose report does not account for makedepends of packages not in abslibre. The positional argument: or may be a 'pkgbase' name, or a path to an abslibre 'pkgbase' directory, or a path to a PKGBUILD. Paths may be absolute or relative. They will all resolve to a 'pkgbase' name. The positional argument: may be of the forms: 'libfoo.so' or 'libfoo.so=1-64'. If 'pkgbase' does not exist in any database, a dummy package will be created, in memory, upon which for pactree to reflect. By default, only first-order dependents are listed, with counts of higher-order dependents. Note: This script makes no assumption regarding the state of the ABS trees. You will need to manage them manually (eg: \`git pull && git checkout master\`). It will however, ignore any PKGBUILDs which are not checked-in to the master branch. Note: This script can take several minutes to complete, if the dependency-graph is large. Options: -v Itemize all higher-order dependents, displaying the dependency-chain." readonly ENVIRONMENT_ERR_FMT="could not find \`'%s\` in the environment" readonly INVALID_CFG_ERR_MSG="space chars in ${TEMP_DB_NAME}\n" readonly INVALID_ARG_ERR_MSG="no dependency package specified${CEND}\n\n${USAGE}" readonly UNPRIVILEGED_ERR_MSG="this script requires super-user privileges" DB_DIR='' # Init() CFG_FILE='' # Init() PACMAN_OPTS='' # Init() PACTREE_OPTS='' # Init() PKGBASE='' # Init() ParabolaDependents=() # CollectResults() ArchDependents=() # CollectResults() ParabolaMakedependents=() # CollectResults() ArchMakedependents=() # CollectResults() declare -A HiorderDependents # CollectResults() ## helpers ## DBG() # (log_fmt [fmt_args]*) { local log_fmt=$1 local fmt_args=( "${@:2}" ) (( DEBUG )) && printf "${CYELLOW}${log_fmt}${CEND}\n" "${fmt_args[@]}" >&2 || : } Log() # (log_fmt [fmt_args]*) { local log_fmt=$1 local fmt_args=( "${@:2}" ) printf "${CBLUE}${log_fmt}${CEND}\n" "${fmt_args[@]}" >&2 } LogError() # (log_fmt [fmt_args]*) { local log_fmt=$1 local fmt_args=( "${@:2}" ) printf "${CRED}ERROR: ${log_fmt}${CEND}\n" "${fmt_args[@]}" >&2 } Exit() # (log_fmt [fmt_args]*) { LogError "$@" ; exit 1 ; } LogScriptError() # (source_file func_name line_n) { local source_file="$1" local func_name=$2 local line_n=$3 local err_loc="$(awk "(( NR == ${line_n} )) { print }" ${source_file})" LogError "in ${func_name}\n${line_n}: ${err_loc=}\n" } DummyPkg() { echo ${PKGBASE}-0.0.0-42-$(uname -m).pkg.tar.xz ; } ParsePkgbuilds() # ("abs_dir" "dep_chains") ( local abs_dir="$1" local dep_chains=( "${@:2}" ) local pkgbuild # DBG "ParsePkgbuilds() (${#dep_chains[@]}) dep_chains=${dep_chains[@]}" # ; return ; # for ea in "${dep_chains[@]}" ; do DBG "=$ea" ; done ; return ; set +o errexit +o errtrace ; trap '' EXIT INT TERM ERR ; # reset debug traps cd "${abs_dir}" for pkgbuild in $(find . -name PKGBUILD) do git ls-tree master ${pkgbuild} | grep -E '/PKGBUILD$' > /dev/null || continue # DBG "ParsePkgbuilds() pkgbuild=${pkgbuild}" ( local repo pkgbase ; source ${pkgbuild} ; # continue if grep ^${DEP}$ <<<${makedepends[*]} > /dev/null || false then # parse direct makedepends repo=$( sed -E "${PKGBUILD_SED_RX}" <<<${pkgbuild} | cut -d ' ' -f 1) pkgbase=$(sed -E "${PKGBUILD_SED_RX}" <<<${pkgbuild} | cut -d ' ' -f 2) echo ${repo}/${pkgbase} else # collect transitive makedepends for makedepend in ${makedepends[*]} do : # TODO: if grep " ${makedepend} " <<<${dep_chains[@]} > /dev/null then DBG "ParsePkgbuilds() pkgbuild=${pkgbuild} makedepend=${makedepend} in-chain" >&2 # else echo "ParsePkgbuilds() pkgbuild=${pkgbuild} makedepend=${makedepend} not in-chain" >&2 fi done fi ) done ) IsArchRepo() # (repo) { local repo=$1 ; [[ "${repo}" =~ ^(community|core|extra|multilib|testing)$ ]] } IsFirstOrderDep() # ("dep_chain") { local dep_chain="$@" (( $(tr '<' '\n' <<<${dep_chain} | wc -l) == 2 )) } PrintDependent() # ("dep_chain") { local dep_chain="$@" local dep_pkg via_pkg declare -i n_hiorder_deps if (( BE_VERBOSE )) then # display all transitive dependents echo " ${dep_chain/ /${JOIN_CHAR}}" elif IsFirstOrderDep "${dep_chain}" then # display only first-order dependents dep_pkg=$(sed 's|.*<- ||' <<<${dep_chain}) via_pkg=$(sed 's|.*\] <- \([^ ]*\).*|\1|' <<<${dep_chain}) n_hiorder_deps=$( awk '{print NF}' <<<${HiorderDependents[${via_pkg}]} ) echo -n " ${dep_pkg}${JOIN_CHAR}" (( n_hiorder_deps )) && echo "(plus ${n_hiorder_deps} higher-order deps)" || echo # DBG "PrintDependent() via_pkg=$via_pkg HiorderDependents='${HiorderDependents[${via_pkg}]}'" fi } ## business ## Init() { # find or create the temporary workspace readonly DB_DIR="$( db_dir=$(ls -1 -d /tmp/${TEMP_DB_NAME}.??? 2> /dev/null | head -n 1) [[ -d "${db_dir}" ]] && echo "${db_dir}" || su $(logname) -c "mktemp -d -p /tmp -t ${TEMP_DB_NAME}.XXX" )" readonly CFG_FILE=${DB_DIR}/pacman-all.conf readonly LOG_FILE=${DB_DIR}/parabola-dependents.log readonly PACMAN_OPTS="--dbpath=${DB_DIR} --config=${CFG_FILE}" readonly PACTREE_OPTS="${PACMAN_OPTS} --sync --reverse --chain" # populate or update the temporary package database if ! (( DEBUG )) || ! (( $(ls -1 /tmp/${TEMP_DB_NAME}.??? | wc -l) )) then Log "updating database ...." printf "[options]\nArchitecture = auto\n" > ${CFG_FILE} for repo in ${REPOS[@]} do printf "[${repo}]\nInclude = /etc/pacman.d/mirrorlist\n" >> ${CFG_FILE} done pacman ${PACMAN_OPTS} -Sy &> ${LOG_FILE} || Exit "$(cat ${LOG_FILE})" else Log "skipping database update in DEBUG mode - delete ${DB_DIR} to sync" fi # create dummy for missing dependency package or sodep local log_msg dep dummy_pkg if ! pacman ${PACMAN_OPTS} -Ss ^${DEP}$ &> ${LOG_FILE} then if [[ "${DEP}" =~ (.*)\.so=? ]] then log_msg="creating dummy package for sodep: '${DEP}'" dep=${DEP} ; PKGBASE=$(sed 's|^\([0-9a-z@._+-]*\)|\1|' <<<${BASH_REMATCH[1]}) ; else log_msg="package: '${DEP}' not found - creating dummy package" dep='' ; PKGBASE=${DEP} ; fi readonly PKGBASE ; dummy_pkg=$(DummyPkg) ; cd ${DB_DIR} Log "${log_msg}" ; rm -f ./${dummy_pkg} ; printf "${DUMMY_PKGBUILD_FMT}\n" "${PKGBASE}" "${dep}" > ./PKGBUILD su $(logname) -c 'makepkg --force &> /dev/null' if [[ -f ./${dummy_pkg} ]] then repo-add ${TEMP_DB_NAME}.db.tar ./${dummy_pkg} &> ${LOG_FILE} printf "[${TEMP_DB_NAME}]\nServer = file://${DB_DIR}\n" >> ${CFG_FILE} pacman ${PACMAN_OPTS} -Sy &> ${LOG_FILE} Log "dummy package '${PKGBASE}' created" else Exit "makepkg failed" fi fi } CollectResults() { local dep_chains n_results dep_chain dep_pkg via_pkg repos repo # query database for dependents Log "querying database ...." # mapfile -t dep_chains < <(pactree ${PACTREE_OPTS} ${DEP} | sort) mapfile -t dep_chains < <(pactree ${PACTREE_OPTS} ${DEP}) DBG "CollectResults() n_results=${#dep_chains[@]} + ${#ParabolaMakedependents[*]} + ${#ArchMakedependents[*]}" DBG "CollectResults() dep_chains=%s" "${dep_chains[@]}" DBG "CollectResults() ParabolaMakedependents=${ParabolaMakedependents[*]}" DBG "CollectResults() ArchMakedependents=${ArchMakedependents[*]}" # compile results n_results=$(( ${#dep_chains[@]} + ${#ParabolaMakedependents[*]} + \ ${#ArchMakedependents[*]} )) # FIXME: Log "compiling results for (${n_results}) dependents ...." for dep_chain in "${dep_chains[@]}" do # example dep_chains: # first-order dep: '[$DEP] <- $dep_pkg' # high-order dep: '[$DEP] <- $via_pkg <- transitive_dep <- $dep_pkg' dep_pkg=$(sed 's|.*<- ||' <<<${dep_chain}) via_pkg=$(sed 's|.*\] <- \([^ ]*\).*|\1|' <<<${dep_chain}) repos="$(pacman ${PACMAN_OPTS} -Si ${dep_pkg} | grep Repository | \ cut -d ':' -f 2 | tr -d ' ' )" for repo in ${repos} do if IsArchRepo ${repo} then ArchDependents+=( "${dep_chain}" ) else ParabolaDependents+=( "${dep_chain}" ) fi if ! IsFirstOrderDep "${dep_chain}" then hiorder_deps="${HiorderDependents[${via_pkg}]} ${repo}/${dep_pkg}" HiorderDependents[${via_pkg}]="$(echo ${hiorder_deps})" fi done done # parse PKGBUILDs for makedepends Log "searching abslibre ...." ParabolaMakedependents+=( $(ParsePkgbuilds "${ABSLIBRE_DIR}" "${dep_chains[@]}") ) return Log "searching abs ...." ArchMakedependents+=( $(ParsePkgbuilds "${ABS_PACKAGES_DIR}" "${dep_chains[@]}") ) ArchMakedependents+=( $(ParsePkgbuilds "${ABS_COMMUNITY_DIR}" "${dep_chains[@]}") ) } PrintReport() { local dep_chain local has_firstorder_deps=0 # report parabola abslibre packages with some degree of run-time dependency # on the input package or sodep if (( ${#ParabolaDependents[*]} )) then for dep_chain in "${ParabolaDependents[@]}" do IsFirstOrderDep "${dep_chain}" && has_firstorder_deps=1 || : done if (( BE_VERBOSE || has_firstorder_deps )) then Log "\ndirect$( ! (( BE_VERBOSE )) || echo " and transitive") parabola dependents:" fi # DBG "PrintReport() ${ParabolaDependents[*]}" for dep_chain in "${ParabolaDependents[@]}" do PrintDependent "${dep_chain}" done | column --table --separator="${JOIN_CHAR}" else echo -e "\n(0) parabola dependents" fi # report arch abs packages with some degree of run-time dependency # on the input package or sodep if (( BE_VERBOSE && ${#ArchDependents[@]} )) then Log "\ndirect$( ! (( BE_VERBOSE )) || echo " and transitive") arch dependents:" for dep_chain in "${ArchDependents[@]}" do PrintDependent "${dep_chain}" done | column --table --separator="${JOIN_CHAR}" else echo -e "\n(${#ArchDependents[@]}) arch dependents" fi # report parabola abslibre packages with some degree of build-time dependency # on the input package or sodep if (( ${#ParabolaMakedependents[*]} )) then Log "\nparabola build dependents:" printf " %s\n" ${ParabolaMakedependents[*]} fi # report arch abs packages with some degree of build-time dependency # on the input package or sodep if (( ${#ArchMakedependents[*]} )) then Log "\narch build dependents:" printf " %s\n" ${ArchMakedependents[*]} fi } Cleanup() { if ! (( DEBUG )) then [[ "${DB_DIR}" =~ ${TEMP_DB_NAME}\....$ ]] && rm -rf ${DB_DIR} || : elif [[ -d "${DB_DIR}" ]] then cd ${DB_DIR} rm -f ./$(DummyPkg) repo-remove ${TEMP_DB_NAME}.db.tar ${PKGBASE} &> ${LOG_FILE} fi } set -o errexit -o errtrace trap 'Cleanup' EXIT INT TERM trap 'LogScriptError "${BASH_SOURCE[0]}" "${FUNCNAME[0]}" "${LINENO}"' ERR # TODO: `pactree --chain` is an un-packaged custom feature # https://git.parabola.nu/pacman-contrib.git (parabola branch) export PATH="/code/pacman-contrib/src:${PATH}" [[ -x /code/pacman-contrib/src/pactree ]] || Exit "${ENVIRONMENT_ERR_FMT}" pactree which git &> /dev/null || Exit "${ENVIRONMENT_ERR_FMT}" git which pactree &> /dev/null || Exit "${ENVIRONMENT_ERR_FMT}" pactree [[ ! "${TEMP_DB_NAME}" =~ ' ' ]] || Exit "${INVALID_CFG_ERR_MSG}" (( ! EUID )) || Exit "${UNPRIVILEGED_ERR_MSG}" (( $# )) || Exit "${INVALID_ARG_ERR_MSG}" export LC_ALL=C Init CollectResults PrintReport