#!/bin/bash

# Version: $Id: bpkg 32 2005-12-17 13:35:41Z athomas $

#
# bpkg is a cross-platform automatic packaging utility. It is similar to
# CheckInstall (in fact it uses InstallWatch from CheckInstall) but goes
# further by attempting to automate the entire extract, configure, build and
# install process.
#
# Copyright (C) 2005  Alec Thomas
# 
# 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
#

# Default autoconf paths
PREFIX=/usr
SYSCONFDIR=/etc
LOCALSTATEDIR=/var/state
DATADIR=/usr/share
# Where to store built packages
PKGDIR=$PWD
# Skip build phase by default?
SKIPBUILD=0
# Program used to download source
DOWNLOADER='wget -c'
# Attempt to automatically download and extract source files given on the
# command line?
AUTOEXTRACT=0
# Create backups of each file about to be overwritten during installation
BACKUP=1
# Do not track files installed to these locations
IGNOREPATHS="/tmp /dev /root /usr/src"

# Load system config
test -r /etc/bpkg.conf && . /etc/bpkg.conf
# Load per-user config
test -r ~/.bpkgrc && . ~/.bpkgrc

#
# XXXXXXXXXXXXXX Not really user-modifiable below here XXXXXXXXXXXXX
#
VERSION="0.5"
PACKAGER=auto
SELF=`basename $0`
SRCDIR=$PWD
MAKEFILE=Makefile
PACKAGE=`basename "$PWD" | rev | cut -d- -f2- | rev | tr A-Z a-z`
PACKAGEVER=`basename "$PWD" | rev | cut -d- -f1 | rev | tr A-Z a-z`
PACKAGEREL=1
INSTALLLOG="/tmp/$SELF.$PACKAGE.$$.log"
TMPDIR=/tmp/$SELF.$PACKAGE.$$
DESTROOT=/tmp/$SELF.$PACKAGE.$$.pkg
TMPFILES="$TMPDIR $DESTROOT $INSTALLLOG"
BUILDER=auto
INSTALLER='make install'
CUSTOMBUILDER=''
EXPLICIT_PACKAGE=0
REQUIRED_PROGRAMS='installwatch cpio perl tar bzip2 unzip gzip'
MD5SUM='none'

colour()
{
	if tty > /dev/null 2>&1; then
		case $1 in
			black) echo -e "\[e30m" ;;
			red) echo -e "\e[31m" ;;
			green) echo -e "\e[32m" ;;
			brown) echo -e "\e[33m" ;;
			blue) echo -e "\e[34m" ;;
			magenta) echo -e "\e[35m" ;;
			cyan) echo -e "\e[36m" ;;
			white) echo -e "\e[37m" ;;
			bold) echo -e "\e[1m" ;;
			underline) echo -e "\e[4m" ;;
			reverse) echo -e "\e[7m" ;;
			*) return 1 ;;
		esac
	else
		case $1 in
			black|red|green|brown|blue|magenta|cyan|white|bold|underline|reverse) return 0 ;;
			*) return 1 ;;
		esac
	fi
	return 0
}

print() {
	while colour $1 > /dev/null 2>&1; do
		echo -n `colour $1`
		shift
	done

	echo
	echo "    bpkg -- $*"
	echo -e "\e[0m"
}

notice() {
	print bold green "notice -- $*"
}

warning() {
	print bold brown "warning -- $*" 1>&2
}

error() {
	print bold red "error -- $*" 1>&2
	exit 1
}

# duplicate_paths <destination>
#  Duplicate permissions for a list of files read from stdin and place them
#  at <destination>.
duplicate_paths() {
	while read file; do
		echo "$file" | perl -ne 'print "/\n"; $out = ""; chomp($_); for $v (split("/", $_)) { next if not $v; print "$out/$v\n"; $out = "$out/$v"; }'
	done | sort | uniq | cpio -pdm "$1" > /dev/null
}

