#!/bin/sh
#
# pkgbuild - Package build utility
# POSIX-compliant shell script for build packages
#

msg() { printf "==> %s\n" "$1"; }
msgerr() { printf "ERROR: %s\n" "$1" >&2; }

get_filepath() {
	for i in $source; do
		case $i in
			 *::*) printf '%s\n' "$SOURCE_DIR/${i%::*}";;
			*://*) printf '%s\n' "$SOURCE_DIR/${i##*/}";;
			    *) printf '%s\n' "$PWD/$i";;
		esac
	done
}

pkg_genchecksums() {
	for i in $(get_filepath); do
		[ -f "$i" ] || {
			msgerr "File missing: $i"
			abort 1
		}
	done
	generatechecksum > .checksums
	msg ".checksums updated"
}

generatechecksum() {
	for s in $(get_filepath); do
		[ -f "$s" ] || continue
		b3sum "$s" | sed -e 's|  .*/|  |'
	done | sort -k 2

}

pkg_checksum() {
	if [ ! -f .checksums ]; then
		pkg_genchecksums
	else
		case "$IGNORE_CHECKSUM" in 
			yes|1) ;; 
			*)
			TMPCHECKSUM="$WORK_DIR/checksumstmp.$$"
			DIFCHECKSUM="$WORK_DIR/checksumsdiff.$$"
			mismatch=0

			msg "Checking checksums..."
			generatechecksum > "$TMPCHECKSUM"

			diff -U 0 .checksums "$TMPCHECKSUM" | \
			sed -e '/---/d;/+++/d;/@@/d' \
			    -e 's/^+/ new    : /' \
			    -e 's/^-/ missing: /' > "$DIFCHECKSUM"

			if [ -s "$DIFCHECKSUM" ]; then
				cat "$DIFCHECKSUM"
				msg "Checksum failed."
				mismatch=1 
			fi

			rm -f "$TMPCHECKSUM" "$DIFCHECKSUM"
			[ "$mismatch" -eq 1 ] && abort 1
			;;
		esac
	fi
}

pkg_fetch() {
	for i in $source; do
		COPT="" 
		
		case $i in
			*::*)  filepath="$SOURCE_DIR/${i%::*}"; url="${i#*::}";;
			*://*) filepath="$SOURCE_DIR/${i##*/}"; url="$i";;
			*) continue;;
		esac
		
		[ -f "$filepath" ] && {
			msg "Source found: $filepath"
			continue
		}
		
		# check partial file
		if [ -f "$filepath.partial" ]; then
			if [ -s "$filepath.partial" ] && [ -r "$filepath.partial" ]; then
				COPT="-C -"
				msg "Continuing download: $i"
			else
				msg "Removing corrupted partial file"
				rm -f "$filepath.partial"
			fi
		fi
		
		msg "Fetching: $i"
		curl $COPT -L --fail --retry 3 --retry-delay 3 -o "$filepath.partial" $CURL_OPTS "$url" || {
			msgerr "Fetching failed: $i"
			rm -f "$filepath.partial"
			abort 1
		}
		mv "$filepath.partial" "$filepath"
	done
}

pkg_unpack() {
	SRC="$WORK_DIR/$name/src"
	PKG="$WORK_DIR/$name/pkg"
	
	umask 022
	
	rm -fr "$WORK_DIR/$name"
	
	mkdir -p "$SRC" "$PKG"
	
	for i in $(get_filepath); do
		if [ ! -f "$i" ]; then
			msgerr "Source missing: $i"
			abort 1
		fi
		for n in $noextract; do
			if [ "${i##*/}" = "$n" ]; then
				msg "Preparing: $i"
				cp --reflink=auto "$i" "$SRC" || {
					msgerr "Preparing failed: $i"
					abort 1
				}
				continue 2
			fi
		done
		case "$i" in
			*.tar|*.tar.*|*.tgz|*.tbz2|*.txz|*.zst|*.zip|*.rpm|*.7z)
				msg "Unpacking: $i"
				bsdtar -xpo -C "$SRC" -f "$i" 2>/dev/null || \
				tar --no-same-owner -xpf "$i" -C "$SRC";;
			*)
				msg "Preparing: $i"
				cp --reflink=auto "$i" "$SRC";;
		esac || {
			msgerr "Unpacking/Preparing failed: $i"
			abort 1
		}
	done	
}

