#!/bin/bash source ScriptFunctions Import File Import GoboLinux Import Log Import OptionParser ### Options ################################################################### scriptDescription="Install RPM packages on GoboLinux." scriptCredits="Copyright (C) Lucas C. Villa Real, 2016,2017 - 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 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_rpm() { local rpmfile="$1" local filename=$(basename "$rpmfile") if Is_URL "$rpmfile" 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 "${rpmfile}" -O "${goboTemp}/${filename}" || Die "Error downloading package." echo "${goboTemp}/${filename}" else echo "$rpmfile" fi } function uncompress_rpm() { local rpmfile="$1" local payload_compressor=$(rpminfo --compressor "$rpmfile") local cpiofile=$(basename "$rpmfile").cpio${payload_compressor:+.$payload_compressor} Log_Normal "Extracting RPM payload." rpm2cpio < "$rpmfile" > "$cpiofile" if [ "$payload_compressor" = "xz" ] then Log_Normal "Decompressing $payload_compressor payload." xz -d "$cpiofile" cpiofile=$(basename "$rpmfile").cpio fi Log_Normal "Extracting CPIO archive." cpio -d -i < "$cpiofile" rm -f -- "$cpiofile" } function determine_flattening_level() { local rootdirs="bin$\|sbin$\|lib$\|lib64$\|libexec$\|include$\|share$" local filenames=$(for i in "${rpmfiles[@]}"; do rpminfo --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_rpm() { local rpmfile="$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 InstallPackage-RPM 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 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" "${rpmnames[@]}" | grep -q "^${pkgname}$" && return 0 return 1 } function populate_dependencies() { local rpmfile="$1" rpminfo --dependencies "$rpmfile" | while read dependency do if echo "$dependency" | grep -q "^/" then depinfo=$(take_dependency_from_path "$rpmfile" "$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 "$rpmfile" "$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_rpmlib_symbol "$dependency" then Log_Verbose "Skipping internal 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 rpmfile="$1" local arch=$(rpminfo --arch "$rpmfile") local description=$(rpminfo --description "$rpmfile") local release=$(rpminfo --release "$rpmfile") local distro=$(rpminfo --distribution "$rpmfile") 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 files under the same # /Programs entry while keeing metadata of all the original files # around. [ -e Resources/PackageSource ] && echo >> Resources/PackageSource echo "[File] $(basename $rpmfile)" >> Resources/PackageSource echo "[Distribution] $distro" >> Resources/PackageSource if [ "$description" ] then [ -e Resources/Description ] && echo "" >> Resources/Description echo "[Name] $(rpminfo --name $rpmfile)" >> Resources/Description echo "[Summary] $(rpminfo --summary $rpmfile)" >> Resources/Description echo "[License] $(rpminfo --license $rpmfile)" >> Resources/Description echo "[Description] $(rpminfo --description $rpmfile)" >> Resources/Description echo "[Homepage] $(rpminfo --url $rpmfile)" >> Resources/Description fi Log_Normal "Processing dependencies." alldeps=$(mktemp InstallPackage-RPM.XXXXXXXXXX) populate_dependencies "$rpmfile" >> 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 rpmfile="$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=$(rpminfo --arch "$rpmfile") local distro=$(rpminfo --distribution "$rpmfile") 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 databases to find which package hosts the # dependency file. if ! Boolean "no-web" then Log_Normal "Searching the remote RPM database for the package hosting $originalpath" depname=$(RPMFinder --path="$originalpath" --arch="$arch" --distro="$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) # 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,,}" ] then echo "$testname" && return 0 fi done # Query the GoboLinux recipe store. if ! Boolean "no-web" then local recipeurl=$(FindPackage -t recipe "${pkgname}") 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 return 1 } function is_basic_symbol() { local dependency="$1" echo "$dependency" | grep -q "^rtld(" && return 0 return 1 } function is_rpmlib_symbol() { local dependency="$1" echo "$dependency" | grep -q "^VersionedDependencies" && return 0 echo "$dependency" | grep -q "^PayloadFilesHavePrefix" && return 0 echo "$dependency" | grep -q "^CompressedFileNames" && return 0 echo "$dependency" | grep -q "^PayloadIs" && return 0 return 1 } function deduce_program_name() { local rpmfile="$1" local name=$(rpminfo --name "$rpmfile") echo $name } function prepare_program_entry() { local rpmfile="$1" if [ ${#rpmfiles[@]} -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 $rpmfile)" fi if Is_Entry "version-number" then programversion=$(Entry "version-number") else programversion=$(printf "%s_%s" $(rpminfo --version "$rpmfile") $(rpminfo --release "$rpmfile")) 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 rpmfiles array holds the full path of all RPM input files # The rpmnames array holds the package name of all RPM input files rpmfiles=() rpmnames=() eval `Args_To_Array rpmfiles_` for entry in "${rpmfiles_[@]}" do rpmfiles+=( "$(readlink -f ${entry} || echo ${entry})" ) rpmnames+=( "$(rpminfo --name $entry)" ) done # 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 "${rpmfiles[@]}" do Log_Normal "Processing $(basename $entry)" rpmfile=$(fetch_rpm "$entry") prepare_program_entry "$rpmfile" Quiet pushd "$target" || Die "Could not enter $target" uncompress_rpm "$rpmfile" flatten_rpm "$rpmfile" populate_resources "$rpmfile" Quiet popd Is_URL "$entry" && rm -f -- "$rpmfile" 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."