# usage: installer
installer()
{
	umask 022
	unset INSTALLWATCH_BACKUP_PATH
	if [ $BACKUP = 1 ]; then
		export INSTALLWATCH_BACKUP_PATH=$SRCDIR/backup-`date +%Y%m%d%H%M%S-backup`
	fi
	installwatch -o $INSTALLLOG $INSTALLER || error "package installation failed"
	if [ $BACKUP = 1 -a -d $INSTALLWATCH_BACKUP_PATH ]; then
		notice "Archiving backup to $INSTALLWATCH_BACKUP_PATH.tar.gz"
		(cd $INSTALLWATCH_BACKUP_PATH && tar cfz $INSTALLWATCH_BACKUP_PATH.tar.gz . 2> /dev/null) || error "backup archiving failed"
		rm -rf $INSTALLWATCH_BACKUP_PATH
	fi
	unset INSTALLWATCH_BACKUP_PATH
	mkdir -p "$DESTROOT"
	# Move filtered file list into $DESTROOT
	cat $INSTALLLOG | awk '{print $3 "\n" $4}' | egrep -v "^(${IGNOREPATHS// /|}|$SRCDIR)" | sort | uniq | while read FILE; do
		if [ -e "$FILE" -a ! -d "$FILE" ]; then
			echo "$FILE"
		fi
	done | duplicate_paths "$DESTROOT"
	test `find $DESTROOT -type f | wc -l` = 0 && error "No files installed. This could mean the source is already installed."
}

# Builders
build_xmkmf()
{
	notice "running xmkmf on Imakefile"
	xmkmf || error "xmkmf failed"
}

build_autoconf()
{
	if ! grep -i autoconf ./configure > /dev/null; then
		notice "./configure does not seem to be generated by autoconf, running anyway"
	fi
	notice "running './configure --prefix=$PREFIX --sysconfdir=$SYSCONFDIR --localstatedir=$LOCALSTATEDIR --datadir=$DATADIR $@'"
	./configure --prefix=$PREFIX --sysconfdir=$SYSCONFDIR --localstatedir=$LOCALSTATEDIR --datadir=$DATADIR "$@" || error "configure failed"
	make
}

build_autogen()
{
	notice "no ./configure, but we have ./autogen.sh - running it"
	./autogen.sh --prefix=$PREFIX --sysconfdir=$SYSCONFDIR --localstatedir=$LOCALSTATEDIR --datadir=$DATADIR "$@" || error "./autogen.sh failed"
	if test ! -r Makefile; then
		notice "./autogen.sh did not run configure, running it now"
		build_autoconf
	fi
	make
}

build_make()
{
	notice "source doesn't appear to use autoconf or Imake, trying standard make"
	test -r $MAKEFILE || MF=makefile
	test -r $MAKEFILE || MF=GNUmakefile
	test -r $MAKEFILE || error "can't determine makefile (tried Makefile, makefile and GNUmakefile)"
	# Change /usr/local to $PREFIX
	if grep /usr/local $MAKEFILE > /dev/null; then
		notice "Changing all instances of /usr/local in $MAKEFILE to $PREFIX"
		sed -e "s,/usr/local,$PREFIX,g" $MAKEFILE > $MAKEFILE~ && mv $MAKEFILE~ $MAKEFILE
	fi
	make
}

build_jam()
{
	jam
}

build_python()
{
	python setup.py build -f
}

build_perl()
{
	perl Makefile.PL || error "Makefile generation failed"
	make
}

build_qmake()
{
	qmake $PACKAGE.pro || error "qmake failed"
	make
}

build_custom()
{
	eval "$CUSTOMBUILDER" || error "custom builder failed"
}

# Packagers
packager_slackware()
{
	installer
	cd $DESTROOT
	local out=$PKGDIR/$PACKAGE-$PACKAGEVER-i386-$PACKAGEREL.tgz
	makepkg -c n -l y $out
	installpkg $out
	notice "Package is" $out
}

packager_rpm()
{
	error "RPM packager not complete"
}

packager_deb()
{
	error "DEB packager not complete"
}

packager_pre_arch()
{
	pacman -Q "$PACKAGE" > /dev/null 2>&1 && error "package '$PACKAGE' already installed, remove it before packaging"
}