pkg_build() {	
	[ "$(id -u)" = 0 ] || {
		msgerr "You must build package as root, or use fakeroot."
	        help
		abort 1
	}
	
	msg "Build start: $name-$version-$release"
	
	cd "$SRC" >/dev/null || abort 1
	if [ "$QUIET" ]; then
		(set -e; build) 2>/dev/null
	else
		(set -e -x; build 2>&1)
	fi
		
	if [ $? -ne 0 ]; then
		msgerr "Build failed: $name-$version-$release"
		abort 1
	else
		msg "Build success: $name-$version-$release"
	fi
	
	cd - >/dev/null || return 1
}

pkglint() {
	linterror=0
	
	# check empty PKG
	if ! find "$PKG" -mindepth 1 -maxdepth 1 2>/dev/null | read -r _ ; then
		msgerr "PKG is empty"
		abort 1
	fi
	
	# check for backup file
	for f in $backup; do
		if [ ! -f "$PKG/$f" ]; then
			msgerr "Backup file '$f' does not exist in PKG!"
			linterror=1
		fi
	done
	
	if [ "$linterror" = 1 ]; then
		abort 1
	fi
}

strip_files() {
	# check skip files
	should_skip() {
		[ -z "$nostrip" ] && return 1
		for pattern in $nostrip; do
			case "$1" in
				*"$pattern"*) return 0 ;;
			esac
		done
		return 1
	}
	
	find . -type f -printf '%P\n' 2>/dev/null | while read -r binary ; do

		should_skip "$binary" && continue
		
		case "$(file -bi "$binary")" in
			*application/x-sharedlib*|*application/x-pie-executable*)  # Libraries (.so) | PIE executables
				${CROSS_COMPILE}strip --strip-unneeded "$binary" 2>/dev/null ;;
			*application/x-archive*)    # Libraries (.a)
				${CROSS_COMPILE}strip --strip-debug "$binary" 2>/dev/null ;;
			*application/x-object*)
				case "$binary" in
					*.ko)       # Kernel module
						${CROSS_COMPILE}strip --strip-unneeded "$binary" 2>/dev/null ;;
					*)
						continue;;
				esac;;
			*application/x-executable*) # Binaries
				${CROSS_COMPILE}strip --strip-all "$binary" 2>/dev/null ;;
			*)
				continue ;;
		esac
	done
}

compressinfomanpages() {
	find . -type f -path "*/share/man/*" | while read -r file; do
		if [ "$file" = "${file%%.gz}" ]; then
			gzip -9 -f "$file"
		fi
	done
	find . -type l -path "*/share/man/*" | while read -r file; do
		FILE="${file%%.gz}.gz"
		TARGET="$(readlink "$file")"
		TARGET="${TARGET##*/}"
		TARGET="${TARGET%%.gz}.gz"
		DIR="${FILE%/*}"
		rm -f "$file"
		if [ -e "$DIR/$TARGET" ]; then
			ln -sf "$TARGET" "$FILE"
		fi
	done
	find . -type f -path "*/share/info/*" | while read -r file; do
		if [ "$file" = "${file%%.gz}" ]; then
			gzip -9 -f "$file"
		fi
	done
}

