#!/bin/bash # Shared library & executable dependency finder for Linux # Copyright (C) 2018 Pekka Helenius # # 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 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 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 . ###################################################################################################################### # TODO print stored dependency packages as red if errors encountered # TODO check packages themselves, too (not only their dependencies...) # TODO # BUG : # # Optional dependencies for linux : # # crda: to set the correct wireless channels of your country # # Required dependencies for linux : # # coreutils Found libraries/executables: 102 # grep: Unmatched [ or [^ # grep: Invalid regular expression # TODO # Depends on: libtinfo.so.6 Provided by: # ei anna oikeaa tulosta... # TODO #Error: octopi not found. Skipping. # Lisää tähän "do you mean...?...logiikka # ------------------------------------------- # BUG # Ignore rules.d folder since it gives permission denied error # ------------------------------------------- # BUG #1/1 (100%) - ncurses #Required dependencies for ncurses : # glibc Found libraries/executables: 302 #ldd: warning: you do not have execution permission for `/usr/lib/libcursesw.so' #error: failed to find 'dynamic' in PATH: No such file or directory #ldd: warning: you do not have execution permission for `/usr/lib/libform.so' #error: failed to find 'dynamic' in PATH: No such file or directory #ldd: warning: you do not have execution permission for `/usr/lib/libmenu.so' #error: failed to find 'dynamic' in PATH: No such file or directory #ldd: warning: you do not have execution permission for `/usr/lib/libncurses++.so' #error: failed to find 'dynamic' in PATH: No such file or directory # gcc-libs-multilib Found libraries/executables: 17 # ------------------------------------------- # BUG # When using command 'pkgdeps wine-git', the following error occurs: #... # lib32-libsm Found libraries/executables: 1 #error: failed to find 'dynamic' in PATH: No such file or directory #error: failed to find 'dynamic' in PATH: No such file or directory ###################################################################################################################### # Look for missing library files for installed packages red=$'\33[91m' orange=$'\033[38;5;208m' yellow=$'\033[93m' green=$'\033[92m' blue=$'\033[94m' bold=$'\033[1m' reset=$'\033[0m' COMMANDS=(sudo pacman date ping package-query) for com in $COMMANDS; do if [[ ! $(which $com |grep "which: no $com in" | wc -w) -eq 0 ]]; then echo -e "\nCommand $com not found. Can't run the script.\n" return fi done echo -e "\n${bold}Dependency tracker - Find broken executables & libraries${reset}" if [[ $# -eq 1 ]]; then pacman -Q $1 2> /dev/null #|| echo 'something' # TODO silent normal output! if [[ $? -eq 1 ]]; then seterror=1 else seterror=0 fi else seterror=0 fi function checkconnection() { PROVIDER="luna.archlinux.org" INTERNET_TEST=$(ping -c 1 $PROVIDER 2>&1 | grep -c "Name or service not known") if [[ ! $INTERNET_TEST -eq 0 ]]; then echo -e "\n${red}Error:${reset} Can't connect to $PROVIDER. Please check your internet connection.\n" else echo -e "\nUpdating file databases with pacman (root required)\n" sudo pacman -Fy fi } LOGFILE=/var/log/pacman.log if [[ -f $LOGFILE ]] && [[ $seterror -ne 1 ]]; then LASTRUN=$(date --date="$(awk '/pacman -Fy/ {a=$0} END{print a}' $LOGFILE | awk -F'[][]' '{print $2}')" +"%s") if [[ $? -ne 0 ]]; then echo -e " ${red}Error:${reset} Can't parse the latest 'pacman -Fy' date value from pacman log file $LOGFILE! Skipping database update.\n" else if [[ $(echo $LASTRUN | wc -w) -eq 1 ]]; then CURTIME=$(date +"%s") LASTUPDATE=$(( ($CURTIME - $LASTRUN)/60 )) TIME_THRESHOLD=180 # 3 hours if [[ $LASTUPDATE -ge $TIME_THRESHOLD ]]; then echo -e "\nPackage databases updated $LASTUPDATE minutes ago.\n" # TODO minutes....hours...days...ago... read -r -p "Do you want to update databases now? [y/N] " dbupdate if [[ $(echo $dbupdate | sed 's/ //g') =~ ^([yY][eE][sS]|[yY])$ ]]; then checkconnection else echo -e "\nSkipping database update.\n" fi else echo -e "\nPackage databases are updated.\n" fi else echo -e "\nPrevious database update timestamp not found. Updating...\n" checkconnection fi fi elif [[ ! -f $LOGFILE ]]; then echo -e "\n ${red}Error:${reset} Pacman log file not found.\n" fi pkgcount=$# curpkgcount=1 typeset -A CHECKEDLIST # Loop through all input arguments (package names) for p in $(echo "${@}"); do # TODO show how many dependencies this package has if more than 1 LOCALDIRS=("/usr/lib" "/usr/bin" "/usr/lib/" "/opt/") w=0 for dir in $(pacman -Ql $p 2> /dev/null | awk '{print $2}'); do if [[ -d $dir ]]; then MAINPKGDIRS[$w]="${dir}" let w++ fi done echo -e "----------------------------------------" echo -e "\n$curpkgcount/$pkgcount ($((100*$curpkgcount/$pkgcount))%) - ${orange}$p${reset}" # Check that the input package can actually be found if [[ $(pacman -Qi $p &>/dev/null || echo "was not found" | wc -l) -eq 1 ]]; then echo -e "\n ${red}Error:${reset} ${orange}$p${reset} not found. Skipping.\n" else # Parse dependencies of the selected package to a string list PKGDEPS=$(pacman -Qi $p | grep "Depends On" | sed 's/.*: //g; s/ / /g') #Parse optional dependencies PKGDEPS_OPT=$(pacman -Qi $p | grep "Optional Deps" | sed 's/.*\s: //; s/ / /') # Loop for each package ([@] means array value X) # We circumvent a pacman bug/issue here # pacman does a hard fixed listing for packages, meaning it doesn't recognize "provides" option # This is mainly a problem of git package versions # if [[ $PKGDEPS_OPT != "None" ]]; then echo -e "\nOptional dependencies for ${orange}$p${reset} :\n\n $PKGDEPS_OPT" fi if [[ $PKGDEPS != "None" ]]; then echo -e "\nRequired dependencies for ${orange}$p${reset} :\n" # TODO go even one step deeper with these! for i in $(echo "${PKGDEPS[@]}"); do function parseversion() { pattern="[<=>]" if [[ $i =~ $pattern ]]; then i=$(echo $i | awk -F'[<=>]' '{print $1}') # TODO do version check here as done in 'risks' function? #i_ver=$(echo $i | awk -F'[<=>]' '{print $3}') #return $pkgname $pkgver #else #return $i fi } parseversion # Error (package was not found) if [[ $(pacman -Qi $i &>/dev/null || echo "was not found" | wc -l) -eq 1 ]]; then # Get name of the package which provides the dependency ALTDEP=$(pacman -Qi | grep -iE "Name|Provides" | grep $i | grep "Name" | sed 's/.*: //') o=0 for altdep in $ALTDEP; do ALTDEPS[$o]=$altdep let o++ done function deepscan() { MAINPKGFILES=$(pacman -Ql $p | awk '{print $2}') for mainpkgfile in $MAINPKGFILES; do if [[ $(mimetype "${mainpkgfile}" | grep -iE "x\-sharedlib|x\-executable" | wc -l) -eq 1 ]]; then for deepscan in $(ldd "${mainpkgfile}" | awk '{print $3}' | sed '/^\s*$/d'); do OWNER_DEEPSCAN=$(pacman -Qo $deepscan | awk '{print $5}') for item in "${ALTDEPS[@]}"; do if [[ $OWNER_DEEPSCAN == "$item" ]]; then i=$OWNER_DEEPSCAN return 0 fi done done fi done return 1 } deepscan if [[ $? -eq 1 ]]; then #Get 'provides' from arch package database FETCHEDPKGS=$(package-query --qprovides "$i" -Q -f "%n") k=0 for fetch in $FETCHEDPKGS; do if [[ ! $(pacman -Q $fetch &>/dev/null || echo "was not found" | wc -l) -eq 1 ]]; then i="$fetch" break # TODO show here the true 'depends on' stuff, not just 'provided by' package? bash <-> sh conversion for example else echo -e " ${yellow}Warning:${reset} Dependency '$i' not found and not recognized as a provided package" i="notfound" NOTFOUNDS[$k]=$i let k++ fi done fi fi if [[ $i != "notfound" ]] && [[ -z ${CHECKEDLIST[$i]} ]]; then FILES=$(pacman -Ql $i | awk '{print $2}') x=0 printf " %-30s%s\r" "$i" "Searching libraries/executables" for mimefile in $FILES; do if [[ $(mimetype "${mimefile}" | grep -iE "x\-sharedlib|x\-executable" | wc -l) -eq 1 ]]; then MIMEFILES[$x]="${mimefile}" let x++ printf " %-30s%s\r" "$i" "Found libraries/executables: $x " #Yes, the trailing space is meant to be here fi done if [[ $x -gt 0 ]]; then CHECKEDLIST[$i]=$(printf " %-30s%s\n" "$i" "Found libraries/executables: $x") else CHECKEDLIST[$i]=$(printf " %-30s%s\n" "$i" "No libraries/executables") fi # Replace the current line (100 characters) with empty space and fill it with checkedlist value echo -e "\r$(for ((i = 0; i < 100; i++)); do echo -n ' '; done)\r${CHECKEDLIST[$i]}" # Go through all listed library & executable files for n in $(echo "${MIMEFILES[@]}"); do # Get count of all matching lines for a file. If no errors, this should return 1 # Enable bash debugging with set -x and run it in subshell (otherwise we need to use set +x to disable debugging) # As debugging output is considered as STDERR (not default STDOUT), we pipe it with |& # Suppress ldd warning messages ("you have no permission") as they are not relevant here and delete empty lines LDDCMDCNT=$( (set -x; ldd $n |grep -i "not found") |& grep -E "$n|=> not found" | sed 's/.*warning.*//g; /^\s*$/d' | wc -l) # Get all lines for a file. If no errors, this should return only "++ ldd " LDDCMD=$( (set -x; ldd $n |grep -i "not found") |& grep -E "$n" | sed 's/.*warning.*//g; /^\s*$/d') LDDCMD_FILE=$(echo "$LDDCMD " | sed 's/++ ldd //g') # Basename doesn't accept empty/null parameters if [[ ! -z "${LDDCMD_FILE// }" ]]; then LDDCMD_FILE_BASENAME=$(basename $LDDCMD_FILE) # | sed "s/[-0-9.]*$//" fi # If we have match for "not found" messages, print the output if [[ $LDDCMDCNT -gt 1 ]]; then LIBNOTFOUND=true function checklocaldirs() { unset PKG for filepkgdir in $1; do #echo "Search dir is $filepkgdir Search term is $2" findfile=$(find "$filepkgdir" -maxdepth 1 -type f -iname "*$2*") if [[ -f $findfile ]]; then PKG="$(pacman -Qo $findfile | awk '{print $5 " " $6}')" return fi done } function checkrepopkgs() { unset PKG unset NIPKG REPOPKGS=$(pacman -Fs $1 | sed '2~2d' | awk -F '[/ ]' '{print $2}') if [[ ! -z $REPOPKGS ]]; then c=0 for repopkg in $REPOPKGS; do t=0 for repopkgfile in $(pacman -Fl "$repopkg" | awk '{print "/"$2}'); do if [[ -f $repopkgfile ]]; then REPOPKGFILES[$t]="$repopkgfile" let t++ fi done if [ ! "${#REPOPKGFILES[@]}" -eq 0 ]; then for matchrepofile in "${REPOPKGFILES[@]}"; do if [[ "${matchrepofile}" == "${2}" ]]; then PKG=$repopkg return # else # if [[ $(pacman -Qo $matchrepofile | grep -o "owned by" | wc -w) -eq 2 ]] # PKG=$(echo "${REPOPKGS[*]}?" | tr '\n' ',' | sed 's/\,*$//g') # #return fi done unset REPOPKGFILES else #echo "do we get here $repopkg" if [[ $(pacman -Q $repopkg &>/dev/null || echo "was not found" | wc -l) -eq 1 ]]; then NIPKG[$c]="$repopkg" let c++ fi #PKG="${red}N/A${reset}" fi done #PKG="${red}N/A${reset}" else PKG="${red}N/A${reset}" fi } checkrepopkgs $LDDCMD_FILE_BASENAME $LDDCMD_FILE # MAINPKG=$PKG #REPOPKG=$(pacman -Fs $(echo $n | awk '{print $NF}' FS=/) | sed '1!d' | awk -F '[/ ]' '{print $2}') #if [[ -z ${PKG} ]]; then # PKG="${red}N/A${reset}" # fi NOTFOUNDFILES=$( (set -x; ldd $n |grep -i "not found") |& grep -E "=> not found" | sed 's/ => not found//g; s/\s//g' | tr '\n' ' ' | sed 's/.*warning.*//g; /^\s*$/d') printf "\n %s %s\n" "${red}=>${reset} Missing or broken dependencies for" "${yellow}$LDDCMD_FILE${reset}" printf " %-38s%s\n" " Owned by system package:" "$(pacman -Qo $n | sed 's/.* by //g')" # if [[ ! $MAINPKG == *"N/A"* ]] || [[ ! -z "${NIPKG}" ]]; then # printf " %-38s%s\n" " Provided by..." # if [[ ! $MAINPKG == *"N/A"* ]]; then # printf " %-38s%s\n" " ...installed repo package(s):" "$MAINPKG" # fi if [[ ! -z "${NIPKG}" ]]; then printf " %-38s%s\n" " Provided by package(s):" "${NIPKG[*]}" # printf " %-38s%s\n" " ...non-installed repo package(s):" "${NIPKG[*]}" fi # fi printf "\n" # Set up a new array for r variables (libary files) and store printf output there. # This is just so that we don't need to check these library files again but instead # we can use a stored value. This is just to speed up processing. # TODO does this really work? r_count=0 typeset -A RLIST for r in $(echo ${NOTFOUNDFILES[@]}); do if [[ ! ${RLIST[$r_count]} ]]; then checklocaldirs "${MAINPKGDIRS[*]}" $r #$r_basename if [[ -z ${PKG} ]]; then checklocaldirs "${LOCALDIRS[*]}" $r #$r_basename fi LIBPKG=$PKG RLIST[$r_count]=$(printf " %-47s%-30s%s\n" " ${red}Depends on${reset}:" "$r" "${blue}Provided by${reset}: $LIBPKG") fi echo "${RLIST[$r_count]}" #unset MAINPKG #unset PKG #r_basename=$(echo $r | sed "s/[-0-9.]*$//") # libicui18n.so.58.4 -> libicui18n.so #unset PKG #if [[ -z ${PKG} ]]; then # checkrepopkgs $r_basename #if [[ -z ${PKG} ]]; then # PKG="${red}N/A${reset}" #fi # fi #check_main $r_basename #checkrepopkgs $r_basename $r # if [[ -z ${PKG} ]]; then # PKG="N/A" # fi # for r_providers in $(pacman -Fs $r_basename | sed '2~2d' | awk -F '[/ ]' '{print $2}'); do # TODO #LIBPKG[0]=$(pacman -Fs $(echo $r | awk '{print $NF}' FS=/) | sed '1!d' | awk -F '[/ ]' '{print $2}') #LIBPKG=$(pacman -Fs $(echo $r | awk '{print $NF}' FS=/) | sed '1!d' | awk -F '[/ ]' '{print $2}') #LIBPKG=$(pacman -Fs $r | sed '1!d' | awk -F '[/ ]' '{print $2}') #if [[ "${#LIBPKG[@]}" -eq 0 ]]; then #if [[ $(echo $LIBPKG | wc -w) -eq 0 ]]; then # LIBPKG="N/A" #fi #printf " %-47s%-30s%s\n" " ${red}Depends on${reset}:" "$r" "${blue}Provided by${reset}: $LIBPKG" #unset LIBPKG #unset PKG #unset MAINPKG let r_count++ done unset RLIST fi done unset MIMEFILES #Unset MIMEFILES array for the next dependency check else printf '%s\n' "${CHECKEDLIST[$i]}" fi done else echo -e "\n No dependencies for this package." fi if [[ ! -z "${NOTFOUNDS}" ]] || [[ $LIBNOTFOUND != true ]]; then echo -e "\n ${green}=>${reset} Dependencies checked. All ${green}OK${reset}.\n" else echo -e "\n ${red}=>${reset} Dependency problems discovered. See above messages for details.\n" fi unset NOTFOUNDS unset LIBNOTFOUND fi let curpkgcount++ done