#!/bin/bash # # scratchpkg # # Copyright (c) 2018 by Emmett1 (emmett1.2miligrams@gmail.com) # # 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 3 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, see . # msg() { echo -e "==> $1" } msg2() { echo -e " -> $1" } msgerr() { echo -e "==> ERROR: $1" } msgwarn() { echo -e "==> WARNING: $1" } updatemdsum() { if [ -z $source ]; then msgwarn "source=() is empty, no need md5sum." abort 0 fi for um in $(seq 0 $((${#source[@]} - 1))); do if [ $(echo ${source[$um]} | grep -E "(ftp|http|https)://") ]; then if [ $(echo ${source[$um]} | grep -E "::(ftp|http|https)://") ]; then sourcename="$SOURCE_DIR/$(echo ${source[$um]} | awk -F '::' '{print $1}')" else sourcename="$SOURCE_DIR/$(echo ${source[$um]} | rev | cut -d / -f 1 | rev)" fi else sourcename="${source[$um]}" fi needupdatechecksum="$needupdatechecksum $sourcename" done for file in ${needupdatechecksum[@]}; do if [ ! -f $file ]; then missingsource+=($file) fi done if [ "${#missingsource[@]}" -gt 0 ]; then msg "Missing source:" for ms in ${missingsource[@]}; do msg2 "$ms" done abort 1 fi echo -e "md5sum=($(md5sum $needupdatechecksum | awk '{ print $1 }'))" } checkmdsum() { if [ ! -z "$source" -a -z "$md5sum" ]; then msgerr "md5sum=() is empty, please provide it." abort 1 fi if [ "${#source[@]}" != "${#md5sum[@]}" ]; then msgerr "Total source and md5sums different." abort 1 fi for s in $(seq 0 $((${#source[@]} - 1))); do if [ $(echo ${source[$s]} | grep -E "(ftp|http|https)://") ]; then if [ $(echo ${source[$s]} | grep -E "::(ftp|http|https)://") ]; then sourcename=$SOURCE_DIR/$(echo ${source[$s]} | awk -F '::' '{print $1}') else sourcename=$SOURCE_DIR/$(echo ${source[$s]} | rev | cut -d / -f 1 | rev) fi else sourcename="${source[$s]}" fi sum=$(md5sum "$sourcename" | awk '{ print $1 }') if [ "$sum" != "${md5sum[$s]}" ] && [ "SKIP" != "${md5sum[$s]}" ]; then errormdsum+=($sourcename) fi done if [ "${#errormdsum[@]}" -gt 0 ]; then msgerr "md5sum mismatch:" for mismatch in ${errormdsum[@]}; do msg2 "$mismatch" done abort 1 fi } download_src() { local FILE FILENAME for FILE in ${source[@]}; do if [[ $FILE =~ ::(http|https|ftp|file):// ]]; then FILENAME=$(echo $FILE | awk -F '::' '{print $1}') SRCURL=$(echo $FILE | awk -F '::' '{print $2}') else FILENAME=$(basename $FILE) SRCURL=$FILE fi case $DOWNLOAD_PROG in curl) DLCMD="curl -C - --fail --ftp-pasv --retry 3 --retry-delay 3 -o $SOURCE_DIR/$FILENAME.partial $CURL_OPTS" ;; wget) DLCMD="wget -c --passive-ftp --no-directories --tries=3 --waitretry=3 --output-document=$SOURCE_DIR/$FILENAME.partial $WGET_OPTS" ;; esac if [ "$FILENAME" != "$FILE" ]; then if [ -f "$SOURCE_DIR/$FILENAME" ] && [ -z "$REDOWNLOAD_SOURCE" ]; then msg "Source '$FILENAME' found." else [ "$REDOWNLOAD_SOURCE" ] && rm -f "$SOURCE_DIR/$FILENAME.partial" if [ -f "$SOURCE_DIR/$FILENAME.partial" ]; then msg "Resuming '$SRCURL'." else msg "Downloading '$SRCURL'." fi $DLCMD $SRCURL if [ $? = 0 ]; then [ "$REDOWNLOAD_SOURCE" ] && rm -f "$SOURCE_DIR/$FILENAME" mv $SOURCE_DIR/$FILENAME.partial $SOURCE_DIR/$FILENAME else msgerr "Failed downloading '$FILENAME'." abort 1 fi fi else if [ ! -f "$FILENAME" ]; then msgerr "Source '$FILENAME' not found." abort 1 else msg "Source '$FILENAME' found." fi fi done } prepare_src() { local FILE FILENAME SRC_DIR [ "$IGNORE_MDSUM" ] || checkmdsum SRC=$WORK_DIR/$name/src PKG=$WORK_DIR/$name/pkg rm -fr $WORK_DIR/$name mkdir -p $SRC $PKG if [ "${#source[@]}" -gt 0 ]; then for FILE in ${source[@]}; do if [[ $FILE =~ ::(http|https|ftp|file):// ]]; then FILENAME=$(echo $FILE | awk -F '::' '{print $1}') SRC_DIR="$SOURCE_DIR" elif [[ $FILE =~ ^(http|https|ftp|file):// ]]; then FILENAME=$(basename $FILE) SRC_DIR="$SOURCE_DIR" else FILENAME=$(basename $FILE) SRC_DIR="$PWD" fi for NOEXT in ${noextract[@]}; do if [ "$NOEXT" = "$FILENAME" ]; then nxt=1 break fi done if [ "$FILENAME" != "$FILE" ] && [ "$nxt" != 1 ]; then case $FILENAME in *.tar|*.tar.gz|*.tar.Z|*.tgz|*.tar.bz2|*.tbz2|*.tar.xz|*.txz|*.tar.lzma|*.zip|*.rpm) if [ $(type -p bsdtar) ]; then COMMAND="bsdtar -p -o -C $SRC -xf $SOURCE_DIR/$FILENAME" else COMMAND="tar -p -o -C $SRC -xf $SOURCE_DIR/$FILENAME" fi MODE="Unpacking" ;; *) COMMAND="cp $SOURCE_DIR/$FILENAME $SRC" MODE="Preparing" ;; esac msg2 "$MODE '$FILENAME'..." $COMMAND if [ $? != 0 ]; then msgerr "$MODE '$FILENAME' failed." abort 1 fi else msg2 "Preparing '$FILENAME'..." cp "$SRC_DIR/$FILENAME" "$SRC" fi nxt= done fi } run_build() { if [ "$UID" != 0 ]; then msgerr "You must build package as root, or use fakeroot." abort 1 fi msg "Start build '$name-$version-$release'." [ "$MAKE_FLAGS" = 1 ] && export MAKEFLAGS || unset MAKEFLAGS [ "$BUILD_FLAGS" = 1 ] && export CFLAGS CXXFLAGS || unset CFLAGS CXXFLAGS pushd $SRC >/dev/null if [ "$LOGGING" = yes ]; then ( set -e -x; build 2>&1 | tee $LOG_DIR/$name-$version-$release.log exit $PIPESTATUS ) else (set -e -x; build) fi if [ $? != 0 ]; then msgerr "Build '$name-$version-$release' failed." abort 1 else msg "Build '$name-$version-$release' success." fi popd >/dev/null } purgefiles() { local OPTIONS [ "${#PURGE_FILES[@]}" -gt 0 ] || return 0 for OPTIONS in ${PURGE_FILES[@]}; do if [ -e $OPTIONS ]; then rm -fr $OPTIONS fi done } removeemptydirs() { find . -type d -empty -delete } removelibtool() { find . ! -type d -name "*.la" -delete } strip_files() { local xstrip local FILTER if [ "$nostrip" ]; then for i in ${nostrip[@]}; do xstrip="$xstrip -e $i" done FILTER="grep -v $xstrip" else FILTER="cat" fi find . -type f -printf "%P\n" 2>/dev/null | $FILTER | while read -r binary ; do case "$(file -bi "$binary")" in *application/x-sharedlib*) # Libraries (.so) strip --strip-unneeded "$binary" 2>/dev/null ;; *application/x-pie-executable*) # Libraries (.so) strip --strip-unneeded "$binary" 2>/dev/null ;; *application/x-archive*) # Libraries (.a) strip --strip-debug "$binary" 2>/dev/null ;; *application/x-object*) case "$binary" in *.ko) # Kernel module strip --strip-unneeded "$binary" 2>/dev/null ;; *) continue;; esac;; *application/x-executable*) # Binaries strip --strip-all "$binary" 2>/dev/null ;; *) continue ;; esac done } compressinfomanpages() { find . -type f -path "*/man/man*/*" | while read file; do if [ "$file" = "${file%%.gz}" ]; then gzip -9 -f "$file" fi done find . -type l -path "*/man/man*/*" | while read file; do FILE="${file%%.gz}.gz" TARGET="$(readlink $file)" TARGET="${TARGET##*/}" TARGET="${TARGET%%.gz}.gz" DIR=$(dirname "$FILE") rm -f $file if [ -e "$DIR/$TARGET" ]; then ln -sf $TARGET $FILE fi done if [ -d usr/share/info ]; then (cd usr/share/info for file in $(find . -type f); do if [ "$file" = "${file%%.gz}" ]; then gzip -9 "$file" fi done ) fi } backupconf() { local FILE for FILE in ${backup[@]}; do if [ ! -f $FILE ]; then msgerr "File '$FILE' not exist!" abort 1 else mv $FILE $FILE.spkgnew fi done } packaging() { local FILE f for f in ${INCLUDEINPKG[@]}; do [ -f $f ] && install -m644 $f $PKG/.pkg$f done #[ -f install ] && install -m644 install $PKG/.pkginstall #[ -f readme ] && install -m644 readme $PKG/.pkgreadme #[ -f mkdirs ] && install -m644 mkdirs $PKG/.pkgmkdirs pushd $PKG >/dev/null rm -f usr/{,share/}info/dir [ "$KEEP_EMPTYDIR" = 0 ] && removeemptydirs [ "$KEEP_LIBTOOL" = 0 ] && removelibtool [ "$STRIP_BINARY" = 1 ] && strip_files [ "$ZIP_MAN" = 1 ] && compressinfomanpages if [ "${#backup[@]}" -gt 0 ]; then backupconf fi for FILE in ${INCLUDEINPKG[@]}; do if [ -f .pkg$FILE ]; then addtotar+=(.pkg$FILE) fi done [ "$FORCE_REBUILD" ] && rm -f "$PACKAGE_DIR/$PKGNAME" case $COMPRESSION_MODE in xz) COMPRESS="-J" ;; gz) COMPRESS="-z" ;; bz2) COMPRESS="-j" ;; esac tar -c $COMPRESS -f $PACKAGE_DIR/$PKGNAME * "${addtotar[@]}" if [ $? != 0 ]; then rm -f $PACKAGE_DIR/$PKGNAME msgerr "Packaging '$PKGNAME' failed." abort 1 fi tar -tvf $PACKAGE_DIR/$PKGNAME pkgsize="$(ls -lh $PACKAGE_DIR/$PKGNAME | awk '{print $5}')" msg "Successfully created package '$PKGNAME'. (${pkgsize})" popd >/dev/null } check_buildscript() { # check the required field in buildscript if [ -z "$name" ]; then msgerr "'name' is empty!" exit 1 elif [ "$(basename `pwd`)" != "$name" ]; then msgerr "Port name and Directory name is different!" exit 1 elif [ -z "$version" ]; then msgerr "'version' is empty!" exit 1 elif [ -z "$release" ]; then msgerr "'release' is empty!" exit 1 elif [ "`type -t build`" != "function" ]; then msgerr "'build' function not exist!" exit 1 elif $(echo "$version" | grep -q '-'); then msgerr "'version' should not contain '-'." exit 1 elif $(echo "$release" | grep -q '-'); then msgerr "'release' should not contain '-'." exit 1 elif [ -z "$description" ]; then msgerr "'description' cant empty." exit 1 fi } set_options() { local OPT for OPT in ${OPTIONS[@]} ${options[@]}; do case $OPT in libtool) KEEP_LIBTOOL=1 ;; !libtool) KEEP_LIBTOOL=0 ;; emptydirs) KEEP_EMPTYDIR=1;; !emptydirs) KEEP_EMPTYDIR=0;; strip) STRIP_BINARY=1 ;; !strip) STRIP_BINARY=0 ;; zipman) ZIP_MAN=1 ;; !zipman) ZIP_MAN=0 ;; buildflags) BUILD_FLAGS=1 ;; !buildflags) BUILD_FLAGS=0 ;; makeflags) MAKE_FLAGS=1 ;; !makeflags) MAKE_FLAGS=0 ;; esac done } checkdir() { local DIR for DIR in "$@"; do if [ ! -d $DIR ]; then msgerr "Directory '$DIR' not exist." abort 1 elif [ ! -w $dir ]; then msgerr "Directory '$DIR' not writable." abort 1 elif [ ! -x $dir ] || [ ! -r $1 ]; then msgerr "Directory '$DIR' not readable." abort 1 fi done } clearworkdir() { if [ ! "$KEEP_WORK" ]; then rm -fr $WORK_DIR/$name fi } interrupted() { echo abort 100 } abort() { rm -f $LOCK_FILE clearworkdir exit $1 } extract_opt() { for opt in $@; do case $opt in --*) BOPTS+=($opt) ;; -*) for (( i=1; i<${#opt}; i++ )); do BOPTS+=(-${opt:$i:1}); done ;; *) BOPTS+=($opt) ;; esac done echo ${BOPTS[@]} } parse_opts() { while [ "$1" ]; do case $1 in -i | --install) INSTALL_PKG=yes ;; -u | --upgrade) UPGRADE_PKG=yes; OPTS+=($1) ;; -r | --reinstall) REINSTALL_PKG=yes; OPTS+=($1) ;; -c | --ignore-conflict) OPTS+=($1) ;; -v | --verbose) VERBOSE=yes; OPTS+=($1) ;; -f | --force-rebuild) FORCE_REBUILD=yes ;; -m | --skip-mdsum) IGNORE_MDSUM=yes ;; -g | --genmdsum) UPDATE_MDSUM=yes ;; -o | --download) DOWNLOAD_ONLY=yes ;; -x | --extract) EXTRACT_ONLY=yes ;; -w | --keep-work) KEEP_WORK=yes ;; -l | --log) LOGGING=yes ;; -h | --help) SHOWHELP=yes ;; --no-preinstall) NO_PREINSTALL=yes; OPTS+=($1) ;; --no-postinstall) OPTS+=($1) ;; --no-preupgrade) OPTS+=($1) ;; --no-postupgrade) OPTS+=($1) ;; --no-backup) OPTS+=($1) ;; --redownload) REDOWNLOAD_SOURCE=yes ;; --config=*) PKGBUILD_CONF="${1#*=}" ;; --srcdir=*) CUSTOM_SOURCE_DIR="${1#*=}" ;; --pkgdir=*) CUSTOM_PACKAGE_DIR="${1#*=}" ;; --logdir=*) CUSTOM_LOG_DIR="${1#*=}" ;; *) msg "Invalid $(basename $0) option! ($1)"; exit 1 ;; esac shift done } help() { cat << EOF Usage: $(basename $0) [ ] Options: -i, --install install package into system -u, --upgrade upgrade package -r, --reinstall reinstall package -c, --ignore-conflict ignore conflict when installing package -v, --verbose verbose install process -f, --force-rebuild rebuild package -m, --skip-mdsum skip md5sum checking -g, --genmdsum generate md5sum -o, --download download only source file -x, --extract extract only source file -w, --keep-work keep working directory -l, --log log build process -h, --help show this help message --config= use custom config file --srcdir= override directory path for sources --pkgdir= override directory path for compiled package --logdir= override directory path for build logs --no-preinstall skip preinstall script before install package --no-postinstall skip postinstall script after install package --no-preupgrade skip preupgrade script before upgrade package --no-postupgrade skip postupgrade script after upgrade package --no-backup skip backup configuration file when upgrading package --redownload re-download source file EOF } main() { parse_opts $(extract_opt $@) if [ -f $PKGBUILD_CONF ]; then source $PKGBUILD_CONF else msgerr "Config file not found ('$PKGBUILD_CONF')" exit 1 fi if [ "$CUSTOM_SOURCE_DIR" ]; then SOURCE_DIR="$CUSTOM_SOURCE_DIR" fi if [ "$CUSTOM_PACKAGE_DIR" ]; then PACKAGE_DIR="$CUSTOM_PACKAGE_DIR" fi if [ "$CUSTOM_LOG_DIR" ]; then LOG_DIR="$CUSTOM_LOG_DIR" fi if [ -z "$SOURCE_DIR" ]; then msgerr "Option '--srcdir=' need argument (path)" exit 1 elif [ -z "$PACKAGE_DIR" ]; then msgerr "Option '--pkgdir=' need argument (path)" exit 1 fi checkdir "$SOURCE_DIR" "$PACKAGE_DIR" "$WORK_DIR" "$LOG_DIR" # show usage if [ "$SHOWHELP" ]; then help exit 0 fi if [ -f $PKGBUILD_BSCRIPT ]; then description=$(grep "^# description[[:blank:]]*:" $PKGBUILD_BSCRIPT | sed 's/^# description[[:blank:]]*:[[:blank:]]*//') backup=$(grep "^# backup[[:blank:]]*:" $PKGBUILD_BSCRIPT | sed 's/^# backup[[:blank:]]*:[[:blank:]]*//') depends=$(grep "^# depends[[:blank:]]*:" $PKGBUILD_BSCRIPT | sed 's/^# depends[[:blank:]]*:[[:blank:]]*//') noextract=$(grep "^# noextract[[:blank:]]*:" $PKGBUILD_BSCRIPT | sed 's/^# noextract[[:blank:]]*:[[:blank:]]*//') source $PKGBUILD_BSCRIPT else msgerr "'$PKGBUILD_BSCRIPT' file not found." exit 1 fi if [ "$LOGGING" = yes ] && [ -f $LOG_DIR/$name-$version-$release.log ] && [ ! -w $LOG_DIR/$name-$version-$release.log ]; then msgerr "You dont have write permission for '$LOG_DIR/$name-$version-$release.log'." abort 1 fi check_buildscript set_options case $COMPRESSION_MODE in gz|bz2|xz) PKGNAME="$name-$version-$release.spkg.tar.$COMPRESSION_MODE" ;; *) msgerr "Invalid compression mode. ($COMPRESSION_MODE)" exit 1 ;; esac LOCK_FILE="/tmp/pkgbuild.$name.lock" # check for lock file if [ -f "$LOCK_FILE" ]; then msgerr "Cant build same package simultaneously." msgerr "remove '$LOCK_FILE' if no build process for '$name'." exit 1 else touch "$LOCK_FILE" if [ "$?" != 0 ]; then msgerr "Cant create lock file in '$LOCK_FILE'" exit 1 fi fi # download source only if [ "$DOWNLOAD_ONLY" ]; then download_src abort 0 fi # extract source only if [ "$EXTRACT_ONLY" ]; then download_src prepare_src KEEP_WORK=yes abort 0 fi # calculate & print md5sum if [ "$UPDATE_MDSUM" ]; then updatemdsum abort 0 fi # build package if [ -f "$PACKAGE_DIR/$PKGNAME" ] && [ ! "$FORCE_REBUILD" ]; then if [ ! "$INSTALL_PKG" ] && [ ! "$REINSTALL_PKG" ] && [ ! "$UPGRADE_PKG" ]; then echo "Package '$PKGNAME' is up-to-date." abort 0 fi else download_src prepare_src run_build packaging clearworkdir fi # install package if [ "$INSTALL_PKG" ] || [ "$REINSTALL_PKG" ] || [ "$UPGRADE_PKG" ]; then pkgadd $PACKAGE_DIR/$PKGNAME $INST_OPT ${OPTS[@]} || abort 1 fi abort 0 } trap "interrupted" SIGHUP SIGINT SIGQUIT SIGTERM export LC_ALL=C PKGBUILD_CONF="/etc/scratchpkg.conf" PKGBUILD_BSCRIPT="spkgbuild" SOURCE_DIR="/var/cache/scratchpkg/sources" PACKAGE_DIR="/var/cache/scratchpkg/packages" LOG_DIR="/var/cache/scratchpkg/log" WORK_DIR="/tmp" DOWNLOAD_PROG="wget" COMPRESSION_MODE="xz" OPTIONS=(!libtool emptydirs strip zipman buildflags makeflags) INCLUDEINPKG=(install readme mkdirs) main "$@"