pkg_package() {
	# lint $PKG before packaging
	pkglint
	
	cd "$PKG" >/dev/null || abort 1
	
	# remove possible conflict junks
	rm -f usr/share/info/dir usr/info/dir
	rm -f usr/lib/charset.alias # on musl system
	find . \( -name perllocal.pod -o -name .packlist \) -delete
	[ -d usr/share/fonts ] && {
		find usr/share/fonts \( -name fonts.dir -o -name fonts.scale \) -delete
	}

	case "$KEEP_LIBTOOL" in 0|no) find . ! -type d -name "*.la" -delete ;; esac
	case "$KEEP_LOCALE"  in 0|no) rm -fr usr/share/locale usr/locale usr/lib/locale ;; esac
	case "$KEEP_DOC"     in 0|no) rm -fr usr/share/doc usr/share/gtk-doc usr/doc usr/gtk-doc ;; esac
	case "$NO_STRIP"     in 0|no) strip_files ;; esac
	
	compressinfomanpages
	
	for FILE in $backup; do
		mv "$FILE" "$FILE.spkgnew"
	done
	
	[ "$FORCE_REBUILD" ] && rm -f "$PACKAGE_DIR/$PKGNAME"
	
	case "$COMPRESSION_MODE" in
		xz) COMPRESS="xz -T0" ;;
		gz) COMPRESS="gzip -9" ;;
		bz2) COMPRESS="bz2" ;;
		lz4) COMPRESS="lz4" ;;
		zstd) COMPRESS="zstd -19 -T0" ;;
	esac
	
	tar -c -I "$COMPRESS" -f "$PACKAGE_DIR/$PKGNAME" * $addtotar || {
		rm -f "$PACKAGE_DIR/$PKGNAME"
		msgerr "Packaging failed: $PKGNAME"
		abort 1
	}
	
	# Capture listing once; reuse for display and pkgfiles (avoids re-reading the archive)
	_tarlist=$(tar -tvf "$PACKAGE_DIR/$PKGNAME")
	printf '%s\n' "$_tarlist" | sort -k 6

	msg "Packaging success: $PKGNAME ($(ls -lh "$PACKAGE_DIR/$PKGNAME" | awk '{print $5}'))"

	cd - >/dev/null || return 1

	if [ ! -f .pkgfiles ] || [ "$(head -n1 .pkgfiles)" != "$name-$version-$release" ]; then
		pkg_genpkgfiles "$_tarlist"
	fi
}

pkg_genpkgfiles() {
	[ -f "$PACKAGE_DIR/$PKGNAME" ] || {
		msgerr "Package not found: $PKGNAME"
		exit 1
	}

	printf '%s\n' "$name-$version-$release" > .pkgfiles

	# If a pre-captured listing is passed as $1, use it; otherwise read the archive
	if [ -n "$1" ]; then
		printf '%s\n' "$1"
	else
		tar -tvf "$PACKAGE_DIR/$PKGNAME"
	fi | awk '{$3=$4=$5=""; print $0}' | sort -k 3 >> .pkgfiles

	msg ".pkgfiles updated"
}

pkg_clean() {
	[ -e "$PACKAGE_DIR/$PKGNAME" ] && {
		rm -f "$PACKAGE_DIR/$PKGNAME"
		msg "Package removed: $PACKAGE_DIR/$PKGNAME"
	}
	for i in $(get_filepath); do
		case $i in
			"$PWD"/*) continue;;
		esac
		[ -e "$i" ] && {
			rm -f "$i"
			msg "File removed: $i"
		}
		[ -e "$i.partial" ] && {
			rm -f "$i.partial"
			msg "File removed: $i.partial"
		}
	done
}

check_buildscript() {	
	# check the required field in buildscript
	[ "$name" ] || { msgerr "'name' is empty!"; exit 1;	}

	case "$name" in
		*[A-Z]*) msgerr "Capital letters for port name are not allowed!"; exit 1;;
	esac

	[ "${PWD##*/}" = "$name" ] || { msgerr "Port name and Directory name is different!"; exit 1; }

	[ "$version" ] || { msgerr "'version' is empty!"; exit 1; }

	[ "$release" ] || { msgerr "'release' is empty!"; exit 1; }

	case "$release" in
		*[A-Z]*|*[a-z]*|*-*|*\.*|*_*) msgerr "only numeric allowed in 'release'"; exit 1;;
	esac

	command -v build >/dev/null 2>&1 || { msgerr "'build' function not exist!"; exit 1; }

	case "$version" in
		*-*) msgerr "'version' should not contain '-'."; exit 1 ;;
	esac

	if [ "$release" -gt 99 ] || [ "$release" -lt 1 ]; then
		msgerr "'release' should numeric between 1 to 99"; exit 1
	fi

	[ "$description" ] || { msgerr "'description' is empty!"; exit 1; }
}

checkdir() {
	for DIR in "$@"; do
		if [ ! -d "$DIR" ]; then
			msgerr "Directory '$DIR' does not exist."
			exit 1
		elif [ ! -w "$DIR" ]; then
			msgerr "Directory '$DIR' not writable."
			exit 1
		elif [ ! -x "$DIR" ] || [ ! -r "$DIR" ]; then
			msgerr "Directory '$DIR' not readable."
			exit 1
		fi
	done
}

