#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only

lockfile=${APORTS_BUILD_LOCKFILE:-/var/run/mqtt-exec.aports-build/aports-build.pid}
tmpdir=${TMPDIR:-/tmp}
release_deps_active=false

# Keep the flock file separate from the PID file and never unlink it: contenders
# must always lock the same inode. Children inherit fd 9 so the lock remains held
# while build commands are still running, even if the orchestrator is killed.
exec 9>"$lockfile.lock" || exit 1
if flock -n 9; then
	:
else
	rc=$?
	[ "$rc" -eq 1 ] && exit 0 # already running
	exit "$rc"
fi

cleanup() {
	if $release_deps_active; then
		abuild-apk del .alpine-release-deps
	fi
	rm -f "$lockfile"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

echo $$ > "$lockfile" || exit 1

logurl=
conf=${APORTS_BUILD_CONF:-/etc/conf.d/mqtt-exec.aports-build}
. "$conf" || exit 1

if [ -z "$git_branch" ]; then
	echo "Please set 'git_branch' in $conf" >&2
	exit 1
fi

rel=$upload_release

if [ -z "$rel" ]; then
	case "$git_branch" in
		master)		rel="edge";;
		[0-9]*-stable)	rel=v${git_branch%-stable} ;;
	esac
fi

if ! arch=$(abuild -A) || [ -z "$arch" ]; then
	printf '%s\n' 'aports-build: could not determine architecture' >&2
	printf '%s: could not determine arch\n' "$(date -Iseconds)" >>"$tmpdir/aports-build.log"
	exit 1
fi

aports=${APORTS:-$HOME/aports}
packages=${REPOSDIR:-$HOME/packages}
# Resolve relative paths before the build loop changes its working directory.
case "$aports" in
/*) ;;
*) aports="$PWD/$aports";;
esac
case "$packages" in
/*) ;;
*) packages="$PWD/$packages";;
esac
releasedir="$packages/releases/$arch"
release_state="$releasedir/.pending-$rel"
repos=${REPOS:-"main community testing"}

: ${buildrepo:="buildrepo -p"}
: ${upload_host:="dl-master.alpinelinux.org"}
: ${upload_prefix:="$upload_host:alpine"}
upload_pkg="$upload_prefix/$rel/"
upload_iso="$upload_prefix/$rel/releases/$arch/"

: ${hostname:=$(hostname)}
: ${mqtt_broker:="msg.alpinelinux.org"}
: ${status_msg:="mosquitto_pub -h $mqtt_broker -t build/$hostname -r -m"}
: ${upload_msg:="mosquitto_pub -h $mqtt_broker -t rsync/$upload_host/$rel/$arch -m"}
: ${logdir:="/var/cache/distfiles/buildlogs"}
: ${logurlprefix:="https://build.alpinelinux.org/buildlogs"}

log() {
	echo "$hostname: $@"
	$status_msg "$1"
}

die() {
	printf '%s: %s\n' "$hostname" "$*" >&2
	exit 1
}

git_value() {
	local value
	value=$(git "$@") || return 1
	[ -n "$value" ] || return 1
	printf '%s\n' "$value"
}

save_release_state() {
	local state_tmp rc
	mkdir -p "$releasedir" || return 1
	state_tmp=$(mktemp "$release_state.XXXXXX") || return 1
	printf '%s\n%s\n%s\n' "$pending_tag" "$pending_commit" "$pending_phase" > "$state_tmp" \
		&& mv -f "$state_tmp" "$release_state"
	rc=$?
	rm -f "$state_tmp"
	return "$rc"
}

# create new_release
create_release() {
	local release="$1"
	local release_deps="abuild apk-tools alpine-conf busybox fakeroot
		xorriso rsync squashfs-tools acct mkinitfs
		mtools"
	case "$arch" in
	aarch64|arm*|loongarch64|riscv64) release_deps="$release_deps sfdisk dosfstools grub-efi";;
	x86*) release_deps="$release_deps syslinux grub-efi";;
	ppc64le) release_deps="$release_deps grub grub-ieee1275";;
	s390x) release_deps="$release_deps s390-tools";;
	esac

	log "creating $release release"
	cd "$aports" || return 1
	release_deps_active=true
	local rc=0
	if abuild-apk add --virtual .alpine-release-deps $release_deps; then
		(
			if [ "$rel" = "edge" ]; then
				sh scripts/mkimage.sh --repository "$packages/main" --yaml \
					--tag "$release" --outdir "$releasedir" --profile "minirootfs netboot" \
					|| return 1
			else
				sh scripts/mkimage.sh --repository "$packages/main" --yaml \
					--tag "$release" --outdir "$releasedir" || return 1
			fi

			if $use_network; then
				log "uploading $release release"
				rsync --archive \
					--update \
					--verbose \
					--mkpath \
					$rsync_opts \
					"$releasedir"/* "$upload_iso" || return 1
			fi
		)
		rc=$?
	else
		rc=$?
	fi
	if abuild-apk del .alpine-release-deps; then
		release_deps_active=false
	elif [ "$rc" -eq 0 ]; then
		rc=1
	fi
	return $rc
}

build() (
	# before starting a build cycle, we might have some stale deps that weren't
	# removed from the previous run. this happens if buildrepo/abuild crash for
	# some reason, builders crash, network goes offline and a rare hang in buildrepo
	# happens (and then it gets killed), ..
	# clean up the environment before starting. this should always make it consistent, because:
	# - abuild cleans up after each build
	# - if it doesn't, that means it crashed, which means buildrepo failed too (unless keep-going is set, but it's not by default)
	# - so, buildrepo is started again, and prunes the deps, ..
	# hence, no build should have stale makedepends installed with just this deletion at the start.
	abuild-apk del .makedepends\*
	# Keep the command's status outside stdout: build output may contain any text.
	# A private status file avoids relying on shell-specific pipeline status APIs.
	statusdir=$(mktemp -d "$tmpdir/aports-build.XXXXXX") || return 1
	trap 'rm -f "$statusdir/status"; rmdir "$statusdir"' EXIT
	(
		$buildrepo -a "$aports" -d "$packages" "$@"
		printf '%s\n' "$?" > "$statusdir/status"
	) | while IFS= read -r line || [ -n "$line" ]; do
		case "$line" in
		[0-9]*/[0-9]*) $status_msg "$line";;
		*) printf '%s\n' "$line";;
		esac
	done
	IFS= read -r build_rc < "$statusdir/status" || return 1
	return "$build_rc"
)

