#!/bin/bash # # Copyright (c) 2009-2010 Haiku, Inc. # Distributed under the terms of the MIT License. # # Authors: # Matt Madia, mattmadia@gmail.com # # Synopsis: # Provides a controlled mechanism for end-users to install certain pre-built # OptionalPackages. The script will determine the host information: the # default GCC, availability of secondary GCC libs, and revision. Using this # information, the user will be limited to the appropriate OptionalPackages # that were available for that specific revision. # DISCLAIMER="\ Disclaimer:\n\ This is a temporary solution for installing OptionalPackages.\n\ In time, there will be an official package manager.\n\ See these URL's for information on the in-development package manager.\n\ http://dev.haiku-os.org/wiki/PackageManagerIdeas\n\ http://dev.haiku-os.org/wiki/PackageFormat\n\ " USAGE="\ Usage: ./installoptionalpackage [ [ ...]]\n\ or ./installoptionalpackage [-a|-s [ ...]]\n\ or ./installoptionalpackage [-f|-h|-l]\n\ \n\ Options:\n\ -a Add one or more packages and all dependencies\n\ -s Show the final list of packages that would be installed\n\ -f Remove cached data and list installable packages\n\ -h Print this help.\n\ -l List installable packages\n\ " declare -A availablePackages declare availablePackagesKeys="" declare wantsToInstall="" declare alreadyInstalled="" # Some Packages cannot be installed, # as they require either the source code or compiled binaries declare packageIgnoreList="Bluetooth Development DevelopmentMin \ DevelopmentBase MandatoryPackages UserlandFS Welcome WifiFirmwareScriptData \ ICU-devel ICU " function CreateInstallerScript() { # This function will create a secondary script, containing all of the # information needed to install the optional package and its dependencies #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ cat << EOF > ${tmpDir}/install-optpkg.sh #!/bin/bash tmpDir=${tmpDir} HAIKU_GCC_VERSION[1]=${HAIKU_GCC_VERSION[1]} isHybridBuild=${isHybridBuild} TARGET_ARCH=${TARGET_ARCH} HAIKU_IMAGE_HOST_NAME=`uname -n` #TODO: possibly add a CLI option to execute InstallSourceArchive HAIKU_INCLUDE_SOURCES=0 $urlLine $sslPkgLine $sslUrlLine declare -a functionArgs expanderRulesFile=`finddir B_COMMON_DATA_DIRECTORY`/expander.rules if [ -f \${expanderRulesFile} ] ; then expanderRulesFileExists=1 fi function ParseFunctionArguments() { # ParseFunctionArguments # Parse arguments for Jam wrapper functions into an array. IN="\$@" OIFS=\$IFS IFS=":" local count=0 functionArgs=( ) for x in \$IN do functionArgs[\${count}]="\${x}" ((count++)) done IFS=\$OIFS } function TrimLeadingSpace() { # TrimLeadingSpace eval local text='\$'"\$1" local _outvar="\$1" local length=\${#text} ((length--)) if [ "\${text:0:1}" == ' ' ] ; then text=\${text#' '} fi eval \$_outvar="'\$text'" } function TrimEndingSpace() { # TrimEndingSpace eval local text='\$'"\$1" local _outvar="\$1" local length=\${#text} ((length--)) if [ "\${text:\$length}" == ' ' ] ; then text=\${text%' '} fi eval \$_outvar="'\$text'" } function Exit() { # Exit # Wrapper for Jam rule echo "\$@" exit 1 } function InstallOptionalHaikuImagePackage() { # InstallOptionalHaikuImagePackage package : url : dirTokens : isCDPackage # Wrapper for Jam rule echo "Installing \$1 ..." cd \$tmpDir archiveFile=\`echo \$3 | sed -s "s/http.*\///"\` if ! [ -f \$archiveFile ] ; then echo "Downloading \$3 ..." # TODO : add some error handling for downloads local attempt=1 while [ \`wget -nv \$3 ; echo \$? \` -ne 0 ]; do if [ \$attempt -eq 5 ]; then break fi (( attempt++ )) echo "Download attempt #\$attempt failed. Retrying ..." if [ -e \$archiveFile ]; then rm \$archiveFile fi done if [ \$attempt -ge 5 ]; then if [ -e \$archiveFile ]; then rm \$archiveFile fi Exit "Max download retries exceeded. Halting installation." fi fi local dirTokens='/boot' local count=4 local i=0 for possibleToken in "\$@" ; do if [ \$i -lt \$count ] ; then ((i++)) else ((i++)) if [ "\$possibleToken" != ':' ] ; then dirTokens=\${dirTokens}/\$possibleToken else break fi fi done echo "Extracting \$archiveFile ..." extractDir="\${dirTokens}" local errorMessage=" ...Failed while extracting \$archiveFile You may need to manually clean up the partially extracted files. " case "\$archiveFile" in *.zip) unzip -q -o -d "\$extractDir" "\$archiveFile" \ || Exit "\$errorMessage" ;; *.tgz|*.tar.gz) tar -C "\$extractDir" -xf "\$archiveFile" \ || Exit "\$errorMessage" ;; *) echo "Unhandled archive extension in InstallOptionalHaikuImagePackage()" exit 1 ;; esac if [ -f '/boot/.OptionalPackageDescription' ] ; then rm '/boot/.OptionalPackageDescription' fi rm "\$archiveFile" } function InstallSourceArchive() { if [ \$HAIKU_INCLUDE_SOURCES -gt 0 ]; then echo "InstallSourceArchive is not implemented." fi } function AddSymlinkToHaikuImage() { # AddSymlinkToHaikuImage : [ : ] # Wrapper for Jam rule ParseFunctionArguments "\$@" local dirTokens="/boot/\${functionArgs[0]}" TrimLeadingSpace dirTokens TrimEndingSpace dirTokens dirTokens=\${dirTokens//' '/\/} local linkTarget="\${functionArgs[1]}" TrimLeadingSpace linkTarget TrimEndingSpace linkTarget local linkName="\${functionArgs[2]}" TrimLeadingSpace linkName TrimEndingSpace linkName mkdir -p "\${dirTokens}" if [ "\${linkName}" == '' ] ; then ln -sf "\${linkTarget}" -t "\${dirTokens}" else ln -sf "\${linkTarget}" "\${dirTokens}/\${linkName}" fi } function AddUserToHaikuImage() { # AddUserToHaikuImage user : uid : gid : home : shell : realName # Wrapper for Jam rule ParseFunctionArguments "\$@" local user=\${functionArgs[0]} local uid=\${functionArgs[1]} local gid=\${functionArgs[2]} local home=\${functionArgs[3]} local shell=\${functionArgs[4]} local realName=\${functionArgs[5]} passwdLine="\${user}:x:\${uid}:\${gid}:\${realName}:\${home}:\${shell}" passwdLine=\${passwdLine//' :'/':'} passwdLine=\${passwdLine//': '/':'} local length=\${#passwdLine} ((length--)) if [ "\${passwdLine:\$length}" == ' ' ] ; then passwdLine=\${passwdLine%' '} fi passwdFile="\`finddir B_COMMON_ETC_DIRECTORY\`/passwd" touch \${passwdFile} local userExists=1 while read line ; do if [ "\${passwdLine}" == "\${line}" ] ; then userExists=0 fi done < \${passwdFile} if [ \$userExists -ge 1 ] ; then echo "\${passwdLine}" >> \${passwdFile} fi } function AddExpanderRuleToHaikuImage() { # AddExpanderRuleToHaikuImage : : : # Wrapper for Jam rule ParseFunctionArguments "\$@" local mimetype=\${functionArgs[0]} local extension=\${functionArgs[1]} local list=\${functionArgs[2]} local extract=\${functionArgs[3]} # clean up the variables TrimLeadingSpace mimetype TrimEndingSpace mimetype TrimLeadingSpace extension TrimEndingSpace extension TrimLeadingSpace list TrimEndingSpace list TrimLeadingSpace extract TrimEndingSpace extract local rule_raw="\${mimetype}\\t\${extension}\\t\${list}\\t\${extract}" # reset this at every invocation ruleFound= if [ \${expanderRulesFileExists} ] ; then # Check if a rule for the mimetype & extension exists. while read line ; do existing_rule=`echo \$line | awk '{ print \$1\$2 }'` if [ "\${mimetype}\${extension}" == "\${existing_rule}" ] ; then ruleFound=1 break fi done < "\${expanderRulesFile}" fi if ! [ \${expanderRulesFileExists} ] || ! [ \${ruleFound} ] ; then # Either expander.rules does not exist or a rule for mimetype & # extension does not exist. Output the new rule directly to it. echo -e \${rule_raw} >> \${expanderRulesFile} fi } EOF #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ cat ${tmpDir}/optpkg.stage2 >> ${tmpDir}/install-optpkg.sh rm ${tmpDir}/optpkg.stage2 } function ContainsSubstring() { # ContainsSubstring local string="$1" local substring="$2" local newString=${string/${substring}/''} if [ ${#string} -eq $((${#newString} + ${#substring})) ] ; then return 0 fi return 1 } function ErrorExit() { echo $1 exit 1 } function Init() { # Set up some directory paths baseDir=`finddir B_COMMON_DATA_DIRECTORY`/optional-packages tmpDir=`finddir B_COMMON_TEMP_DIRECTORY` libDir=`finddir B_SYSTEM_LIB_DIRECTORY` installedPackagesFile="${baseDir}/InstalledPackages" # Make sure these files are empty. echo "" > ${tmpDir}/optpkg.jam echo "" > ${tmpDir}/optpkg.stage1 if ! [ -d ${baseDir} ] ; then mkdir -p ${baseDir} fi DetectSystemConfiguration DownloadAllBuildFiles ReadInstalledPackagesIntoMemory ReadPackageNamesIntoMemory } function DownloadAllBuildFiles() { # DownloadAllBuildFiles # Retreive the necessary jam files from svn. local buildFiles="OptionalPackages OptionalPackageDependencies \ OptionalBuildFeatures OptionalLibPackages" for file in ${buildFiles} ; do GetBuildFile ${file} done } function GetBuildFile() { # GetBuildFile # Downloads files from Haiku's svn local buildfile="$1" if ! [ -f ${baseDir}/${buildfile} ] ; then echo "Fetching ${buildfile} ..." cd ${baseDir} local baseURL=http://cgit.haiku-os.org/haiku/plain local revisionTag=`uname -v | awk '{print $1}' | sed -e 's/-.*//'` # the sed invocation above drops potential dirty markers off the # revision tag local url="${baseURL}/build/jam/${buildfile}?id=${revisionTag}" wget -q ${url} -O ${buildfile} \ || ErrorExit "...failed to download $buildfile" fi } function DetectSystemConfiguration() { # Determine which GCC we're running if [ -f "$libDir"/libsupc++.so ] ; then HAIKU_GCC_VERSION[1]=4 else HAIKU_GCC_VERSION[1]=2 fi # Test for hybrid if [ -d "$libDir"/gcc4 -a -d "$libDir"/gcc2 ]; then echo "Sorry, but your build appears to be broken ..." echo "Both gcc2 and gcc4 subdirs exist." exit 1 elif [ -d "$libDir"/gcc4 -o -d "$libDir"/gcc2 ]; then isHybridBuild=1 else isHybridBuild="" fi # Determine the Architecture. if [ `uname -m` == "BePC" ] ; then TARGET_ARCH='x86' else echo "Sorry, x86 only for now." exit 1 fi } function ReadInstalledPackagesIntoMemory() { while read line ; do alreadyInstalled="${alreadyInstalled} $line" packageIgnoreList=${packageIgnoreList/"${line} "/' '} done < ${installedPackagesFile} } function ReadPackageNamesIntoMemory() { local file="${baseDir}/OptionalPackageNames" if ! [ -f ${file} ] ; then GeneratePackageNames fi # read list into associative array while read line ; do local pkg=`echo ${line} | awk '{print $1}'` local pkgDeps=${line/"${pkg} :"/} availablePackages[${pkg}]="${pkgDeps}" availablePackagesKeys="${availablePackagesKeys} ${pkg}" done < ${file} } function GeneratePackageNames() { # GeneratePackageNames # Creates a file containing available package names # Each line shows a pakage and all of its recrusive dependencies # " : ..." echo "Generating a list of Package Names ..." local file="${baseDir}/OptionalPackageNames" if [ -e "${file}" ]; then rm "${file}" fi local regExp='/^if\ \[\ IsOptionalHaikuImagePackageAdded/p' sed -n -e "$regExp" ${baseDir}/OptionalPackages > ${file}.temp sed -n -e "$regExp" ${baseDir}/OptionalLibPackages >> ${file}.temp while read line ; do # in each non-filtered line, the 4th word is the optional package local pkg=`echo ${line} | awk '{print $4}'` nonRepeatingDeps="" GetPackageDependencies "$pkg" local lowerCasePkg=`echo ${pkg} | tr '[A-Z]' '[a-z]'` if ! ContainsSubstring "${alreadyInstalled} " "${pkg} " ; then if IsPackageAndDepsOkToInstall ${pkg} ; then echo "${lowerCasePkg} : ${pkg} ${nonRepeatingDeps}" >> ${file} fi fi done < ${file}.temp rm ${file}.temp } function GetPackageDependencies() { # GetPackageDependencies # parse OptionalPackageDependencies for the single line that defines # this optional package's dependencies. local regExp="^OptionalPackageDependencies\ ${1}\ \:" local inputFile="${baseDir}/OptionalPackageDependencies" # print that single line sed -n -e "/${regExp}\ /p" ${inputFile} > ${tmpDir}/optpkg.temp # strip out "OptionalPackageDependencies PackageName :" # this leaves " .... ;" tempDeps=`sed -e "s/${regExp}\ //" ${tmpDir}/optpkg.temp` for foo in ${tempDeps%' ;'} ; do # Prevent duplicate entries of the same dependency package. if ! ContainsSubstring "${nonRepeatingDeps} " "${foo} " ; then nonRepeatingDeps="$foo $nonRepeatingDeps " nonRepeatingDeps="${nonRepeatingDeps// / }" fi done # Recursively get the dependencies of these dependencies. for dep in ${tempDeps%' ;'} ; do GetPackageDependencies "$dep" done } function IsPackageAndDepsOkToInstall() { # IsPackageAndDepsOkToInstall if ContainsSubstring "${packageIgnoreList}" "${1}"; then #echo "...warning: ${1} cannot be installed" return 1 fi for foo in ${nonRepeatingDeps} ; do if ContainsSubstring "${packageIgnoreList}" "${foo}"; then #echo "...warning: ${1} cannot be installed because of ${foo}" return 1 fi done return 0 } function BuildListOfRequestedPackages() { if [ "$1" = '-a' ] || [ "$1" = '-s' ]; then shift fi while [ $# -gt 0 ]; do local lowerCase=`echo $1 | tr '[A-Z]' '[a-z]'` wantsToInstall="${wantsToInstall} $lowerCase" shift done } function AddPackages() { # AddPackages # If one or more packages can be installed, do it. if BuildFinalListOfPackagesToInstall ; then for package in ${packagesToInstall} ; do # output the "if [ IsOptionalHaikuImagePackageAdded..." code block local regExp="if\ \[\ IsOptionalHaikuImagePackageAdded\ ${package}\ " for inputFile in OptionalPackages OptionalLibPackages ; do sed -n "/^$regExp/,/^\}/p" "${baseDir}/${inputFile}" >> ${tmpDir}/optpkg.jam done done ConvertJamToBash "${tmpDir}/optpkg.jam" rm "${tmpDir}/optpkg.jam" CreateInstallerScript sh ${tmpDir}/install-optpkg.sh exitcode=$? if [ $exitcode -gt 0 ]; then ErrorExit "... something went wrong when installing packages." fi rm ${tmpDir}/install-optpkg.sh # update files to account for the newly installed packages alreadyInstalled="${alreadyInstalled} ${packagesToInstall} " RecordInstalledPackages GeneratePackageNames echo "... done." fi } function BuildFinalListOfPackagesToInstall() { # BuildFinalListOfPackagesToInstall packagesToInstall="" proceedWithInstallation=false for desiredPackage in ${wantsToInstall}; do if IsPackageNameValid $desiredPackage ; then for item in ${availablePackages[${desiredPackage}]} ; do if ! ContainsSubstring "${packagesToInstall}" "${item}" ; then packagesToInstall="${packagesToInstall} ${item}" fi done proceedWithInstallation=true fi done # pad the variable packagesToInstall="${packagesToInstall} " # remove entries that are already installed for skip in ${alreadyInstalled}; do packagesToInstall=${packagesToInstall/"${skip} "/} done # strip double spaces packagesToInstall=${packagesToInstall/" "/" "} if ! [ ${#packagesToInstall} -gt 1 ]; then echo "... no packages need to be installed." echo "" echo "If you wish to re-install a package, run these two commands" echo " rm ${baseDir}/OptionalPackageNames" echo " open $installedPackagesFile" echo "and delete the line containing the package name(s)." echo "" proceedWithInstallation=false fi if ! $proceedWithInstallation ; then echo 'Not proceeding with installation.' return 1 fi echo "To be installed: ${packagesToInstall}" return 0 } function IsPackageNameValid() { # IsPackageNameValid for name in ${availablePackagesKeys} ; do if [ "$1" == "$name" ] ; then return 0 fi done return 1 } function RecordInstalledPackages() { echo -e ${alreadyInstalled} | tr '\ ' '\n' | sort > ${installedPackagesFile} } function ConvertJamToBash() { # ConvertJamToBash # The main Jam-to-Bash conversion function. local inputFile=$1 declare -a generatedBash countGenBashLine=0 # Parse out some variable declarations # TODO : add these following variables to the CreateInstallerScript # TODO : parse HAIKU_ICU_GCC_2_PACKAGE #local regExp='/^HAIKU_ICU_GCC_2_PACKAGE/p' #icuGcc2PkgLine=`sed -n -e "$regExp" ${baseDir}/OptionalBuildFeatures` #ConvertVariableDeclarationLines "$regExp" 'icuGcc2PkgLine' # TODO : parse HAIKU_ICU_GCC_4_PACKAGE #local regExp='/^HAIKU_ICU_GCC_4_PACKAGE/p' #icuGcc4PkgLine=`sed -n -e "$regExp" ${baseDir}/OptionalBuildFeatures` #ConvertVariableDeclarationLines "$regExp" 'icuGcc4PkgLine' # TODO : parse HAIKU_ICU_DEVEL_PACKAGE #local regExp='/^HAIKU_ICU_DEVEL_PACKAGE/p' #icuDevelPkgLine=`sed -n -e "$regExp" ${baseDir}/OptionalBuildFeatures` #ConvertVariableDeclarationLines "$regExp" 'icuDevelPkgLine' local regExp='/^HAIKU_OPENSSL_PACKAGE/p' sslPkgLine=`sed -n -e "$regExp" ${baseDir}/OptionalBuildFeatures` ConvertVariableDeclarationLines "$regExp" 'sslPkgLine' local regExp='/^HAIKU_OPENSSL_URL/p' sslUrlLine=`sed -n -e "$regExp" ${baseDir}/OptionalBuildFeatures` ConvertVariableDeclarationLines "$regExp" 'sslUrlLine' local regExp='/^local\ baseURL/p' urlLine=`sed -n -e "$regExp" ${baseDir}/OptionalPackages` urlLine=${urlLine/local\ /''} ConvertVariableDeclarationLines "$regExp" 'urlLine' # Convert the easy bits. while read line ; do line=${line/'Echo'/'echo'} # TODO: add support for converting for loops. # will need to introduce curly brace counting ConvertIfStatements "$line" ConvertVariables "$line" #ReplaceComparators "$line" line=${line/"IsOptionalHaikuImagePackageAdded"/'"SomeText" !='} generatedBash[$countGenBashLine]=${line} ((countGenBashLine++)) done < ${tmpDir}/optpkg.jam # output stage 1 generated code local i=0 while [ $i -lt $countGenBashLine ] ; do echo ${generatedBash[$i]} >> ${tmpDir}/optpkg.stage1 ((i++)) done # This converts multi-line jam statements into a single line. # --- Start awk --- awk ' /InstallOptionalHaikuImagePackage/,/\;/{ isRule=1; if($0~/\;/) ORS="\n"; else ORS=" "; print } /AddSymlinkToHaikuImage/,/\;/{ isRule=1; if($0~/\;/) ORS="\n"; else ORS=" "; print } /AddUserToHaikuImage/,/\;/{ isRule=1; if($0~/\;/) ORS="\n"; else ORS=" "; print } /AddExpanderRuleToHaikuImage/,/\;/{ isRule=1; if($0~/\;/) ORS="\n"; else ORS=" "; print } /Exit/,/\;/{ isRule=1; if($0~/\;/) ORS="\n"; else ORS=" "; print } { if($1!='InstallOptionalHaikuImagePackage' && isRule!=1 && $1!="\;") print $0 } { isRule=0; } ' ${tmpDir}/optpkg.stage1 > ${tmpDir}/optpkg.stage2 2>/dev/null # --- End awk --- rm ${tmpDir}/optpkg.stage1 } function ConvertVariableDeclarationLines() { # ConvertVariableDeclarationLines # One of the Jam-to-Bash conversion functions. # Jam lines that define variables need to be parsed differently. eval local input='$'"$2" local regex="$1" local _outvar="$2" input=${input/\ =\ /=} input=${input/\;/''} input=${input//\(/'{'} input=${input//\)/'}'} eval $_outvar="'$input'" } function ConvertIfStatements() { # ConvertIfStatements # One of the Jam-to-Bash conversion functions. line=${line//'} else {'/'else '} line=${line//'} else if '/'elif '} if ContainsSubstring "$line" "if " ; then if ! ContainsSubstring "$line" "if [" ; then line=${line/'if '/'if [ '} fi if ContainsSubstring "$line" '] {' ; then line=${line/'{'/' ; then'} elif ContainsSubstring "$line" '{' ; then line=${line/'{'/' ] ; then'} fi for compound in '&&' '||' ; do if ContainsSubstring "$line" "$compound" ; then line=${line/"$compound"/"] $compound ["} fi done ReplaceComparators "$line" fi # Assume all remaining closing braces are part of if statements line=${line/'}'/'fi'} } function ConvertVariables() { # ConvertVariables # One of the Jam-to-Bash conversion functions. # NOTE: jam's variables are normally '$(VARIABLE)'. \n # The issue is with '(' and ')', so let's replace them globally. if ContainsSubstring "$line" '$(' ; then line=${line//'('/'{'} line=${line//')'/'}'} fi } function ReplaceComparators() { # ReplaceComparators # One of the Jam-to-Bash conversion functions. # Preserve string comparators for TARGET_ARCH. if ! ContainsSubstring "$line" 'TARGET_ARCH' ; then line=${line//'>='/'-ge'} line=${line//'<='/'-le'} line=${line//'>'/'-gt'} line=${line//'<'/'-lt'} line=${line//'!='/'-ne'} line=${line//'='/'-eq'} fi } function DisplayUsage() { echo -e "$DISCLAIMER" echo -e "$USAGE" } function RemoveCachedFiles() { # RemoveCachedFiles echo "Removing cached files ..." if [ -e ${baseDir}/OptionalPackageNames ]; then rm ${baseDir}/OptionalPackageNames fi # Unset variables, which prevents duplicate entries. declare -A availablePackages declare availablePackagesKeys="" # Reinitialize Init } function ListPackages() { # ListPackages echo "" echo "Optional Packages that have been installed:" echo ${alreadyInstalled} echo "" echo "" echo "Installable Optional Packages:" # single line: echo ${availablePackagesKeys} # one per line: #for package in ${availablePackagesKeys} ; do # echo ${package} #done } # If no arguments were passed to the script, display its usage and exit. if [ "$#" -lt 1 ] ; then DisplayUsage exit 0 else Init fi # Support `installoptionalpackage ...` if [ "$1" != '-f' ] && [ "$1" != '-l' ] && [ "$1" != '-h' ] \ && [ "$1" != '-s' ]; then BuildListOfRequestedPackages $@ AddPackages exit 0 fi # Parse the arguments given to the script. while getopts "as:fhl" opt; do case $opt in a) BuildListOfRequestedPackages $@ AddPackages exit 0 ;; f) RemoveCachedFiles ListPackages exit 0 ;; h) DisplayUsage exit 0 ;; l) ListPackages exit 0 ;; s) BuildListOfRequestedPackages $@ BuildFinalListOfPackagesToInstall exit 0 ;; \?) echo "Invalid option: -$OPTARG" >&2 exit 1 ;; :) echo "Option -$OPTARG requires an argument." >&2 exit 1 ;; esac done