# cbuild - custom build option
cbuild_restore() {
	_rs_dest="$1"
	_rs_cfg="$CBUILD_DIR/${2:-$name}.cfg"

	[ -n "$_rs_dest" ] || {
		msgerr "cbuild_restore: destination path required"
		return 1
	}

	[ -f "$_rs_cfg" ] || return 1
		cp "$_rs_cfg" "$_rs_dest" || {
		msgerr "cbuild_restore: failed to copy to '$_rs_dest'"
		return 1
	}
}

cbuild_save() {
	_sv_src="$1"
	_sv_cfg="$CBUILD_DIR/${2:-$name}.cfg"

	[ -n "$_sv_src" ] || {
		msgerr "cbuild_save: source path required"
		return 1
	}
	[ -f "$_sv_src" ] || {
		msgerr "cbuild_save: '$_sv_src' not found"
		return 1
	}
	cp "$_sv_src" "$_sv_cfg" || {
		msgerr "cbuild_save: failed to write '$_sv_cfg'"
		return 1
	}
}

cbuild_options() {
	_op_cfg="$CBUILD_DIR/${1:-$name}.opt"
	if [ -f "$_op_cfg" ]; then . "$_op_cfg"; fi
}

pkg_cleanup() {
	if [ ! "$KEEP_WORK" ] && [ "$name" ]; then
		rm -fr "$WORK_DIR/$name"
	fi
}

interrupted() {
	printf '\n'
	abort 100
}

abort() {
	rm -f "$LOCK_FILE"
	pkg_cleanup
	exit "$1"
}

parse_opts() {
	while [ "$1" ]; do
		case $1 in
			-[!-]?*)
				_rest=${1#-}; shift
				while [ -n "$_rest" ]; do
					_chr="${_rest%"${_rest#?}"}"
					set -- "-$_chr" "$@"
					_rest=${_rest#?}
				done
				continue ;;
		esac
		case "$1" in
		-q | --quiet)           QUIET=yes ;;
		-i | --install)         INSTALL_PKG=yes ;;
		-u | --upgrade)         UPGRADE_PKG=yes; OPTS="$OPTS $1" ;;
		-r | --reinstall)       REINSTALL_PKG=yes; OPTS="$OPTS $1" ;;
		-c | --ignore-conflict) OPTS="$OPTS $1" ;;
		-v | --verbose)         OPTS="$OPTS $1" ;;
		-f | --force-rebuild)   FORCE_REBUILD=yes ;;
		-m | --skip-checksum)   IGNORE_CHECKSUM=yes ;;
		-g | --genchecksum)     UPDATE_CHECKSUM=yes ;;
		-o | --download)        DOWNLOAD_ONLY=yes ;;
		-x | --extract)         EXTRACT_ONLY=yes ;;
		-w | --keep-work)       KEEP_WORK=yes ;;
		-p | --pkgfiles)        GENPKGFILES=yes ;;
		-h | --help)            SHOWHELP=yes ;;
		-C | --clean)           CLEANUP=yes ;;
		     --no-backup)       OPTS="$OPTS $1" ;;
		     --root=*)          OPTS="$OPTS $1" ;;
		     --config=*)        PKGBUILD_CONF="${1#*=}" ;;
		     --srcdir=*)        CUSTOM_SOURCE_DIR="${1#*=}" ;;
		     --pkgdir=*)        CUSTOM_PACKAGE_DIR="${1#*=}" ;;
		     --workdir=*)       CUSTOM_WORK_DIR="${1#*=}" ;;
		     *)                 msg "Invalid ${0##*/} option! ($1)"; exit 1 ;;
		esac
		shift
	done
}

help() {	
	cat << 'EOF'
Usage:
  pkgbuild [ <options> <arguments> ]

Options:
  -q  --quiet               show only status messages and errors
  -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-checksum       skip checksum checking
  -g, --genchecksum         generate checksum
  -o, --download            download only source file
  -x, --extract             extract only source file
  -p, --pkgfiles            generate list files in package
  -w, --keep-work           keep working directory
  -h, --help                show this help message
  -C, --clean               remove downloaded sources and prebuilt packages
      --config=<config>     use custom config file
      --srcdir=<path>       override directory path for sources
      --pkgdir=<path>       override directory path for compiled package
      --workdir=<path>      override directory path for working dir
      --no-backup           skip backup configuration file when upgrading package
      
EOF
}

