#!/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-2018 - Released under the GNU GPL." helpOnNoArguments=yes scriptUsage=" [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 whether they should be forced on conflicts." "no" "yes no force" Add_Option_Boolean "d" "install-dependencies" "Attempt to download and install dependencies" Parse_Options "$@" ### Functions ################################################################# function pretty_name() { local program="$1" local pretty=$(GuessProgramCase "$program") [ "$pretty" = "$program" ] && pretty=$(NamingConventions "$program") echo "$pretty" } 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 ${wget_cmd} -c "${inputfile}" -O "${goboTemp}/${filename}" || Die "Error downloading package." echo "${goboTemp}/${filename}" else echo "$(readlink -f $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) 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 Log_Debug "$filenames" } 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 } function remove_links_to_opt() { local fromdir="$1" find "$fromdir/bin" "$fromdir/sbin" -type l 2> /dev/null | while read symlink do readlink -f "$symlink" | grep -q "/opt/" && rm ${verbose} -f "$symlink" done } # Prune empty top-level directories and symlinks rmdir ${verbose} * 2> /dev/null for linkname in bin sbin lib lib64 do [ -L "$linkname" ] && rm ${verbose} -f "$linkname" done if [ -d "./usr" ] then remove_links_to_opt "./usr" cp ${verbose} -a ./usr/* . rm -rf -- ./usr fi if [ -d "./local" ] then remove_links_to_opt "./local" cp ${verbose} -a ./local/* . rm -rf -- ./local fi if [ -d "./etc" ] then mkdir -p Resources/Defaults/Settings Quiet mv ./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 Quiet mv ./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 depinfo=$(lookup_pkgname "$inputfile" "$dependency") if [ "$depinfo" ] then echo "$depinfo" else echo "# Unresolved path-based library dependency: $dependency" fi 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 "$inputfile" "$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_name "$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_name "$inputfile") local distrocode=$(thirdparty_distribution_code "$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 $(thirdparty_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 Log_Verbose "Symbol $symbol not found, will resort to $depname $depversion as fallback" fi 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" if [ "$symbol" ] then Log_Verbose "Looking for symbol on candidate file $candidate ($depname, $testversion)" lookup_symbol "$depname" "$testversion" "$arch" "$symbol" && return 0 fi 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. Log_Normal "Searching the remote $(thirdparty_backend) database for the package providing $originalpath" depname=$(thirdparty_search_remotedb "$originalpath" "$arch" "$distro" "$distrocode") if [ "$depname" ] then # TODO: we could now lookup the GoboLinux recipe store to find whether we # have it or not echo "$(pretty_name $depname)" return 0 fi fi } function lookup_pkgname() { local inputfile="$1" local dependency="$2" 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. 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 # Query the remote database for $pkgnames if [ "$pkgname" != "$dependency" ] then pkgnames=( "$pkgname" "$dependency" ) else pkgnames=( "$pkgname" ) fi for pkgname_to_search in "${pkgnames[@]}" do Log_Normal "Searching the remote $(thirdparty_backend) database for the package providing $pkgname_to_search" local arch=$(thirdparty_arch "$inputfile") local distro=$(thirdparty_distribution_name "$inputfile") local distrocode=$(thirdparty_distribution_code "$inputfile") local depname=$(thirdparty_search_remotedb "$pkgname_to_search" "$arch" "$distro" "$distrocode") if [ "$depname" ] then local originalname=$(echo $depname | cut -d" " -f1) local prettyname="$(pretty_name $originalname)" local result=$(echo "$depname" | sed "s,$originalname,$prettyname,1") Log_Verbose "$result" echo "$result" && return 0 fi done 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) if [ "$programname" = `echo "$programname" | tr '[:upper:]' '[:lower:]'` ] then Log_Normal "Program name is $(pretty_name $programname)" else Log_Normal "Program name is $programname" fi # If a package with the same name is already installed then reuse it to prevent CamelCaseInconsistencies existing=$(ls $goboPrograms/ | grep -i "^${programname}$") [ "$existing" ] && programname="$existing" PrepareProgram --batch --keep -t "$programname" "$programversion" programname=$(ls $goboPrograms/ | grep -i "^${programname}$") target="$goboPrograms/$programname/$programversion" } function has_mixed_packages() { grep -vq "\.pkg.tar" <<< "$@" local has_arch=$? grep -vq "\.deb" <<< "$@" local has_deb=$? grep -vq "\.rpm" <<< "$@" local has_rpm=$? # only one of these should be 1 for mutual exclusivity [ "$(( $has_arch + $has_deb + $has_rpm ))" != "1" ] } ### Operation ################################################################# Is_Writable "${goboPrograms}" || Verify_Superuser if Boolean "verbose" then verbose="--verbose" else verbose= fi # Sanity checks, then import the backend to handle the input package(s) if has_mixed_packages $@ then Die "Error: cannot handle package archives of more than one format in one shot." elif echo "$@" | grep -q "\.rpm" then Import RPM elif echo "$@" | grep -q "\.deb" then Import DEB elif echo "$@" | grep -q "\.pkg.tar" then Import Arch else Die "Error: this tool can only handle RPM, DEB, and Arch Linux archives." 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=() eval `Args_To_Array inputfiles_` for entry in "${inputfiles_[@]}" do Is_URL "$entry" && continue [ ! -f "$entry" ] && Die "$entry: file not found" done for entry in "${inputfiles_[@]}" do inputfile=$(fetch_package "$entry") inputfiles+=( "$inputfile" ) inputnames+=( "$(thirdparty_name $inputfile)" ) done inputfilescount=$(( "${#inputfiles[@]}" -1 )) # 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 i in $(seq 0 $inputfilescount) do inputfile="${inputfiles[$i]}" Log_Normal "Processing $(basename $inputfile)" prepare_program_entry "$inputfile" Quiet pushd "$target" || Die "Could not enter $target" uncompress_package "$inputfile" flatten_package "$inputfile" populate_resources "$inputfile" Quiet popd if Is_URL "${inputfiles_[$i]}" then rm -f -- "$inputfile" fi done # Symlinking if [ $(Entry "symlink") != "no" ] then [ -d "$target/Resources/Defaults/Settings" ] && UpdateSettings "$programname" "$programversion" SymlinkProgram "$programname" "$programversion" fi # Install dependencies if Boolean "install-dependencies" then install_cmd="$0 --symlink=$(Entry symlink) --install-dependencies" cat "$target/Resources/Dependencies" 2> /dev/null | grep "http*://" | while read line do depname=$(echo "$line" | awk {'print $1'}) depurl=$(echo "$line" | awk {'print $NF'}) programname="$(pretty_name $depname)" if ! [ -d "$goboPrograms/$programname" ] then Log_Normal "Installing dependency $depname from $depurl" bash -c "$install_cmd -n $depname $depurl" fi done fi Log_Normal "Done."