diff options
Diffstat (limited to 'bin/ThirdPartyInstaller')
-rwxr-xr-x | bin/ThirdPartyInstaller | 508 |
1 files changed, 508 insertions, 0 deletions
diff --git a/bin/ThirdPartyInstaller b/bin/ThirdPartyInstaller new file mode 100755 index 0000000..131a31e --- /dev/null +++ b/bin/ThirdPartyInstaller @@ -0,0 +1,508 @@ +#!/bin/bash + +source ScriptFunctions +Import File +Import GoboLinux +Import Log +Import OptionParser + +### Options ################################################################### + +scriptDescription="Install RPM and DEB packages on GoboLinux." +scriptCredits="Copyright (C) Lucas C. Villa Real, 2016,2017 - Released under the GNU GPL." +helpOnNoArguments=yes +scriptUsage="<file.rpm> [file.rpm]" +scriptExample="xispita-2.0.3-1.x86_64.rpm" + +Add_Option_Entry "n" "app-name" "Override program name" +Add_Option_Entry "e" "version-number" "Override program version number" +Add_Option_Entry "l" "symlink" "If symlinks should be created and wether they should be forced on conflicts." "yes" "yes no force" +Add_Option_Boolean "W" "no-web" "Do not search the web to resolve dependencies." +Parse_Options "$@" + +### Functions ################################################################# + +function fetch_package() { + local inputfile="$1" + local filename=$(basename "$inputfile") + if Is_URL "$inputfile" + then + if wget --help | grep -q "no-check-certificate" + then wget_cmd="wget --no-check-certificate" + else wget_cmd="wget" + fi + Quiet ${wget_cmd} -c "${inputfile}" -O "${goboTemp}/${filename}" || Die "Error downloading package." + echo "${goboTemp}/${filename}" + else + echo "$inputfile" + fi +} + +function uncompress_package() { + local inputfile="$1" + thirdparty_uncompress "$inputfile" +} + +function determine_flattening_level() { + local rootdirs="bin$\|sbin$\|lib$\|lib64$\|libexec$\|include$\|share$" + local filenames=$(for i in "${inputfiles[@]}"; do thirdparty_filenames $i; done) + + Log_Verbose "$filenames" + if echo "$filenames" | grep -q "/opt/$rootdirs" + then + # 1-level /opt hierarchy + echo "1" + elif echo "$filenames" | grep -q "/opt/[^/]*/$rootdirs" + then + # 2-level /opt hierarchy + echo "2" + elif echo "$filenames" | grep -q "/opt" + then + # Get the longest common prefix among the $filenames list that starts + # with "/opt" and then determine how many path levels we have there. + local commonprefix=$(echo "$filenames" | grep "/opt" | sed -e 'N;s/^\(.*\).*\n\1.*$/\1\n\1/;D') + local numslashes=$(echo "$commonprefix" | grep -o "/" | wc -l) + let numslashes=numslashes-1 + if [ "$numslashes" = 1 ] + then + Log_Verbose "Package seems to have a 1-level /opt structure" + echo "1" + elif [ "$numslashes" = 2 ] + then + Log_Verbose "Package seems to have a 2-level /opt structure" + echo "2" + else + Log_Error "Could not determine this package's /opt structure, assuming 2-level" + echo "2" + fi + fi +} + +function flatten_package() { + local inputfile="$1" + + Log_Normal "Flattening directory structure." + + function create_opt_links() { + local vendordir="$1" + local unmanagedopt="Resources/Unmanaged/opt" + for pkgdir in $(basename --multiple ./opt/$vendordir/*) + do + mkdir ${verbose} -p "${unmanagedopt}/$vendordir/$pkgdir" + if [ -h "/opt/$vendordir/$pkgdir" ] + then + Log_Error "Too many programs are attempting to populate /opt/$vendordir/$pkgdir" + Log_Error "This feature is not supported by ThirdPartyInstaller at this time." + continue + fi + for optfile in $(basename --multiple ./opt/$vendordir/$pkgdir/*) + do + if [ ! -e "${unmanagedopt}/$vendordir/$pkgdir/$optfile" ] + then + ln ${verbose} -fs $target/$optfile "${unmanagedopt}/$vendordir/$pkgdir/$optfile" + fi + done + done + } + + function flatten_opt_1_level() { + # Flatten 1-level dir: opt/pkgname/{bin,sbin,...} + cp ${verbose} -a ./opt/*/* . + create_opt_links "" + realpath ./opt/* | sed "s,$(realpath $PWD),,g" >> Resources/UnmanagedFiles + } + + function flatten_opt_2_levels() { + # Flatten 2-levels dir: opt/vendorname/pkgname/{bin,sbin,...} + cp ${verbose} -a ./opt/*/*/* . + for vendordir in $(basename --multiple ./opt/*) + do + create_opt_links "$vendordir" + done + realpath ./opt/*/* | sed "s,$(realpath $PWD),,g" >> Resources/UnmanagedFiles + } + + if [ -d "./usr" ] + then + cp ${verbose} -a ./usr/* . + rm -rf -- ./usr + fi + if [ -d "./etc" ] + then + mkdir -p Resources/Defaults/Settings + mv ${verbose} ./etc/* Resources/Defaults/Settings + rm -rf -- ./etc + fi + if [ -d "./lib64" ] + then + Quiet rmdir lib + if [ ! -d "./lib" ] + then mv ./lib64 lib + else cp -a ./lib64/* ./lib/ + fi + rm -rf ./lib64 + fi + if [ -d "./var" ] + then + mkdir -p Resources/Unmanaged/$goboVariable + find ./var | sed "s,./var,$goboVariable,g" >> Resources/UnmanagedFiles + mv ${verbose} ./var/* Resources/Unmanaged/$goboVariable + rm -rf -- ./var + fi + if [ -d "./opt" ] + then + # Prevent the creation of backlinks to directories that would be pruned later on + find share -type d | xargs rmdir -p --ignore-fail-on-non-empty + rmdir * 2> /dev/null + mkdir -p Resources/Unmanaged/opt + if [ "$flatteninglevel" = "1" ] + then + Log_Verbose "Flattening 1-level /opt directory" + flatten_opt_1_level + elif [ "$flatteninglevel" = "2" ] + then + Log_Verbose "Flattening 2-level /opt directory" + flatten_opt_2_levels + fi + rm -rf -- ./opt + fi + if [ -e Resources/UnmanagedFiles ] + then + # If multiple RPM/DEB files are being merged, then ensure we have no dups + cat Resources/UnmanagedFiles | sort -n | uniq > x && mv x Resources/UnmanagedFiles + fi + rmdir * 2> /dev/null +} + +function is_inputfile() { + local dependency="$1" + local pkgname=$(echo "$dependency" | cut -d'(' -f1 | awk {'print $1'}) + printf "%s\n" "${inputnames[@]}" | grep -q "^${pkgname}$" && return 0 + return 1 +} + +function populate_dependencies() { + local inputfile="$1" + thirdparty_dependencies "$inputfile" | while read dependency + do + if echo "$dependency" | grep -q "^/" + then + depinfo=$(take_dependency_from_path "$inputfile" "$dependency" "") + if [ "$depinfo" ] + then echo "$depinfo" + else echo "# Unresolved path-based dependency: $dependency" + fi + elif echo "$dependency" | grep -q "^lib.*\.so*\|.*\.so\.*" + then + libname=$(echo "$dependency" | cut -d'(' -f1) + wantedsymbol=$(echo "$dependency" | cut -d'(' -f2 | cut -d')' -f1) + depinfo=$(take_dependency_from_path "$inputfile" "$goboLibraries/$libname" "$wantedsymbol") + if [ "$depinfo" ] + then echo "$depinfo" + else echo "# Unresolved path-based library dependency: $dependency" + fi + elif is_basic_symbol "$dependency" + then + Log_Verbose "Skipping basic symbol: $dependency" + elif is_inputfile "$dependency" + then + Log_Verbose "Skipping dependency passed as input file: $dependency" + else + depinfo=$(lookup_pkgname "$dependency") + if [ "$depinfo" ] + then echo "$depinfo" + else echo "# Unresolved dependency: $dependency" + fi + fi + done +} + +function populate_resources() { + local inputfile="$1" + local arch=$(thirdparty_arch "$inputfile") + local description=$(thirdparty_description "$inputfile") + local release=$(thirdparty_release "$inputfile") + local distro=$(thirdparty_distribution "$inputfile") + + Log_Normal "Populating Resources." + mkdir -p Resources + + if [ "$arch" ] + then echo "$arch" > Resources/Architecture + else echo "$(uname -m)" > Resources/Architecture + fi + echo "$release" > Resources/Revision + + # Note that we never truncate neither Resources/PackageSource nor + # Resources/Description or Resources/Dependencies. This is to + # enable the installation of multiple RPM/DEB files under the same + # /Programs entry while keeing metadata of all the original files + # around. + + [ -e Resources/PackageSource ] && echo >> Resources/PackageSource + echo "[File] $(basename $inputfile)" >> Resources/PackageSource + echo "[Distribution] $distro" >> Resources/PackageSource + + if [ "$description" ] + then + [ -e Resources/Description ] && echo "" >> Resources/Description + echo "[Name] $(thirdparty_name $inputfile)" >> Resources/Description + echo "[Summary] $(thirdparty_summary $inputfile)" >> Resources/Description + echo "[License] $(thirdparty_license $inputfile)" >> Resources/Description + echo "[Description] $(thirdparty_description $inputfile)" >> Resources/Description + echo "[Homepage] $(thirdparty_url $inputfile)" >> Resources/Description + fi + + Log_Normal "Processing dependencies." + alldeps=$(mktemp ThirdPartyInstaller.XXXXXXXXXX) + populate_dependencies "$inputfile" >> Resources/Dependencies + cat Resources/Dependencies | sort -n | uniq > "$alldeps" + cat "$alldeps" > Resources/Dependencies + rm -f -- "$alldeps" +} + +function lookup_symbol() { + local depname="$1" + local testversion="$2" + local arch="$3" + local symbol="$4" + local testarch=$(cat "$goboPrograms/$depname/$testversion/Resources/Architecture" 2> /dev/null) + + if [ "$testarch" ] && [ "$testarch" = "$arch" ] + then + Log_Verbose "Looking for symbol $symbol on $goboPrograms/$depname/$testversion/$path" + if nm "$goboPrograms/$depname/$testversion/$path" 2> /dev/null | grep --max-count=1 -q "$symbol" + then + Log_Verbose "Match: $depname $testversion" + echo "$depname $testversion" + return 0 + fi + fi + return 1 +} + +function take_dependency_from_path() { + # TODO since $2 may come in the form '/usr/lib/libfoo >= version', we have to pick the + # path from the first part of the string. Right now we're using awk to get it, but that + # will not work if the dependency path contains spaces. + + local inputfile="$1" + local originalpath=$(echo "$2" | awk '{print $1}') + local path=$(echo "$originalpath" | sed 's,/usr,,g') + local symbol="$3" + local fullpath="$(readlink -f $path)" + local arch=$(thirdparty_arch "$inputfile") + local distro=$(thirdparty_distribution "$inputfile") + + local depname= + local depversion= + + if echo "$fullpath" | grep -q "^${goboPrograms}" + then + # If given, we search for the presence of @symbol on the given target file. + # We iterate over different installations of the same program looking for + # that symbol. If none of the installations have it, we fallback to printing + # the dependency currently linked on /System/Index. + # + # Note that when iterating over installed programs we skip those entries whose + # Resources/Architecture do not match the output of $(rpminfo --arch). + + depname=$(echo "$fullpath" | cut -d/ -f3) + depversion=$(echo "$fullpath" | cut -d/ -f4) + if [ "$symbol" ] + then + for testversion in $(ls $goboPrograms/$depname/ | grep -v "Settings\|Variable\|Current") + do + lookup_symbol "$depname" "$testversion" "$arch" "$symbol" && return 0 + done + fi + + Log_Verbose "Fallback: $depname $depversion" + echo "$depname $depversion" + else + # We have a path, but we don't have a link to that file under /System/Index. + # Our first attempt is to search over the list of installed programs anyhow, + # because some programs may not be currently activated. + + for fullpath in $(ls $goboPrograms/*/*/$path 2> /dev/null | grep -v "Current") + do + depname=$(echo "$fullpath" | cut -d/ -f3) + testversion=$(echo "$fullpath" | cut -d/ -f4) + [ -z "$depversion" ] && depversion="$testversion" + Log_Verbose "Looking for symbol on candidate file $candidate ($depname, $testversion)" + lookup_symbol "$depname" "$testversion" "$arch" "$symbol" && return 0 + done + + # We don't have a match. If we have a file name that satisfies the path but + # that doesn't contain the requested symbol, we simply return that path. + + if [ "$depname" ] && [ "$depversion" ] + then + echo "$depname $depversion" + return 0 + fi + + # We don't have a matching filename under /System/Index nor under /Programs/*/*. + # What we do now is to query remote RPM/DEB databases to find which package hosts the + # dependency file. + + if ! Boolean "no-web" + then + Log_Normal "Searching the remote $(thirdparty_backend) database for the package hosting $originalpath" + depname=$(third_party_search_remotedb "$originalpath" "$arch" "$distro") + if [ "$depname" ] + then + # TODO: we could now lookup the GoboLinux recipe store to find whether we + # have it or not + echo "$(GuessProgramCase $depname)" + return 0 + fi + fi + fi +} + +function lookup_pkgname() { + local dependency="$1" + local pkgname=$(echo "$dependency" | cut -d'(' -f1) + + # GoboLinux doesn't have "devel" packages like most mainstream distros do + local goboname=$(echo "$pkgname" | sed 's,-devel$,,g') + + # Do we have a GoboLinux package installed with a matching name? + for testname in $(ls $goboPrograms/) + do + # Case-insensitive comparison (requires Bash 4) + if [ "${testname,,}" = "${pkgname,,}" ] || [ "${testname,,}" = "${goboname,,}" ] + then + echo "$testname" && return 0 + fi + done + + # Query the GoboLinux recipe store. + if ! Boolean "no-web" + then + + local recipeurl=$(FindPackage -t recipe "${pkgname}" || FindPackage -t recipe "${goboname}") + if [ "$recipeurl" ] + then + # TODO we're potentially discarding the wanted version(s) of the given dep + echo "$(basename $recipeurl | sed 's,\(.*\)--.*--.*,\1,g')" && return 0 + fi + fi + + # TODO Query the remote RPM database for $pkgname + + # TODO Query the remote DEB database for $pkgname + + return 1 +} + +function is_basic_symbol() { + local dependency="$1" + echo "$dependency" | grep -q "^rtld(" && return 0 + return 1 +} + +function deduce_program_name() { + local inputfile="$1" + local name=$(thirdparty_name "$inputfile") + echo $name +} + +function prepare_program_entry() { + local inputfile="$1" + + if [ ${#inputfiles[@]} -gt 1 ] && Is_Entry "app-name" && [ ! -z "$programname" ] + then + # We have already prepared this program's entry on /Programs + return + fi + if Is_Entry "app-name" + then programname=$(Entry "app-name") + else programname="$(deduce_program_name $inputfile)" + fi + if Is_Entry "version-number" + then programversion=$(Entry "version-number") + else programversion=$(printf "%s_%s" $(thirdparty_version "$inputfile") $(thirdparty_release "$inputfile")) + fi + + # Prepare /Programs tree and update program name (PrepareProgram may have changed its case) + PrepareProgram -t "$programname" "$programversion" + programname=$(ls $goboPrograms/ | grep -i "^${programname}$") + target="$goboPrograms/$programname/$programversion" +} + +### Operation ################################################################# + +Is_Writable "${goboPrograms}" || Verify_Superuser + +if Boolean "verbose" +then verbose="--verbose" +else verbose= +fi + +# The inputfiles array holds the full path of all RPM/DEB input files +# The inputnames array holds the package name of all RPM/DEB input files +inputfiles=() +inputnames=() +rpmcount=0 +debcount=0 +eval `Args_To_Array inputfiles_` +for entry in "${inputfiles_[@]}" +do + inputfiles+=( "$(readlink -f ${entry} || echo ${entry})" ) + inputnames+=( "$(rpminfo --name $entry)" ) + echo "$entry" | grep -qi "\.deb" && let debcount=debcount+1 + echo "$entry" | grep -qi "\.rpm" && let rpmcount=rpmcount+1 +done + +# Sanity checks, then import the backend to handle the package format +if [ $rpmcount -gt 0 ] && [ $debcount -gt 0 ] +then + Die "Error: cannot handle both RPM and DEB files in a single shot." +elif [ $rpmcount -gt 0 ] +then + Import RPM +elif [ $debcount -gt 0 ] +then + Import DEB +fi + +# These will be set by prepare_program_entry() +unset programname +unset programversion +unset target + +# Determine the flattening level of /opt. In other words, whether +# we have something like: +# /opt/pkgname/{bin,sbin...} (1-level), or +# /opt/vendorname/pkgname/{bin,sbin...} (2-level) + +flatteninglevel=$(determine_flattening_level) + +# Installation pipeline +for entry in "${inputfiles[@]}" +do + Log_Normal "Processing $(basename $entry)" + inputfile=$(fetch_package "$entry") + prepare_program_entry "$inputfile" + Quiet pushd "$target" || Die "Could not enter $target" + + uncompress_package "$inputfile" + flatten_package "$inputfile" + populate_resources "$inputfile" + + Quiet popd + Is_URL "$entry" && rm -f -- "$inputfile" +done + +# Symlinking +if [ $(Entry "symlink") = "no" ] +then + Log_Normal "Done." + exit 0 +fi + +[ -d "$target/Resources/Defaults/Settings" ] && UpdateSettings "$programname" "$programversion" +SymlinkProgram "$programname" "$programversion" +Log_Normal "Done." |