#!/usr/bin/env bash
# Build the binary package sys-kernel/bentoo-kernel-bin (or, with --gentoo,
# sys-kernel/gentoo-kernel-bin) installs.
#
#     [JOBS=<n>] [CPUS=<list>] scripts/build-bentoo-kernel-bin.sh [--gentoo] <version>
#     scripts/build-bentoo-kernel-bin.sh [--gentoo] --from-gpkg <gpkg> <version>
#
# <version> is a PVR, e.g. 7.2.10 or 7.2.10-r1. JOBS caps make's parallel
# jobs and load average (default: nproc). CPUS pins the build to a taskset(1)
# CPU list, e.g. CPUS=8-15,24-31 for one CCD of a 16-core Ryzen, leaving the
# rest of the machine free. --from-gpkg skips the build and runs only the
# checks and the copy below on a gpkg built earlier (e.g. one a failed check
# left behind).
#
# Builds this checkout's sys-kernel/bentoo-kernel-<version> into a gpkg and
# copies it into DISTDIR as <PF>-<build id>.amd64.gpkg.tar, the name
# bentoo-kernel-bin's SRC_URI expects (BINPKG-BINPKG_ID).
#
# --gentoo builds this checkout's sys-kernel/gentoo-kernel-<version> instead,
# for a gentoo-kernel-bin mirrored ahead of ::gentoo's binpkgs. The copy is
# named <PF>-<build id>-bentoo.amd64.gpkg.tar: ::gentoo publishes
# <PF>-<build id>.amd64.gpkg.tar, and a same-named file in a user's DISTDIR
# would fail its Manifest check. USE follows the official binpkg's (debug
# modules-sign strip, read from gentoo-kernel-7.2.12-1's metadata) minus
# generic-uki and secureboot: a generic UKI embeds an initramfs built from
# this machine's binaries, and secureboot needs a key. The official binary
# also leaves CONFIG_MODULE_SIG_FORCE unset, as below. Its sources carry no
# detached signature of their own; Portage checks them against the Manifest.
# The gpkg is compressed with xz -9e, like the official one: with zstd -19
# the 7.3.0 debug build came to 349 MB, over the 300 MiB that
# `wrangler r2 object put` accepts; xz -9e made it 300 MB. Portage puts the
# build id in the file name only: the directory inside is plain <PF>, which
# is what the -bin ebuild's BINPKG names. It never uploads
# and never edits an ebuild: publishing to R2 is a remote write, so the
# upload command is only printed.
#
# A binary for every bentoo user must not carry this machine's settings, and
# a plain `emerge` here would carry three of them:
#   - /etc/kernel/config.d/*.config, which kernel-build merges last (this
#     host forces CONFIG_MODULE_SIG_FORCE there, for example);
#   - make.conf CFLAGS (-march=native-class flags) for the host tools;
#   - package.env, which points MODULES_SIGN_KEY at the host's private key.
# So the build runs with a PORTAGE_CONFIGROOT of its own (nothing from
# /etc/portage), inside bwrap with /etc/kernel/config.d replaced by a
# directory of our own (LOCALVERSION, and MODULE_SIG_FORCE below). With MODULES_SIGN_KEY unset the
# kernel generates a throwaway key; kernel-build keeps it out of the image.
#
# USE=modules-sign makes kernel-build force CONFIG_MODULE_SIG_FORCE=y. For a
# binary that would reject every out-of-tree module (nvidia-drivers, ...) on
# every machine, Secure Boot or not, since the signing key is discarded. The
# config.d fragment, merged last, turns it back off: modules stay signed,
# untrusted ones load with a taint, and Secure Boot's lockdown still
# enforces signatures (Fedora's behaviour).
#
# Fixed USE for bentoo-kernel, matching bentoo-kernel-bin's promises: bore sched-ext modules-sign,
# no lto (GCC, provisional -- see the -bin ebuild), no hardened, no debug, no
# generic-uki.
#
# The CachyOS tarball's signature is checked here with gpgv rather than by
# USE=verify-sig, which needs sec-keys/openpgp-keys-cachyos installed on the
# build machine; the key files come from that package's distfiles, whose
# hashes its Manifest pins.
#
# Checks before exit (any failure exits 1, the gpkg is not copied):
#   - KV is <version>-bentoo-dist-bin (-gentoo-dist-bin with --gentoo) and
#     the .config was resolved under it;
#   - CONFIG_MODULE_SIG=y and CONFIG_MODULE_SIG_FORCE NOT set (the host's
#     config.d forces it on, so this is also the leakage canary);
#   - the embedded module cert is the build's autogenerated one;
#   - no private key anywhere in the image.
#
# Exit: 0 built, 1 a check failed, 2 usage/environment error.
# Needs the network only when a distfile is missing from DISTDIR.
set -euo pipefail