cd "$aports" || die "could not enter aports directory: $aports"
[ -z "$repos" ] && return 1

use_network=true
force_release=false
skip_build=false

# parse opts
while getopts "fFu:ns" opt; do
	case $opt in
	'f') force=true;;
	'F') force_release=true;;
	'u') logurl=" $OPTARG";;
	'n') use_network=false
	     status_msg="echo status_msg:"
	     upload_msg="echo upload_msg:"
	     ;;
	's') skip_build=true;;
	esac
done
shift $(( $OPTIND - 1 ))

pending_tag=
pending_commit=
pending_phase=
if [ -f "$release_state" ]; then
	{
		IFS= read -r pending_tag && IFS= read -r pending_commit && IFS= read -r pending_phase
	} < "$release_state" || die "could not read pending release: $release_state"
	[ -n "$pending_tag" ] || die "empty pending release tag"
	case "$pending_commit" in
	''|*[!0-9a-f]*) die "invalid pending release commit";;
	esac
	case "${#pending_commit}" in
	40|64) ;;
	*) die "invalid pending release commit length";;
	esac
	case "$pending_phase" in
	build|release) ;;
	*) die "invalid pending release phase";;
	esac
fi

while true; do
	do_release=false
	rebuild=true
	cd "$aports" || die "could not enter aports directory: $aports"
	# check if we need to rebuild
	_old=$(git_value describe) || die "git describe failed before pull"
	_old_tag=$(git_value describe --abbrev=0) || die "git tag detection failed before pull"
	if $use_network; then
		log "pulling git"
		git checkout "$git_branch" || die "git checkout failed for branch: $git_branch"
		git pull || die "git pull failed for branch: $git_branch"
	fi
	_current=$(git_value describe) || die "git describe failed after source update"
	_current_tag=$(git_value describe --abbrev=0) || die "git tag detection failed after source update"
	if [ "$_old_tag" != "$_current_tag" ]; then
		log "$_old_tag -> $_current_tag"
		do_release=true
	fi

	# don't create release candidates on edge
	if [ "$rel" = "edge" ] && [ "${_current_tag%_rc*}" != "$_current_tag" ]; then
		do_release=false
	fi

	if [ "$_old" = "$_current" ] && [ -f "$tmpdir/uploaded" ] && [ -z "$force" ] && [ "$_current_tag" = "$_old_tag" ] && [ -z "$pending_tag" ]; then
		break
	fi

	if $force_release; then
		do_release=true
	fi
	force_release=false
	force=
	release_tag=$_current_tag
	if [ -n "$pending_tag" ]; then
		do_release=true
		release_tag=$pending_tag
		[ "$pending_phase" = release ] && rebuild=false
	fi

	# check if we need make new release
	if $do_release; then
		# we want build the realease from this tag
		git checkout "${pending_commit:-$release_tag}" || die "git checkout failed for release tag: $release_tag"
		_current=$(git_value describe) || die "git describe failed for release tag: $release_tag"
	fi
	_commit=$(git_value rev-parse --verify 'HEAD^{commit}') || die "could not determine source commit"
	if $do_release; then
		if [ -n "$pending_tag" ]; then
			[ "$_commit" = "$pending_commit" ] || die "pending release commit changed"
		else
			pending_tag=$release_tag
			pending_commit=$_commit
			pending_phase=build
			if $use_network; then
				save_release_state || die "could not record pending release"
			fi
		fi
	fi

	rc=0
	if $rebuild; then
		rm -f "$tmpdir/uploaded"

		# do the compile, send output to log
		log "building $_current"
		printf 'source commit: %s\n' "$_commit" > "$logdir/$hostname.log" \
			|| die "could not initialize build log"
		! $skip_build && for repo in $repos; do
			if ! build $repo >>"$logdir"/$hostname.log 2>&1 ; then
				errlog=$hostname.$_current.log
				cp "$logdir"/$hostname.log "$logdir"/$errlog
				# todo: revert last commit?
				log "failed"
				exit 1
			fi

			# upgrade our running system
			log "upgrading system"
			abuild-apk upgrade -U -a --quiet || log 'failed to apk upgrade'

			# copy for distribution
			cd "$packages" || die "could not enter package directory: $packages"
			if [ -z "$upload_pkg" ]; then
				continue
			fi

			log "uploading packages to $repo"
			$use_network && for i in $upload_pkg; do
				rsync --recursive \
					--update \
					--itemize-changes \
					--delete-delay \
					--delay-updates \
					--mkpath \
					$rsync_opts \
					$repo/$arch $i/$repo/ > "$tmpdir/upload-$repo"
				if [ $? -ne 0 ]; then
					rc=1
				elif [ -s "$tmpdir/upload-$repo" ]; then
					$upload_msg "$rel/$repo/$arch"
				fi
			done
		done
		[ $rc -eq 0 ] && touch "$tmpdir/uploaded" && abuild-apk update
		if $do_release && [ "$rc" -eq 0 ]; then
			pending_phase=release
			if $use_network; then
				save_release_state || die "could not record completed release package uploads"
			fi
		fi
	fi

	if $do_release && [ "$rc" -eq 0 ]; then
		if ! create_release "${release_tag#v}"; then
			log "failed"
			die "release creation, upload, or dependency cleanup failed: $release_tag"
		fi
		if $use_network; then
			if ! $rebuild; then
				touch "$tmpdir/uploaded" || die "could not restore completed package upload marker"
			fi
			rm -f "$release_state" || die "could not clear pending release"
		fi
		$upload_msg "$rel/releases/$arch"
		pending_tag=
		pending_commit=
	fi

done

log "idle"

