#!/bin/bash ##### # # Scanelf-Packagecheck -- Attempts to find and rebuild broken link-level # dependencies. # # Goals: - Don't use tempfiles. # - As fast as possible. # - Package-manager agnostic. # # TODO: Sanity checks. # # NOTE: If there's ever a package manager that doesn't respect --pretend, bugs # will arise. # ##### # Readonly variables: declare -r APP_NAME="${0##*/}" # The name of this application declare -r OIFS="$IFS" # Save the IFS # Other global variables: declare -a pm_opts # Options passed to the package manager declare -a orphans_to_skip # libs to ignore . /etc/init.d/functions.sh ## # Print usage when option is -h, --help, or not understood. # Usually quit afterwords. print_usage() { # TODO: Make this more complete. cat << EOF Usage: $APP_NAME [OPTIONS] [--] [PACKAGE_MANAGER_OPTIONS] Broken reverse dependency rebuilder. -d, --debug Way too much information (uses bash's set -xv) -h, --help Print this usage -m, --package-manager Specify a package manager (default is Portage) -q, --quiet Less output -p, --pretend Pass --pretend to package-manager -v, --verbose More output All options after -- are passed directly to your package manager. Examples: - $APP_NAME -m pkgcore -pv - $APP_NAME --quiet -- --ask Report bugs to EOF } ## # Exit and optionally output to sterr. die() { local status=$1 shift [[ $1 ]] && eerror "$@" exit $status } ## # scan the filesystem for information about binaries. do_scanelf() { # Block "Permission denied" errors with 2> /dev/null scanelf -BlpRF'%F %n' 2> /dev/null } ## # Get a list of orphaned files get_orphans() { local IFS=$'\n' awk '{ $1 = "" # Ignore the binary split($2, sonames, ",") # Split across commas for(soname in sonames) { soname = sonames[soname] # Reassign index to value if(!seen[soname]++) print soname # Print if unique and not masked } }' <<< "$scanelf" | grep -Fv "${orphans_to_skip[*]}" | qfile -Cof- } ## # Get a list of objects that need orphaned files. get_needful_objs() { local IFS=$'\n' grep -F "${orphans[*]}" <<< "$scanelf" | awk '{print $1}' } ## # Get a list of packages that own the objects that need orphaned files. get_needful_pkgs() { qfile -qvC "${needful_objs[@]}" | sort -u } ## # Format the list of packages as cat-egory/pkg:SLOT get_rebuild_list() { local pkg for pkg in "${needful_pkgs[@]}"; do qatom "$pkg" | awk '{printf("%s/%s:",$1,$2)}' echo $(<"/var/db/pkg/$pkg/SLOT") done } ## # Verbose output -- only has an effect when --verbose flag is used chatter() { :; } ## # Get whole-word commandline options preceded by two dashes. get_longopts() { case $1 in --debug) set -xv;; --help) print_usage exit 0;; --package-manager) if (( $# > 1 )); then shift get_pm "$@" else get_pm "${1#*=}" fi;; --pretend) pm_opts+=("--pretend");; --quiet) einfo() { :; }; ewarn() { :; };; --verbose) chatter() { einfo "$@"; };; *) print_usage die 1 "Encountered unrecognized option $1";; esac } ## # Get single-letter commandline options preceded with a single dash. get_shortopts() { local OPT OPTSTRING OPTARG OPTIND while getopts ":dhm:qpv" OPT; do case $OPT in d) set -xv;; h) print_usage exit 0;; m) get_pm "$OPTARG";; p) pm_opts+=("-p");; q) einfo() { :; }; ewarn() { :; };; v) chatter() { einfo "$@"; };; *) print_usage die 1 "Encountered unrecognized option $OPTARG";; esac done } ## # Get command-line options. get_opts() { local -a args while [[ $1 ]]; do case $1 in --) shift; break;; -*) while true; do args+=("$1") shift [[ ${1:--} = -* ]] && break done if [[ ${args[0]} = --* ]]; then get_longopts "${args[@]}" else get_shortopts "${args[@]}" fi;; *) print_usage die 1 "Unrecognized option $1";; esac unset args done pm_opts=( "$@" ) # Make sure we have superuser rights or --pretend is set if (( UID )) && is_real_merge; then ewarn "You are not superuser." ewarn "Prepending --pretend to package manager options." pm_opts=("--pretend" "${pm_opts[@]}") fi (( ${#pm_opts} )) && chatter "Package manager options set to ${pm_opts[@]}" } ## # is_real_merge() { local IFS=$'\a' # Using BEL as a delimiter is usually safe. case $'\a'"${pm_opts[*]}"$'\a' in # If one of the elements is -p or --pretend, return false *$'\a'--pretend$'\a'*|*$'\a'-p$'\a'*) return 1;; esac # The merge is not --pretend, return true return 0; } ## # Package-manager wrapper function. # Ideally in the future this could be specified by an external config file # (in a hopefully secure manner). remerge() { ewarn "No package manager specified. Using Portage by default." get_pm Portage # Redefines remerge() remerge "$@" } ## # Redefine the package-manager wrapper function based on commandline options. # TODO: Get this from a config file. get_pm() { if [[ ! $1 ]]; then ewarn "You must specify a package manager" return 1 fi chatter "Setting package manager to $1." shopt -s nocasematch case $1 in Pkgcore) if hash pmerge 2> /dev/null; then remerge() { pmerge -1D "$@"; } else eerror "Unable to find pmerge binary in PATH." eerror "Please make sure it's properly installed." ewarn "Falling back to Portage default." get_pm Portage fi;; Paludis) if hash Paludis 2> /dev/null; then remerge() { paludis -1i "$@"; } else eerror "Unable to find Paludis binary in PATH." eerror "Please make sure it's properly installed." ewarn "Falling back to Portage default." get_pm Portage fi;; Portage) if hash emerge 2> /dev/null; then remerge() { emerge -1D "$@"; } else eerror "Unable to find emerge binary in PATH." eerror "Please make sure Portage is properly installed" eerror "or specify an alternative package manager." die 1 "Unable to continue without a package manager." fi;; *) eerror "Unknown package manager $1:" eerror 'Valid values are "Portage", "Pkgcore", and "Paludis".' ewarn "Using Portage by default." get_pm Portage;; esac shopt -u nocasematch } get_env() { [[ -d /etc/revdep-rebuild ]] || return 0 local conf for conf in /etc/revdep-rebuild/*; do [[ -r $conf ]]&& orphans_to_skip+=($(. "$conf"; echo "$LD_LIBRARY_MASK")) done } ## # Usage: countdown n # n: number of seconds to count countdown() { local i for ((i=1; i<$1; i++)); do echo -ne '\a.' ((i<$1)) && sleep 1 done echo -e '\a.' } do_remerge() { einfo 'All prepared. Starting rebuild' is_real_merge && countdown 10 # Link file descriptor #6 with stdin so --ask will work exec 6<&0 remerge "${pm_opts[@]}" "${unsorted_pkgs[@]}" <&6 # Now restore stdin from fd #6, where it had been saved, and close fd #6 # ( 6<&- ) to free it for other processes to use. exec 0<&6 6<&- } main() { get_opts "$@" # HACK: Don't depend on r-r config files in the future. einfo "Grabbing masks from /etc/revdep-rebuild" get_env echo einfo "Generating list of binaries" einfo "(This takes a while the first time, but subsequent runs are faster.)" local scanelf=$(do_scanelf) echo einfo "Generating list of orphans" local orphans=($(get_orphans)) (( ${#orphans[@]} )) || die 0 "No orphaned files found -- nothing to fix" ewarn "The following files are orphans: ${orphans[@]}" echo einfo "Generating list of binaries linking to those orphans" local needful_objs=($(get_needful_objs)) (( ${#needful_objs[@]} )) || die 1 "No objects appear to need those files. This may indicate a bug." ewarn "The following files link to those orphans: ${needful_objs[@]}" echo einfo "Generating list of packages to rebuild" local needful_pkgs=($(get_needful_pkgs)) local unsorted_pkgs=($(get_rebuild_list)) ewarn "The following packages need to be rebuilt: ${unsorted_pkgs[@]}" echo do_remerge } main "$@"