All pastes #736123 Raw Edit

Someone

public text v1 · immutable
·published 2007-10-14 06:11 UTC
rendered paste body
#!/bin/bash

# Copyright (C) 2007 Lorenzo J. Lucchini <ljlbox@tiscali.it>
#
# 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 <ljlbox@tiscali.it>"
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}'"