main() {
	parse_opts "$@"
	
	if [ -f "$PKGBUILD_CONF" ]; then
		. "$PKGBUILD_CONF"
	else
		msgerr "Config file not found: $PKGBUILD_CONF"
		exit 1
	fi
	
	[ "$CUSTOM_SOURCE_DIR" ]  && SOURCE_DIR="$CUSTOM_SOURCE_DIR"	
	[ "$CUSTOM_PACKAGE_DIR" ] && PACKAGE_DIR="$CUSTOM_PACKAGE_DIR"	
	[ "$CUSTOM_WORK_DIR" ]    && WORK_DIR="$CUSTOM_WORK_DIR"

	# Ensure cbuild directory exists; non-fatal if it cannot be created
	[ -d "$CBUILD_DIR" ] || mkdir -p "$CBUILD_DIR" 2>/dev/null

	checkdir "$SOURCE_DIR" "$PACKAGE_DIR" "$WORK_DIR"
	
	# show usage
	[ "$SHOWHELP" ] && {
		help
		exit 0
	}
	
	if [ -f "$PKGBUILD_BSCRIPT" ]; then
		. ./"$PKGBUILD_BSCRIPT"
	else
		msgerr "'$PKGBUILD_BSCRIPT' file not found."
		exit 1
	fi
	
	check_buildscript

	case "$COMPRESSION_MODE" in
		gz|bz2|xz|lz4|zstd) PKGNAME="$name-$version-$release.spkg.tar.$COMPRESSION_MODE" ;;
		        *) msgerr "Invalid compression mode: $COMPRESSION_MODE"; exit 1 ;;
	esac
	
	# generate .pkgfiles
	[ "$GENPKGFILES" ] && {
		pkg_genpkgfiles
		exit 0
	}
	
	# download source only
	[ "$DOWNLOAD_ONLY" ] && {
		pkg_fetch
		exit 0
	}
	
	# extract source only
	[ "$EXTRACT_ONLY" ] && {
		pkg_unpack
		exit 0
	}
	
	# update checksum
	[ "$UPDATE_CHECKSUM" ] && {
		pkg_genchecksums
		exit 0
	}
	
	# remove source and package
	[ "$CLEANUP" ] && {
		pkg_clean
		exit 0
	}

	LOCK_FILE="/tmp/pkgbuild.$name.lock"
	
	# check for lock file
	[ -f "$LOCK_FILE" ] && {
		msgerr "Cannot build same package simultaneously."
		msgerr "remove '$LOCK_FILE' if no build process for '$name'."
		exit 1
	}
	
	# create lock file
	touch "$LOCK_FILE" 2>/dev/null || {
		msgerr "Cannot create lock file in '$LOCK_FILE'."
		exit 1
	}
	
	# build package
	if [ -f "$PACKAGE_DIR/$PKGNAME" ] && [ ! "$FORCE_REBUILD" ]; then
		if [ ! "$INSTALL_PKG" ] && [ ! "$REINSTALL_PKG" ] && [ ! "$UPGRADE_PKG" ]; then
			msg "Package is up-to-date: $PKGNAME"
			abort 0
		fi
	else
		
		msg "Building: $name-$version-$release"
		if [ "$QUIET" ]; then
			pkg_fetch 2>/dev/null
			pkg_checksum >/dev/null
			pkg_unpack >/dev/null
			pkg_build >/dev/null
			pkg_package >/dev/null
			pkg_cleanup
		else
			pkg_fetch
			pkg_checksum
			pkg_unpack
			pkg_build 
			pkg_package
			pkg_cleanup
		fi
	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" 1 2 3 15

export LC_ALL=C

PKGBUILD_CONF="/etc/scratchpkg.conf"
PKGBUILD_BSCRIPT="spkgbuild"

SOURCE_DIR="/var/cache/scratchpkg/sources"
PACKAGE_DIR="/var/cache/scratchpkg/packages"
WORK_DIR="/var/cache/scratchpkg/work"
CBUILD_DIR="/var/lib/scratchpkg/cbuild"
COMPRESSION_MODE="xz"
NO_STRIP="no"
IGNORE_CHECKSUM="no"
KEEP_LIBTOOL="no"
KEEP_LOCALE="no"
KEEP_DOC="no"

main "$@"