die() { printf 'build-bentoo-kernel-bin: %s\n' "$*" >&2; exit 2; }
fail() { printf 'build-bentoo-kernel-bin: FAIL: %s\n' "$*" >&2; exit 1; }

pkg="bentoo-kernel"
if [[ ${1:-} == --gentoo ]]; then
	pkg="gentoo-kernel"
	shift
fi
from_gpkg=
if [[ ${1:-} == --from-gpkg ]]; then
	[[ $# -ge 2 && -f $2 ]] || die "--from-gpkg needs an existing gpkg file"
	from_gpkg=$(realpath "$2")
	shift 2
fi
[[ $# -eq 1 && $1 =~ ^[0-9][0-9.]*(-r[0-9]+)?$ ]] || die "usage: $0 [--gentoo] [--from-gpkg <gpkg>] <version>"
pvr=$1 pv=${1%-r*}

repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
ebuild_file=$repo/sys-kernel/$pkg/$pkg-$pvr.ebuild
[[ -f $ebuild_file ]] || die "no such ebuild: $ebuild_file"
for bin in bwrap ebuild portageq tar zstd xz openssl gpg gpgv taskset; do
	command -v "$bin" >/dev/null || die "missing tool: $bin"
done
gentoo=$(portageq get_repo_path / gentoo) || die "::gentoo not found"
distdir=$(portageq distdir)
[[ -w $distdir ]] || die "$distdir is not writable (portage group?)"

if [[ $pkg == bentoo-kernel ]]; then
	localversion=-bentoo-dist-bin suffix=
	compress=zstd compress_flags="-19 -T0"
	use="bore sched-ext modules-sign strip -verify-sig -lto -hardened -debug -generic-uki -secureboot -savedconfig -test"
	cachy_rel=$(sed -n 's/^CACHY_REL=\([0-9]\+\)$/\1/p' "$ebuild_file")
	[[ -n $cachy_rel ]] || die "no CACHY_REL= in $ebuild_file"
	tarball=$distdir/cachyos-$pv-$cachy_rel.tar.gz
	shopt -s nullglob
	keys=( "$distdir"/openpgp-keys-cachyos-*.asc )
	shopt -u nullglob
	[[ ${#keys[@]} -gt 0 ]] || die "no openpgp-keys-cachyos distfiles in $distdir (ebuild ... fetch it)"
	[[ -f $tarball && -f $tarball.asc ]] || die "missing $tarball{,.asc} (ebuild $ebuild_file fetch)"
else
	localversion=-gentoo-dist-bin suffix=-bentoo
	compress=xz compress_flags="-9e -T0"
	use="debug modules-sign strip -verify-sig -hardened -generic-uki -secureboot -savedconfig -test"
fi

jobs=${JOBS:-$(nproc)}
[[ $jobs =~ ^[1-9][0-9]*$ ]] || die "JOBS must be a positive integer, got: $jobs"
pin=()
if [[ -n ${CPUS:-} ]]; then
	taskset -c "$CPUS" true 2>/dev/null || die "CPUS is not a usable CPU list: $CPUS"
	pin=( taskset -c "$CPUS" )
fi
kv=$pv$localversion
# Short path on purpose: a long PORTAGE_TMPDIR breaks some builds (SUN_LEN).
work=$(mktemp -d /var/tmp/bkb.XXXXXX)
# Removed on success only: after a failure the build log under it is the
# evidence, so the path is printed instead.
ok=0
cleanup() {
	if ((ok)); then
		chmod -R u+w "$work" 2>/dev/null
		rm -rf "$work"
	else
		printf 'build-bentoo-kernel-bin: kept %s for inspection\n' "$work" >&2
	fi
}
trap cleanup EXIT

if [[ -z $from_gpkg && $pkg == bentoo-kernel ]]; then
	cat "${keys[@]}" | gpg --homedir "$work" --batch --quiet --dearmor > "$work/cachyos.gpg" \
		|| die "cannot read the CachyOS keys"
	gpgv --keyring "$work/cachyos.gpg" "$tarball.asc" "$tarball" 2>/dev/null \
		|| fail "$tarball: bad signature"
fi
if [[ -z $from_gpkg ]]; then
	cr=$work/cr
	mkdir -p "$cr/etc/portage/repos.conf" "$work/config.d" "$work/tmp" "$work/pkg"
	ln -s "$gentoo/profiles/default/linux/amd64/23.0" "$cr/etc/portage/make.profile"
	cat > "$cr/etc/portage/repos.conf/repos.conf" <<-EOF
		[DEFAULT]
		main-repo = gentoo

		[gentoo]
		location = $gentoo

		[bentoo]
		location = $repo
	EOF
	cat > "$cr/etc/portage/make.conf" <<-EOF
		COMMON_FLAGS="-march=x86-64-v3 -O2 -pipe"
		CFLAGS="\${COMMON_FLAGS}"
		CXXFLAGS="\${COMMON_FLAGS}"
		MAKEOPTS="-j$jobs -l$jobs"
		USE="$use"
		FEATURES="unprivileged -userpriv -usersandbox -ccache -distcc -test -buildpkg"
		PORTAGE_USERNAME="$(id -un)"
		PORTAGE_GRPNAME="$(id -gn)"
		PORTAGE_INST_UID="$(id -u)"
		PORTAGE_INST_GID="$(id -g)"
		DISTDIR="$distdir"
		PORTAGE_TMPDIR="$work/tmp"
		PKGDIR="$work/pkg"
		BINPKG_FORMAT="gpkg"
		BINPKG_COMPRESS="$compress"
		BINPKG_COMPRESS_FLAGS="$compress_flags"
	EOF
	# kernel-build merges config.d after the ebuild's own fragments.
	printf '%s\n' "CONFIG_LOCALVERSION=\"$localversion\"" \
		'# CONFIG_MODULE_SIG_FORCE is not set' > "$work/config.d/bentoo-dist-bin.config"

	printf 'build-bentoo-kernel-bin: building %s as %s in %s (%s jobs, CPUs %s)\n' "$pvr" "$kv" "$work" "$jobs" "${CPUS:-all}"
	env -i HOME="$HOME" PATH="$PATH" TERM="${TERM:-dumb}" \
		PORTAGE_CONFIGROOT="$cr" \
		"${pin[@]}" bwrap --dev-bind / / --ro-bind "$work/config.d" /etc/kernel/config.d \
		-- ebuild "$ebuild_file" clean package \
		|| die "ebuild $pvr failed (log under $work/tmp/portage)"

	mapfile -t gpkgs < <(find "$work/pkg" -name '*.gpkg.tar')
	[[ ${#gpkgs[@]} -eq 1 ]] || fail "expected one gpkg, found ${#gpkgs[@]}"
	gpkg=${gpkgs[0]}
else
	gpkg=$from_gpkg
fi
# File name: <PF>-<build id>; directory inside: the image's own top level.
binpkg=$(basename "$gpkg" .gpkg.tar)
mapfile -t images < <(tar -tf "$gpkg" | grep -E '^[^/]+/image\.tar\.[a-z0-9]+$')
[[ ${#images[@]} -eq 1 ]] || fail "expected one image.tar.* in $gpkg, found ${#images[@]}"
inner=${images[0]%%/*}

x=$work/check
mkdir -p "$x"
tar -xOf "$gpkg" "${images[0]}" | "$compress" -dc | tar -C "$x" -xf - \
	|| fail "cannot unpack $gpkg"
src=$x/image/usr/src/linux-$kv
[[ -s $src/.config ]] || fail "no usr/src/linux-$kv/.config in the image"
[[ -d $x/image/lib/modules/$kv ]] || fail "no lib/modules/$kv in the image"
grep -qx "CONFIG_LOCALVERSION=\"$localversion\"" "$src/.config" \
	|| fail "LOCALVERSION is not $localversion"
grep -qx 'CONFIG_CC_IS_GCC=y' "$src/.config" || fail "not a GCC build"
grep -qx 'CONFIG_MODULE_SIG=y' "$src/.config" || fail "modules are not signed"
grep -qx 'CONFIG_MODULE_SIG_FORCE=y' "$src/.config" \
	&& fail "CONFIG_MODULE_SIG_FORCE=y: our config.d did not win (host leak?)"
subject=$(openssl x509 -inform DER -in "$src/certs/signing_key.x509" -noout -subject) \
	|| fail "no readable certs/signing_key.x509"
[[ $subject == *'Build time autogenerated kernel key'* ]] \
	|| fail "module cert is not the build's own: $subject"
leaked=$(grep -rlI -e '-----BEGIN [A-Z ]*PRIVATE KEY-----' "$x/image" || true)
[[ -z $leaked ]] || fail "private key in the image: $leaked"

out=$distdir/$binpkg$suffix.amd64.gpkg.tar
cp "$gpkg" "$out"
ok=1
printf 'build-bentoo-kernel-bin: OK %s (%s)\n' "$out" "$(du -h "$out" | cut -f1)"
cat <<-EOF

	Next, by hand:
	  1. sys-kernel/$pkg-bin: BINPKG=$inner BINPKG_ID=${binpkg##*-}, then
	     pkgdev manifest sys-kernel/$pkg-bin
	  2. upload (remote write; run from the overlay, profile "bentoo"):
	     npx --yes wrangler@latest r2 object put \\
	       obentoo-distfiles/$binpkg$suffix.amd64.gpkg.tar \\
	       --file=$out --content-type=application/x-tar --remote
	  3. curl -sI https://distfiles.obentoo.org/$binpkg$suffix.amd64.gpkg.tar
EOF