packager_arch()
{
	installer
	local configs=`(cd $DESTROOT && find etc -type f) 2> /dev/null`
	local deps=`(cd $DESTROOT && find . -type f | xargs --no-run-if-empty file | grep 'ELF.*executable' | cut -d: -f1 | xargs --no-run-if-empty ldd 2> /dev/null | awk '{print $3}' | grep ^/ | xargs --no-run-if-empty pacman -Qo 2> /dev/null | awk '{print $5}' | sort | uniq) 2> /dev/null`
	notice "detected configuration files: "$configs
	notice "detected dependencies: "$deps
	cd $TMPDIR
	cat <<-EOF > PKGBUILD
	pkgname=$PACKAGE
	pkgver=$PACKAGEVER
	pkgrel=$PACKAGEREL
	pkgdesc="Automatically generated by $SELF"
	url="http://swapoff.org/$SELF"
	depends=($deps)
	backup=($configs)
	source=()
	md5sums=()

	build() {
		cp -a $DESTROOT/* \$startdir/pkg/
	}
	EOF
	makepkg || error "makepkg failed"
	cp $TMPDIR/*.pkg.tar.gz $PKGDIR
	pacman -fA $PKGDIR/$PACKAGE-$PACKAGEVER*.pkg.tar.gz
	notice "Package is " $PKGDIR/$PACKAGE-$PACKAGEVER*.pkg.tar.gz
}

packager_pre_gentoo()
{
	eval `grep ^PORTDIR_OVERLAY /etc/make.conf`
	if [ -z "$PORTDIR_OVERLAY" ]; then
		error "You have not configured a portage overlay directory. Refer to http://gentoo-wiki.com/HOWTO_Installing_3rd_Party_Ebuilds for more information."
	fi
}

packager_gentoo()
{
	installer
	#local deps=`(cd $DESTROOT && find . -type f | xargs --no-run-if-empty file | grep 'ELF.*executable' | cut -d: -f1 | xargs --no-run-if-empty ldd 2> /dev/null | awk '{print $3}' | grep ^/ | xargs --no-run-if-empty pacman -Qo 2> /dev/null | awk '{print $5}' | sort | uniq) 2> /dev/null`
	ls -d /usr/portage/*/$PACKAGE > /dev/null 2>&1 && error "Package '$PACKAGE' already exists in portage."
	eval `grep ^PORTDIR_OVERLAY /etc/make.conf`
	if [ ! -d "$PORTDIR_OVERLAY" ]; then
		notice "Creating portage overlay directory: $PORTDIR_OVERLAY"
	fi
	mkdir -p $PORTDIR_OVERLAY/app-misc/$PACKAGE
	cd $PORTDIR_OVERLAY/app-misc/$PACKAGE
	local ebuild=$PACKAGE-$PACKAGEVER.ebuild
	notice "Generating ebuild: $ebuild"
	cat <<-EOF > $ebuild
	inherit eutils flag-o-matic
	DESCRIPTION="$PACKAGE (automatically packaged by $SELF)"
	HOMEPAGE="Unknown, refer to http://swapoff.org/bpkg for information on packager"
	SRC_URI=""
	LICENSE="UNKNOWN"
	SLOT="0"
	DEPENDS=""
	KEYWORDS="alpha amd64 arm hppa ia64 m68k mips ppc ppc64 ppc-macos s390 sh sparc x86 x86-obsd x86-fbsd"

	src_unpack() {
		:
	}

	src_compile() {
		:
	}

	src_install() {
		test -d $DESTROOT || die "You need to install $PACKAGE with $SELF. Can not be emerged."
		cp -a $DESTROOT/* \${D}/ || die "packaging failed"
	}
	EOF
	notice "Integrating package into world"
	ebuild $ebuild digest
	emerge $PACKAGE
}

packager_redhat() {
	packager_rpm "$@"
}

packager_suse() {
	packager_rpm "$@"
}

detect_os()
{
	if [ -r /etc/slackware-version ]; then
		echo slackware
	elif [ -r /etc/redhat-release ]; then
		echo redhat
	elif [ -r /etc/SuSE-release ]; then
		echo suse
	elif [ -r /etc/arch-release ]; then
		echo arch
	elif [ -r /etc/gentoo-release ]; then
		echo gentoo
	else
		error "could not determine packaging system to use"
	fi
}

# Special case for --detect-os
if [ "$1" == "--detect-os" ]; then
	(detect_os) 2> /dev/null || echo unknown
	exit 0
fi

# Look for programs we require
for x in $REQUIRED_PROGRAMS; do
	which $x > /dev/null 2>&1 || error "required program '$x' not found"
done

# Make sure some default directories exist
mkdir -p "$TMPDIR"
if [ ! -w $PKGDIR ]; then
	warning "$PKGDIR is not writeable or does not exist, package will be left in $PWD"
	PKGDIR=$PWD
fi

trap "for f in $TMPFILES; do rm -rf \$f; done" EXIT KILL HUP QUIT

quitloop=0
while [ $quitloop = 0 -a $# != 0 ]; do
	if [[ -f "$1" || "$1" = ftp://* || "$1" = http://* ]]; then
		AUTOEXTRACT="$1"
		if [[ "$AUTOEXTRACT" == *bz2 || "$AUTOEXTRACT" == *tbz ]]; then
			FLAGS=j
		elif [[ "$AUTOEXTRACT" == *gz ]]; then
			FLAGS=z
		elif [[ "$AUTOEXTRACT" == *Z ]]; then
			FLAGS=z
		elif [[ "$AUTOEXTRACT" == *zip || "$AUTOEXTRACT" == *egg ]]; then
			FLAGS=zip
		else
			error "unknown archive format for '$AUTOEXTRACT'"
		fi
		# Download package?
		if [[ "$AUTOEXTRACT" = ftp://* || "$AUTOEXTRACT" = http://* ]]; then
			notice "Downloading $AUTOEXTRACT"
			$DOWNLOADER "$AUTOEXTRACT" || error "downloader failed"
			AUTOEXTRACT=`basename "$AUTOEXTRACT"`
		fi
		if [ "$MD5SUM" != "none" ]; then
			verify_md5=`md5sum "$AUTOEXTRACT" | awk '{print $1}'`
			if [ $MD5SUM != $verify_md5 ]; then
				error "MD5 checksums do not match! $MD5SUM != $verify_md5"
			else
				notice "MD5 checksum OK"
			fi
		fi
		notice "uncompressing package '$AUTOEXTRACT'"
		if [ $FLAGS = zip ]; then
			unzip "$AUTOEXTRACT" || error "failed to auto-extract '$AUTOEXTRACT'"
			BUILDDIR=`unzip -lqq | awk '{print $NF}' | cut -d/ -f1 | head -1`
		else
			tar xf$FLAGS "$AUTOEXTRACT" || error "failed to auto-extract '$AUTOEXTRACT'"
			BUILDDIR=`tar tf$FLAGS "$AUTOEXTRACT" | cut -d/ -f1 | head -1`
		fi
		notice "package uncompressed, moving into build directory '$BUILDDIR'"
		cd "$BUILDDIR"
		SRCDIR=$PWD
		if [ $EXPLICIT_PACKAGE = 0 ]; then
			PACKAGE=`basename "$PWD" | rev | cut -d- -f2- | rev | tr A-Z a-z`
			PACKAGEVER=`basename "$PWD" | rev | cut -d- -f1 | rev | tr A-Z a-z`
			if [ "_$PACKAGE" = "_$PACKAGEVER" ]; then
				notice "Using source tarball for package name and version"
				PACKAGE=`basename "$AUTOEXTRACT" | sed -e 's/\(\.tar.*\|\.t[gb]z.*\|\.zip\|\.egg\)$//' | rev | cut -d- -f2- | rev | tr A-Z a-z`
				PACKAGEVER=`basename "$AUTOEXTRACT" | sed -e 's/\(\.tar.*\|\.t[gb]z.*\|\.zip\|\.egg\)$//' | rev | cut -d- -f1 | rev | tr A-Z a-z`
			fi
		fi
	else
		case $1 in
			--version)
				echo $VERSION
				exit 0
			;;
			--detect-os)
				(detect_os) 2> /dev/null || echo unknown
				exit 0
			;;
			--help)
			cat <<EOF
bpkg [<filename>] [<bpkg-options>] [<configure-options>]

<filename> will be automatically extracted before packaging continues. If not
given, packaging is performed from the current directory.

Any options not recognised by bpkg will be passed on to ./configure if the
package is autoconf based.

Available <bpkg-options> are:
  --help
    This help.
  --detect-os
    Display the O/S that bpkg thinks you are running.
  --version
    Show bpkg version.
  --packager=arch|slackware|rpm|deb|gentoo|redhat|suse|auto
    Generate packages using the specified packaging system. 'auto' is the
    default.
  --package=<name>-<version>
    Override package name and version auto-detection. By default this is
    obtained by extracting the package name and version from the package
    source directory name.
  --unique
    Pass options to autoconf to install into configuration and data directories
    unique to this package. (ie. --sysconfdir=/etc/<package>,
    --localstatedir=/var/state/<package> and --datadir=/usr/share/<package>).
    The base directories are used if this option is not given.
  --skip-build
    Do not perform the build phase of bpkg.
  --build-with=<command>
    Use the given command to build, rather than the default 'make'.
  --install-with=<command>
    Use the given command to install, rather than the default 'make install'.
  --md5=<md5sum>
    Verify that the source file has the given MD5 checksum.

For further information, including examples, visit http://swapoff.org/bpkg
EOF
				exit 0
			;;
			--install-with=*)
				INSTALLER=`echo "$1" | cut -d= -f2-`
			;;
			--build-with=*)
				CUSTOMBUILDER=`echo "$1" | cut -d= -f2-`
				BUILDER=custom
			;;
			--skip-build)
				SKIPBUILD=1
			;;
			--prefix=*)
				PREFIX=`echo $1 | cut -d= -f2-`
			;;
			--sysconfdir=*)
				SYSCONFDIR=`echo $1 | cut -d= -f2-`
			;;
			--localstatedir=*)
				LOCALSTATEDIR=`echo $1 | cut -d= -f2-`
			;;
			--datadir=*)
				DATADIR=`echo $1 | cut -d= -f2-`
			;;
			--unique)
				PREFIX=/usr
				SYSCONFDIR=/etc/$PACKAGE
				LOCALSTATEDIR=/var/state/$PACKAGE
				DATADIR=/usr/share/$PACKAGE
			;;
			--package=*)
				PACKAGE=`echo "$1" | cut -d= -f2- | rev | cut -d- -f2- | rev`
				PACKAGEVER=`echo "$1" | cut -d= -f2- | rev | cut -d- -f1 | rev`
				EXPLICIT_PACKAGE=1
			;;
			--packager=*)
				PACKAGER=`echo $1 | cut -d= -f2-`
			;;
			--md5=*)
				MD5SUM=`echo $1 | cut -d= -f2-`
			;;
			*)
				quitloop=1
				break
			;;
		esac
	fi
	shift
done

test $PACKAGER = auto && PACKAGER=`detect_os`

declare -F packager_pre_$PACKAGER > /dev/null && eval packager_pre_$PACKAGER
declare -F packager_$PACKAGER > /dev/null || error "no such function packager_$PACKAGER()"

if [ "$PACKAGE" = "$PACKAGEVER" ]; then
	# Last resort, try Python distutils version
	if [ -r setup.py ]; then
		PACKAGEVER=`python setup.py --version`
	fi
	if [ "_$PACKAGE" = "_$PACKAGEVER" ]; then
		error "Could not determine package version from current directory. Specify with --package=<package>-<version>"
	fi
fi

if [ $BUILDER = auto ]; then
	if [ -x ./configure ]; then
		BUILDER=autoconf
	elif [ -x autogen.sh ]; then
		BUILDER=autogen
	elif [ -r Imakefile ]; then
		BUILDER=xmkmf
	elif [ -r GNUmakefile -o -r makefile -o -r Makefile ]; then
		BUILDER=make
	elif [ -r Jamfile ]; then
		BUILDER=jam
		INSTALLER='jam install'
	elif [ -r setup.py ]; then
		BUILDER=python
		INSTALLER='python setup.py install -f'
	elif [ -r Makefile.PL ]; then
		BUILDER=perl
	elif [ -r $PACKAGE.pro ]; then
		BUILDER=qmake
	else
		error "couldn't auto-detect build mechanism"
	fi
fi

if [ $SKIPBUILD = 0 ]; then
	eval build_$BUILDER || error "build failed"
fi

eval packager_$PACKAGER

if [ "$AUTOEXTRACT" != 0 ]; then
	notice "extracted source left in $SRCDIR"
fi
