_build/deploy/collect_ldd_deps.sh
#!/bin/bash
# Script ONLY for execution.
[[ -n "$BASH" && (-z "$BASH_LINENO" || BASH_LINENO[0] -eq 0) ]] || return 0 || exit 0 # exit to avoid continue if the return can not be called
if [[ -z "$SOURCE_TACKLELIB_BASH_TACKLELIB_SH" || SOURCE_TACKLELIB_BASH_TACKLELIB_SH -eq 0 ]]; then
# builtin search
for BASH_SOURCE_DIR in '/usr/local/bin' '/usr/bin' '/bin'; do
if [[ -f "$BASH_SOURCE_DIR/bash_tacklelib" ]]; then
source "$BASH_SOURCE_DIR/bash_tacklelib" || exit $?
break
fi
done
fi
tkl_include_or_abort '../__init__/__init__.sh'
if [[ "$1" == "--help" || "$1" == "-h" ]]; then
echo "Usage: <SearchRoot> <File1>[:<File2>[:...[:<FileN>]]] <OutDepsFile> <OutDepsDir>"
echo "Example: $BASH_SOURCE_FILE_NAME .. \"*.so:*.so.*\" deps.lst ."
exit 1
fi
APP_ROOT="$1"
SEARCH_ROOT_LIST="$2" # directory path list where to start search files dependencies not recursively
FILE_LIST_TO_FIND="$3" # `:`-separated list of wildcard case insensitive file names or file paths to find
FILE_LIST_TO_EXCLUDE="$4" # `:`-separated list of wildcard case insensitive file names or file paths to exclude
LD_LIBRARY_PATH_LIST="$5" # directory path list for the LD_LIBRARY_PATH
OUT_DEPS_FILE="$6" # output dependencies text file
OUT_DEPS_DIR="$7" # directory there to copy found dependencies
if [[ -n "$OUT_DEPS_FILE" ]]; then
touch "$OUT_DEPS_FILE" 2> /dev/null || {
echo "$BASH_SOURCE_FILE_NAME: error: cannot create OUT_DEPS_FILE file: \"$OUT_DEPS_FILE\"." >&2
exit 2
} 1>&2
fi
if [[ ! -d "$OUT_DEPS_DIR" ]]; then
echo "$BASH_SOURCE_FILE_NAME: error: directory OUT_DEPS_DIR is not found: \"$OUT_DEPS_DIR\"." >&2
exit 3
fi
if [[ ! -d "$CWD" ]]; then
CWD="$APP_ROOT"
else
CWD="`readlink -f "$CWD"`"
fi
function FindFiles()
{
file_list_to_find=()
file_list_to_exclude=()
file_list_found=()
local IFS
local search_root
local file
local file2
local i
i=0
local IFS=$':\t\r\n'; for search_root in $SEARCH_ROOT_LIST; do
if tkl_pushd "$search_root"; then
local IFS=$':\t\r\n'; for file in $FILE_LIST_TO_FIND; do
if [[ -f "$file" ]]; then
file_list_to_find[i++]="$file"
echo " +$file"
(( i++ ))
fi
done
tkl_popd
else
echo "$BASH_SOURCE_FILE_NAME: error: search root is not found: \"$search_root\"." >&2
return 1
fi
done
(( ! ${#file_list_to_find[@]} )) && {
echo "$BASH_SOURCE_FILE_NAME: warning: file search list is empty." >&2
return 2
}
i=0
local IFS=$':\t\r\n'; for file in $FILE_LIST_TO_EXCLUDE; do
file_list_to_exclude[i++]="$file"
echo " -$file"
(( i++ ))
done
# for lowercase comparison globbing
local SHELLNOCASEMATCH=`shopt -p nocasematch`
shopt -s nocasematch
local file_name
local file_name2
local is_file_excluded
local iname_cmd_line=""
for file in "${file_list_to_find[@]}"; do
GetFileName "$file"
file_name="$RETURN_VALUE"
is_file_excluded=0
for file2 in "${file_list_to_exclude[@]}"; do
GetFileName "$file2"
file_name2="$RETURN_VALUE"
if [[ "$file_name" == "$file_name2" ]]; then
is_file_excluded=1
break # excluded
fi
done
if (( !is_file_excluded )); then
if [[ -n "$iname_cmd_line" ]]; then
iname_cmd_line="$iname_cmd_line -o -iname \"$file\""
else
iname_cmd_line="-type f -iname \"$file\""
fi
fi
done
# restore previous case comparison globbing
eval $SHELLNOCASEMATCH
i=0
local IFS=$':\t\r\n'; for search_root in $SEARCH_ROOT_LIST; do
local IFS=$' \t\r\n'; for file in `eval find "\$search_root/" $iname_cmd_line`; do
file_list_found[i++]="$file"
echo " -> $file"
(( i++ ))
done
done
return 0
}
function ReadCommandLineFlags()
{
local out_args_list_name_var="$1"
shift
local args
args=("$@")
local args_len=${#@}
local i
local j
j=0
for (( i=0; i < $args_len; i++ )); do
# collect all flag arguments until first not flag
if [[ "${args[i]//-/}" != "" && "${args[i]#-}" != "${args[i]}" ]]; then
eval "$out_args_list_name_var[j++]=\"\${args[i]}\""
shift
else
break # stop on empty string too
fi
done
}
function RemoveEmptyArgs()
{
RETURN_VALUE=()
local args
args=("$@")
local arg
local i
local j
i=0
j=0
for arg in "${args[@]}"; do
if [[ -n "$arg" ]]; then
RETURN_VALUE[j++]="$arg"
fi
(( i++ ))
done
}
function GetCanonicalPath()
{
local file_in="$1"
local file_in_abs
if [[ -n "$file_in" ]]; then
# Use `readlink` to convert a path from any not globbed path into canonical path, but
# use not existed path prefix to avoid convertion from a symlink.
if [[ "${file_in:0:1}" == "." || "${file_in:0:1}" != "/" ]]; then
file_in_abs="$(readlink -m "/::$(pwd)/$file_in")"
else
file_in_abs="$(readlink -m "/::$file_in")"
fi
RETURN_VALUE="${file_in_abs#/::}"
else
RETURN_VALUE="."
fi
}
function GetFileDir()
{
local file_in="$1"
if [[ -n "$file_in" ]]; then
RETURN_VALUE="${file_in%/*}"
[[ -z "$RETURN_VALUE" ]] && RETURN_VALUE="/"
else
RETURN_VALUE="."
fi
}
function GetFileName()
{
local file_in="$1"
RETURN_VALUE="${file_in##*/}"
}
function MakeSymlink()
{
local flag_args=()
ReadCommandLineFlags flag_args "$@"
(( ${#flag_args[@]} )) && shift ${#flag_args[@]}
local ignore_if_same_link_exist=0
local flag
local i
i=0
for flag in "${flag_args[@]}"; do
if [[ "${flag//I/}" != "$flag" ]]; then
ignore_if_same_link_exist=1
flag_args[i]="${flag//I/}" # remove external flag
if [[ -z "${flag_args[i]//-/}" ]]; then
flag_args[i]=""
fi
break
fi
(( i++ ))
done
RemoveEmptyArgs "${flag_args[@]}"
flag_args=("${RETURN_VALUE[@]}")
local Name="$1"
local Path="$2"
GetCanonicalPath "$Name"
local LinkPath="$RETURN_VALUE"
GetCanonicalPath "$Path"
local RefPath="$RETURN_VALUE"
if (( ignore_if_same_link_exist )); then
# check if symlink with the same path already exist
if [[ -f "$Name" && -L "$Name" ]]; then
local PrevRefPath="$(readlink -e "$Name")"
[[ "$RefPath" == "$PrevRefPath" ]] && return 0
fi
fi
echo ">ln: ${flag_args[@]} \"$LinkPath\" -> \"$RefPath\""
ln -s "${flag_args[@]}" "$Path" "$Name"
return $?
}
function CopyFile()
{
local flag_args=()
ReadCommandLineFlags flag_args "$@"
(( ${#flag_args[@]} )) && shift ${#flag_args[@]}
local create_symlinks=0
local flag
local i
i=0
for flag in "${flag_args[@]}"; do
if [[ "${flag//L/}" != "$flag" ]]; then
create_symlinks=1
flag_args[i]="${flag//L/}" # remove external flag
if [[ -z "${flag_args[i]//-/}" ]]; then
flag_args[i]=""
fi
break
fi
(( i++ ))
done
RemoveEmptyArgs "${flag_args[@]}"
flag_args=("${RETURN_VALUE[@]}")
local FILE_IN="$1"
shift
if [[ -z "$FILE_IN" ]]; then
echo "CopyFile: error: input file is not set." >&2
return 255
fi
# convert to canonical path
GetCanonicalPath "$FILE_IN"
local file_in="$RETURN_VALUE"
# split canonical path into components
GetFileDir "$file_in"
local file_in_dir="$RETURN_VALUE"
GetFileName "$file_in"
local file_in_name="$RETURN_VALUE"
local file
local file_dir
local file_name
local link_file
local link_file_dir
local copy_to_file
local copy_to_file_abs
local copy_to_file_dir
local copy_to_file_name
local copy_to_list
local i
local IFS=$' \t\r\n'; for file in `find "$file_in_dir" -maxdepth 1 -type f -name "$file_in_name" -o -type l -name "$file_in_name"`; do
if [[ -f "$file" && ! -L "$file" ]]; then
GetFileDir "$file"
file_dir="$RETURN_VALUE"
copy_to_list=()
i=0
for copy_to_file in "$@"; do
GetCanonicalPath "$copy_to_file"
copy_to_file_abs="$RETURN_VALUE"
if [[ -d "$copy_to_file_abs" ]]; then
GetFileDir "$copy_to_file_abs/"
else
GetFileDir "$copy_to_file_abs"
fi
copy_to_file_dir="$RETURN_VALUE"
if [[ "$file_dir" != "$copy_to_file_dir" ]]; then
copy_to_list[i++]="$copy_to_file_abs"
fi
done
if (( ${#copy_to_list[@]} )); then
tkl_call cp "${flag_args[@]}" "$file" "${copy_to_list[@]}" || return $?
fi
elif [[ -L "$file" ]]; then
if (( create_symlinks )); then
link_file="$(readlink -f "$file")"
# symlink to a regular file or another symlink
if [[ -f "$link_file" ]]; then
GetFileName "$link_file"
link_file_name="$RETURN_VALUE"
CopyFile "${flag_args[@]}" "$link_file" "$@" || return $?
GetFileName "$file"
file_name="$RETURN_VALUE"
if [[ "$link_file_name" != "$file_name" ]]; then
for copy_to_file in "$@"; do
GetCanonicalPath "$copy_to_file"
copy_to_file_abs="$RETURN_VALUE"
if [[ -d "$copy_to_file_abs" ]]; then
GetFileDir "$copy_to_file_abs/"
else
GetFileDir "$copy_to_file_abs"
fi
copy_to_file_dir="$RETURN_VALUE"
tkl_pushd "$copy_to_file_dir" && {
MakeSymlink -I "$file_name" "$link_file_name"
tkl_popd
}
done
fi
fi
fi
fi
done
return 0
}
function AppendItemToUArray()
{
# drop return value
RETURN_VALUE=-1
local i
local item
declare -a "UArraySize=(\${#$1[@]})"
for (( i=0; i<UArraySize; i++ )); do
eval "item=\"\${$1[i]}\""
if [[ "$item" == "$2" ]]; then
RETURN_VALUE=$i
return 1
fi
done
eval "$1[UArraySize]=\"\$2\""
RETURN_VALUE=$UArraySize
return 0
}
function RemoveItemFromUArray()
{
local IFS=$' \t\r\n' # workaround for the bug in the "[@]:i" expression under the bash version lower than 4.1
local i
local item
declare -a "UArraySize=(\${#$1[@]})"
for (( i=0; i<UArraySize; i++ )); do
eval "item=\"\${$1[i]}\""
if [[ "$item" == "$2" ]]; then
# remove it from the array
eval "$1=(\"\${$1[@]:0:\$i}\" \"\${$1[@]:\$i+1}\")"
return 0
fi
done
return 1
}
function CollectLddDeps()
{
local LDD_TOOL=ldd #alternative: `lddtee`
echo "Scanning for \"$FILE_LIST_TO_FIND\" with current working directory in \"$CWD\"..."
local file_list_to_find
local file_list_to_exclude
local file_list_found
FindFiles
local LastError=$?
(( LastError != 0 && LastError != 2 )) && return $LastError
(( LastError == 2 )) && return 0
if (( ! ${#file_list_found[@]} )); then
echo "$BASH_SOURCE_FILE_NAME: info: nothing to search." >&2
return 10
fi
echo
echo "Reading and collecting dependencies..."
local IFS
local LinkName
local Op
local RefPath
local Address
[[ -n "$OUT_DEPS_FILE" ]] && echo -n "" > "$OUT_DEPS_FILE"
(
# external shell process to isolate the change of exported variables
function ctrl_c()
{
echo
echo "** search interrupted **"
return 255
}
trap ctrl_c INT
# We use `LD_LIBRARY_PATH` to resolve all dependencies.
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH_LIST${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
echo
echo " LD_LIBRARY_PATH=$LD_LIBRARY_PATH"
echo
ldd_output_file="$(mktemp /tmp/ldd_output.XXXXXX)"
function on_exit()
{
rm "$ldd_output_file"
}
trap on_exit EXIT
function CheckCopyTo()
{
local FromFile="$1"
local ToDir="$2"
# read the link and if the end file has the same file in the destination but different content, then stop with error immediately
link_file="`readlink -f "$FromFile"`"
GetFileDir "$link_file"
link_file_dir="$RETURN_VALUE"
GetFileName "$link_file"
link_file_name="$RETURN_VALUE"
if [[ "$link_file_dir" != "$ToDir" ]]; then
if [[ -f "$ToDir/$link_file_name" ]]; then
if ! cmp "$link_file" "$ToDir/$link_file_name" > /dev/null; then
echo "$BASH_SOURCE_FILE_NAME: error: being copied dependency file is already exist with different content: \"$LinkName\" -> \"$link_file\" copy to \"$ToDir/\"" >&2
return 1
fi
return 255
fi
fi
return 0
}
# collect all not found dependencies to throw the error at the end of the search
not_found_lib_list=()
for scan_file in "${file_list_found[@]}"; do
echo " $scan_file"
[[ -n "$OUT_DEPS_FILE" ]] && echo "#%% $scan_file" >> "$OUT_DEPS_FILE"
# first check the exit code because `ldd` prints an error to stdout instead of stderr
$LDD_TOOL "$scan_file" > "$ldd_output_file" || continue
local IFS
while IFS=$' \t\r\n' read -r LinkName Op RefPath Address; do
if [[ "$Op" != "=>" ]]; then
Address="$RefPath"
RefPath="$Op"
fi
# ignore statical linkage message
if [[ "$LinkName" == "statically" && "$RefPath" == "linked" ]]; then
GetFileName "$scan_file"
# remove from not found
RemoveItemFromUArray not_found_lib_list "$RETURN_VALUE"
continue
fi
if [[ -n "$RefPath" && "${RefPath:0:1}" != "/" ]]; then
if [[ "${RefPath:0:1}" == "(" ]]; then
Address="$RefPath"
RefPath=""
fi
fi
if [[ "${Address:0:1}" == "(" ]]; then
Address="${Address:1:-1}"
fi
[[ -n "$OUT_DEPS_FILE" ]] && echo "$LinkName:$RefPath:$Address" >> "$OUT_DEPS_FILE"
if [[ -n "$RefPath" && -f "$RefPath" ]]; then
echo " V $LinkName -> $RefPath $Address"
CheckCopyTo "$RefPath" "$OUT_DEPS_DIR"
LastError=$?
if (( LastError > 0 && LastError < 255 )); then
return 20
elif (( ! LastError )); then
CopyFile -L "$RefPath" "$OUT_DEPS_DIR/" || return 11
fi
# remove from not found
RemoveItemFromUArray not_found_lib_list "$LinkName"
elif [[ -n "$LinkName" && -f "$LinkName" ]]; then
echo " L $LinkName -> $LinkName $Address"
CheckCopyTo "$LinkName" "$OUT_DEPS_DIR"
LastError=$?
if (( LastError > 0 && LastError < 255 )); then
return 21
elif (( ! LastError )); then
CopyFile -L "$LinkName" "$OUT_DEPS_DIR/" || return 12
fi
# remove from not found
RemoveItemFromUArray not_found_lib_list "$LinkName"
else
echo " X $LinkName -> ${RefPath:-X} $Address"
# append unique to not found list
AppendItemToUArray not_found_lib_list "$LinkName"
fi
done < "$ldd_output_file"
echo
if (( ${#not_found_lib_list[@]} )); then
echo " * not found: ${not_found_lib_list[@]}"
echo
fi
done
# for lowercase comparison globbing
local SHELLNOCASEMATCH=`shopt -p nocasematch`
shopt -s nocasematch
local file
local file_name
for file in "${file_list_to_exclude[@]}"; do
GetFileName "$file"
file_name="$RETURN_VALUE"
# remove specific not existed objects
RemoveItemFromUArray not_found_lib_list "$file_name"
done
# restore previous case comparison globbing
eval $SHELLNOCASEMATCH
if (( ${#not_found_lib_list[@]} )); then
echo "$BASH_SOURCE_FILE_NAME: error: having not found dependencies." >&2
echo "$BASH_SOURCE_FILE_NAME: info: not found dependencies list:"
for link_name in "${not_found_lib_list[@]}"; do
echo " ${link_name}"
done
echo
return 11
fi
)
return $?
}
CollectLddDeps || tkl_exit
echo "Done."
echo
tkl_exit 0