#!/bin/bash # Copyright (C) 2007 Lorenzo J. Lucchini # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # # First thing, we state that the directory where the sources reside is # the same as the argument the user passed to us. We will change this # later if needed, but since there is an "rm -r -f" that depends on # the contents of this variable, we better set it immediately. ## Human language strings are stored in variables to ease a possible future localization. LOC_SELECT_FILE="Select a file or directory" LOC_TYPE_FILE="Type the name of the archive or directory containing the source code" LOC_PRESS_ENTER_DEFAULT="Press Enter to use" LOC_PRESS_CTRL_D_TO_STOP="Press Ctrl-D when finished" LOC_TYPE_PASSWORD="This program is EXPERIMENTAL and DANGEROUS. Type your administrator password" LOC_INVALID_FILE="A file or directory name must be given" LOC_EXTRACTING="Extracting archive" LOC_EXTRACT_FAILED="Archive extraction failed" LOC_NO_BUILD_ENV="No known build environment found in source tree" LOC_UPDATING_AUTOAPT="Updating packages database" LOC_TRYING_CONFIGURE="Trying to configure" LOC_TRYING_MAKE="Trying to build" LOC_TRYING_INSTALL="Trying to install" LOC_CREATING_PACKAGE="Creating package" LOC_INSTALLING_DEPENDS="Installing runtime dependencies" LOC_APT_INSTALLING="APT is installing packages (do NOT interrupt)" LOC_APT_REMOVING="APT is removing packages (do NOT interrupt)" LOC_REMOVING_BUILD_DEPS="Removing build dependencies" LOC_SEARCHING_FILE="Searching for file" LOC_INSTALLING_PACKAGES="Installing package(s)" LOC_REMOVING_PACKAGES="Removing package(s)" LOC_FINISHED="Finished" LOC_RUNNING="Running" LOC_BUILD_COMPLETED="Finished building" LOC_BUILD_ABORTED="Build aborted. Incomplete build tree is in" LOC_UNEXPECTED_ABORT="Unexpected build system abort" LOC_FILE_NOT_FOUND="No such file or directory" LOC_CANNOT_READ="Cannot read" LOC_CONFIGURE_FAILED="Could not configure build system" LOC_BUILD_FAILED="Could not build program" LOC_INSTALL_FAILED="Could not install package" LOC_NOT_KNOWN_GUI="is not a known user interface" LOC_BAD_PASSWORD="Wrong password" LOC_REPORT_BUGS="Report bugs to " LOC_DESCRIPTION="Build the source tree contained in FILE (which can also be a directory)." LOC_USAGE="Usage" LOC_NOT_ROOT="You must be root to use this program" LOC_APT_FAILURE="APT reported a problem. Try 'apt-get -f install'" ## Terminate with an error message (given as parameter), and remove any dangling ## build dependencies. function Abort { if [[ -n "$BUILD_DEPENDS" ]]; then ## Avoid going into an infinite loop with RemovePackages Packages=$BUILD_DEPENDS BUILD_DEPENDS="" RemovePackages $Packages fi if [[ -n "$1" ]]; then ShowError "$1" else ShowError "$LOC_BUILD_ABORTED '$SOURCE_DIR'" fi exit 1 } ## Execute a command (passed in the parameters) as user $USERNAME rather than root. function UserMode { su -c "$*" $USERNAME } ## Request a file or directory name to the user, and return it in the $File variable, ## or return the empty string if the user does not provide a valid filename. function FileRequester { if [[ $INTERFACE == "zenity" ]]; then ## Unfortunately, Zenity doesn't allow chosing a file *or* a directory using ## the same dialog. Here we just create a file selector dialog. File=$(zenity 2>/dev/null --title "$LOC_SELECT_FILE" --file-selection) elif [[ $INTERFACE == "kdialog" ]]; then ## With KDialog, selecting a directory is not completely intuitive, but can ## be done with the same dialog that allows selecting a file: select the ## directory, and click 'Open' twice. File=$(kdialog 2>/dev/null --getopenfilename $(pwd)) else read -e -p "$LOC_TYPE_FILE : " File fi } ## Request a line of text from the user, and return it in the $String variable. The first ## argument is the prompt to the user, and the second argument, if given, is the ## default text. function StringRequester { if [[ $INTERFACE == "zenity" ]]; then String=$(zenity 2>/dev/null --title "Input" --text "$1" --entry-text "$2" --entry) elif [[ $INTERFACE == "kdialog" ]]; then String=$(kdialog 2>/dev/null --title "Input" --inputbox "$1" "$2") else read -p "$1 - $LOC_PRESS_ENTER_DEFAULT '$2' : " String if [[ -z "$String" ]]; then String="$2"; fi fi } ## Request multi-line text to the user, and return it in the $Text variable. The first ## argument is the prompt to the user, and the second argument, if given, is the ## default text. function TextRequester { if [[ $INTERFACE == "zenity" ]]; then ## Zenity currently doesn't support '--text' for multiline input boxes, it just ignores ## it. We put it there anyway, in the hope that it will supported in the future. Text=$(echo "$2" | zenity 2>/dev/null --title "Input" --text "$1" --text-info --editable) elif [[ $INTERFACE == "kdialog" ]]; then Text=$(kdialog 2>/dev/null --title "Input" --textinputbox "$1" "$2") else read -d $'\x4' -p "$1 - $LOC_PRESS_CTRL_D_TO_STOP :"$'\n' Text echo "" if [[ -z "$Text" ]]; then Text="$2"; fi fi } ## Show an error message (passed via the first parameter) to the user, and terminate. function ShowError { echo ERROR: "${1}." HideStatus HideNotification if [[ $INTERFACE == "zenity" ]]; then zenity 2>/dev/null --error --text "${1}." elif [[ $INTERFACE == "kdialog" ]]; then kdialog 2>/dev/null --error "${1}." fi } ## Show an empty status window with a progressbar starting at 0%. function ShowStatus { if [[ $INTERFACE == "zenity" ]]; then exec 8> >(zenity 2>/dev/null --width 550 --height 250 --progress --auto-kill --text '\n\n\n\n\n\n\n ') elif [[ $INTERFACE == "kdialog" ]]; then DCOP_REFERENCE=$(kdialog 2>/dev/null --geometry 550x250 --title "AutoDeb" --progressbar "") dcop $DCOP_REFERENCE showCancelButton true dcop $DCOP_REFERENCE setAutoClose true fi } ## Close a previously opened status window. function HideStatus { if [[ $INTERFACE == "zenity" ]]; then exec >&8- elif [[ $INTERFACE == "kdialog" ]]; then if [[ -n "$DCOP_REFERENCE" ]]; then dcop $DCOP_REFERENCE close; fi fi } ## With a status window open, update it as follows: ## - 1st parameter: description of the current state ## - 2nd parameter: a percentage, or none to leave percentage unchanged ## - 3rd parameter: a file descriptor to read sub-states from ## If the 3rd parameter is given, lines of text can be piped into the file ## descriptor to make the sub-state description change accordingly in real time. function UpdateStatus { echo >>"${LOG_FILE}" -e "\n$(date): $1\n" if [[ -n "$2" ]]; then echo "${1}... (${2}%)"; else echo "${1}..."; fi if [[ $INTERFACE == "zenity" ]]; then echo >&8 '# '"${1}..."'\n\n\n\n\n\n\n ' echo >&8 "$2" if [[ -n "$3" ]]; then ## Using sed to prepend text is far from foolproof. I don't know of a better way; meanwhile, we just ## hope that our program won't output pipe characters. tee <"$3" --append "${LOG_FILE}" | sed >&8 --unbuffered --regexp-extended \ 's|^[[:space:]]*(.{0,70})(.{0,70})(.{0,70})(.{0,70}).*|# '"${1}..."'\\n\\n\1\\n\2\\n\3\\n\4\\n\\n |g' fi elif [[ $INTERFACE == "kdialog" ]]; then dcop "$DCOP_REFERENCE" setLabel "$(echo -e "\n${1}...\n\n\n\n\n\n ")" if [[ -n "$2" ]]; then dcop "$DCOP_REFERENCE" setProgress "$2"; fi if [[ -n "$3" ]]; then (echo -e -n "\n" ; tee <"$3" --append "${LOG_FILE}" | sed --unbuffered --regexp-extended \ 's|^[[:space:]]*(.{0,70})(.{0,70})(.{0,70})(.{0,70}).*|'"${1}..."'\n\n\1\n\2\n\3\n\4\n \x0|g' ) | \ xargs -n 1 -0 --replace dcop "$DCOP_REFERENCE" setLabel "{}" fi else if [[ -n "$3" ]]; then tee <"${3}" --append "${LOG_FILE}" | cat; fi fi } ## Show a notification (try icon, balloon... depends on the interface), taking the ## text from the first parameter. function ShowNotification { echo "Warning: $1" if [[ $INTERFACE == "zenity" ]]; then exec 9> >(zenity 2>/dev/null --listen --text "$1" --notification) elif [[ $INTERFACE == "kdialog" ]]; then kdialog 2>/dev/null --passivepopup "$1" 5 & fi } ## Remove a previously shown notification. function HideNotification { if [[ $INTERFACE == "zenity" ]]; then exec 9>&- fi } ## Install the Debian packages given as parameters. function InstallPackages { ShowNotification "$LOC_APT_INSTALLING" apt-get -qq install $* | UpdateStatus "$LOC_INSTALLING_PACKAGES $*..." "" /dev/stdin if [[ $PIPESTATUS -ne 0 ]]; then Abort "$LOC_APT_FAILURE"; fi HideNotification } ## Remove the Debian packages given as parameters. function RemovePackages { ShowNotification "$LOC_APT_REMOVING" apt-get -qq remove $* | UpdateStatus "$LOC_REMOVING_PACKAGES $*" 90 /dev/stdin if [[ $PIPESTATUS -ne 0 ]]; then Abort "$LOC_APT_FAILURE"; fi HideNotification } ## Ask the user to fill in metadata for the package, based on a string (passed ## as parameter) in the form programname-version.number. function SetPackageInfo { PKG_NAME=$(echo "$1" | rev | cut -d "-" -f 2- | rev | sed 's/ /_/g') PKG_VERS=$(echo "$1" | rev | cut -d "-" -f 1 | rev | sed 's/ /_/g') StringRequester "Choose a name for the package to create" $PKG_NAME; PKG_NAME="$String" StringRequester "Type a a version number for the package" $PKG_VERS; PKG_VERS="$String" TextRequester "Type in a description of the package" "$PKG_NAME $PKG_VERS, generated by AutoDeb"; PKG_DESC="$Text" } ## Run the command that was passed as parameter which keeping track of any files ## it tries to access. If the command fails (non-zero exit status), install packages ## that may fix the problem, and try again. Keep doing this until out of ideas about ## what more to install. function MonitoredExecute { true; while [[ $? -eq 0 ]]; do TRACE_FILE=$(tempfile) UserMode strace -f -F -q -e trace=file,exit_group -e signal=none "$1" \ 2> >(grep 'ENOENT\|exit\|pkg\-config' >${TRACE_FILE}) | \ UpdateStatus "$LOC_RUNNING '${1}'" "" /dev/stdin ## Since strace discards the traced program's exit value, we need another way ## to know if the configure was successful; so we explicitely asked strace to ## store any exit_group call that was made, and now we read the argument that ## was passed to last one made. if ! tail -1 $TRACE_FILE | grep -q 'exit'; then Abort "$LOC_UNEXPECTED_ABORT" elif tail -1 $TRACE_FILE | grep -q '0'; then ## Configure was successful, leave the loop break fi ## If we reach this point, it means the build was unsuccessful, so we look at ## the strace output and install a package we "guess" as the culprit. ProcessTraceFile $TRACE_FILE done return $Status } ## Given a filename, passed as parameter, find a package containing it (if multiple packages ## contain the same file, heuristics are used and only one is chosen) and return it in the ## variable $Package. If no package is found, an empty string is returned. function FindPackageForFile { UpdateStatus "$LOC_SEARCHING_FILE '${1}'" ## auto-apt can take full pathames using the 'check' command, or regexps with 'search' if [[ "${1:0:1}" == "/" ]]; then Packages=$(auto-apt check "$1" | tr "," " ") else Packages=$(auto-apt search "${1}[^[:print:]]" | cut -f 2 | tr "\n" "," | tr "," " ") fi ## auto-apt check outputs '*' when it cannot find anything. if [[ "$Packages" == '*' ]]; then Package=""; return 1; fi ## If multiple packages provide the same file, choose the one with the shortest name. ## This helps making the right choice between 'package' and 'package-dbg', and the like. Package="" for Candidate in $Packages; do if [[ -z "$Package" || ${#Candidate} -lt ${#Package} ]]; then Package=$Candidate; fi done Package="$(basename "$Package")" if [[ -n "$Package" ]]; then return 0; else return 1; fi } ## Parse a file given as argument containing strace output for files not found during a build attempt, ## and install the Debian package judged most relevant to solve the problem. Return 0 if something was ## installed, 1 otherwise. function ProcessTraceFile { ## Read the strace output and find out which files could not be accessed. Sort them heuristically ## so as to have the most likely candidates for missing packages listed first. MainStatCalls=$(tac $1 | grep -v '\[' | grep stat | head -40 | grep --only-matching '"/[^" ]\+"') ForkStatCalls=$(tac $1 | grep '\[' | grep stat | head -40 | grep --only-matching '"/[^" ]\+"') MainMiscCalls=$(tac $1 | grep -v '\[' | grep -v stat | head -40 | grep --only-matching '"/[^" ]\+"') ForkMiscCalls=$(tac $1 | grep '\[' | grep -v stat | head -40 | grep --only-matching '"/[^" ]\+"') PkgConfigCalls=$(tac $1 | grep 'exec' | grep 'pkg-config' | grep --only-matching '"[^-/",][^/", ]\+[" ]' | sed 's/[" ]$/.pc"/g') FileList=$(echo "$PkgConfigCalls $MainStatCalls $ForkStatCalls $MainMiscCalls $ForkMiscCalls" | sed 's:"::g') for File in $FileList; do ## If the file is in the blacklist, skip it for ToSkip in $BLACKLIST; do if [[ "$File" == "$ToSkip" ]]; then continue; fi; done if echo "$File" | grep -q '^/tmp\|^/home\|^/usr/local'; then continue; fi FindPackageForFile $File if [[ -z $Package ]]; then ## No package owns that file, so we put the file into the blacklist in order to ## avoid wasting time searching for it again. BLACKLIST="$BLACKLIST $File" else ## Install the found package, add it to the list of install development packages ## and terminate successfully. InstallPackages $Package BUILD_DEPENDS="$BUILD_DEPENDS $Package" return 0 fi done return 1 } ## Give some help if requested. if [[ "$1" == "--help" ]]; then echo "$LOC_USAGE: $0 [--kde|--gnome] [FILE]" echo "$LOC_DESCRIPTION" echo "" echo "$LOC_REPORT_BUGS" exit 0 fi ## The preferred user interface can be specified in the command line, prefixed with '--'. if [[ "${1:0:2}" == "--" ]]; then INTERFACE="${1:2}" shift fi ## The following doesn't really do very much at the moment, except fall back to the console ## interface if whatever the user specified is not available. In the future, it might provide ## a more sophisticated fallback mechanism, in case more interface choices are added. while ! which "$INTERFACE" >/dev/null; do if [[ "$INTERFACE" == "" ]]; then break elif [[ "$INTERFACE" == "kde" ]]; then INTERFACE="kdialog" elif [[ "$INTERFACE" == "gnome" ]]; then INTERFACE="zenity" elif [[ "$INTERFACE" == "kdialog" ]]; then INTERFACE="" elif [[ "$INTERFACE" == "zenity" ]]; then INTERFACE="" else Abort "'$INTERFACE' $LOC_NOT_KNOWN_GUI" fi done ## Check that we are root. if [[ $UID -ne 0 ]]; then Abort "$LOC_NOT_ROOT" fi ## If no argument was passed, then ask the user where the source tarball, or directory, is located. if [[ -e "$1" ]]; then SOURCE_PTR="$1" else FileRequester; SOURCE_PTR="$File" fi if [[ ! -e "$SOURCE_PTR" ]]; then Abort "$LOC_FILE_NOT_FOUND '${SOURCE_PTR}'"; fi if [[ ! -r "$SOURCE_PTR" ]]; then Abort "$LOC_CANNOT_READ '${SOURCE_PTR}'"; fi ## We'll need to know which directory we started from later, and we also need ## a $LOG_FILE variable for UpdateStatus to work. WORK_DIR=$(pwd) LOG_FILE="$WORK_DIR/autodeb.log" ## If the user gave a directory, use it; if a file was given, attempt to ## extract it using tar into a temporary directory, and use that. USERNAME=$(stat -c "%U" "$SOURCE_PTR") if [[ -z "$SOURCE_PTR" ]]; then Abort "$LOC_INVALID_FILE" elif [[ -d "$SOURCE_PTR" ]]; then SOURCE_DIR="$SOURCE_PTR" SetPackageInfo $(cd "$SOURCE_DIR"; basename $(pwd)) ShowStatus else SOURCE_DIR=$(UserMode mktemp -d) SetPackageInfo $(basename $SOURCE_PTR | sed 's/\.tar$\|\.tar\.gz$\|\.tar\.bz2$\|\.tgz$\|\.zip$//') ShowStatus UpdateStatus "$LOC_EXTRACTING" 0 UserMode tar --extract --directory "$SOURCE_DIR" --file "$SOURCE_PTR" if [[ $? -ne 0 ]]; then Abort "$LOC_EXTRACT_FAILED" fi ## If the tar archive created one single sub-directory, use that. ## Otherwise, just hope the parent directory is the right one. if [[ $(ls ${SOURCE_DIR} | wc -l) -eq 1 ]]; then SOURCE_DIR=$(echo "${SOURCE_DIR}"/*) fi fi cd "$SOURCE_DIR" trap Abort 1 2 3 4 5 6 7 8 9 ## The "batch job" starts here, and we try to avoid having to stop to ask the user ## things from now on. ## 1) Install essential packages InstallPackages build-essential auto-apt checkinstall ## Create the auto-apt database if it doesn't yet exist if ! ( auto-apt list | head -1 >/dev/null ); then auto-apt update | UpdateStatus "$LOC_UPDATING_AUTOAPT" 5 /dev/stdin fi ## 1) Find out what build system the program uses if [[ -x "configure" ]]; then CONF_CMD="./configure" MAKE_CMD="make" INST_CMD="make install" else Abort "$LOC_NO_BUILD_ENV" fi ## 2) Configure build environment UpdateStatus "$LOC_TRYING_CONFIGURE" 10 MonitoredExecute $CONF_CMD if [[ $? -ne 0 ]]; then Abort "$LOC_CONFIGURE_FAILED"; fi ## 3) Build program UpdateStatus "$LOC_TRYING_MAKE" 20 MonitoredExecute $MAKE_CMD if [[ $? -ne 0 ]]; then Abort "$LOC_BUILD_FAILED"; fi ## 4) Install program PKG_DIR=$(UserMode mktemp -d) UserMode checkinstall --default --install=no --pakdir "$PKG_DIR" \ --pkgname "$PKG_NAME" --pkgversion "$PKG_VERS" $INST_CMD | \ UpdateStatus "$LOC_TRYING_INSTALL" 50 /dev/stdin if [[ $PIPESTATUS -ne 0 ]]; then Abort "$LOC_INSTALL_FAILED"; fi ## 5) Find runtime dependencies for File in $(UserMode dpkg --vextract "$PKG_DIR/"*.deb "$PKG_DIR"); do ## Right now, we only care about executable files. ## One could think about later implementing heuristics to extract ## meaningful dependencies from other filetypes, too... who knows. File="${PKG_DIR}/${File}" if ! ( file --mime "$File" | grep -q "x-executable" ); then continue; fi echo "$File" | UpdateStatus "Finding runtime dependencies..." 60 /dev/stdin RequiredLibraries="$RequiredLibraries $(ldd -u $File | awk '{ print $1 }' | tail -n +3)" done RequiredLibraries=$(echo "${RequiredLibraries}" | sort | uniq) for File in $RequiredLibraries; do FindPackageForFile "$(basename "$File")" if [[ $? -eq 0 ]]; then RUN_DEPENDS="$RUN_DEPENDS, $Package"; fi done ## Remove duplicates. More elegant ways to achieve that are welcome. RUN_DEPENDS="$(echo "${RUN_DEPENDS#, }" | tr " " "\n" | sort | uniq | tr "\n" " ")" ## 6) Create package echo "$PKG_DESCRIPTION" | \ checkinstall --default --install=no --pakdir "$PKG_DIR" --pkgname "$PKG_NAME" \ --pkgversion "$PKG_VERS" --requires "$RUN_DEPENDS" $INST_CMD | \ UpdateStatus "$LOC_CREATING_PACKAGE" 70 /dev/stdin ## 7) Install package and dependencies dpkg -i "$PKG_DIR/"*.deb 2>/dev/null | UpdateStatus "$LOC_INSTALLING_DEPENDS" 80 /dev/stdin apt-get -qq -f install | UpdateStatus "$LOC_INSTALLING_DEPENDS" 85 /dev/stdin if [[ $PIPESTATUS -ne 0 ]]; then Abort "$LOC_APT_FAILURE"; fi ## 8) Remove build dependencies if [[ -n "$BUILD_DEPENDS" ]]; then UpdateStatus "$LOC_REMOVING_BUILD_DEPS" 90 RemovePackages $BUILD_DEPENDS fi ## 9) Clean up UserMode cp "$PKG_DIR/"*.deb "$WORK_DIR"/ UpdateStatus $LOC_FINISHED 100 HideStatus ShowNotification "$LOC_BUILD_COMPLETED '${PKG_NAME}'"