#!/usr/bin/env bash
# Check, and regenerate, the Go vendor tarball a package hosts on
# distfiles.obentoo.org.
#
#     scripts/go-vendor.sh [--check] <category/package>
#
# Several Go packages here build with `-mod=vendor` from a second distfile,
# `<name>-vendor.tar.xz`, generated by `go mod vendor` and uploaded to the R2
# bucket behind distfiles.obentoo.org. The autoupdate applier bumps PV and
# re-digests whatever SRC_URI names, but it cannot know whether the vendor
# tree still matches the new go.mod. When it does not, nothing turns red at
# bump time: the Manifest is complete, pkgcheck is clean, and the build dies
# on the user's machine with "inconsistent vendoring". dev-vcs/trufflehog
# 3.97.9 is the case that prompted this: its ebuild reused the 3.97.5 tree
# while go.mod had moved go-osc52 and mimetype.
#
# What it does, for the highest ebuild of <category/package>:
#   1. unpacks the source distfile (DISTDIR, else its SRC_URI) and runs
#      `go mod vendor`;
#   2. compares the resulting vendor/modules.txt with the one inside the
#      vendor tarball the ebuild currently names (from DISTDIR, or fetched
#      from distfiles.obentoo.org);
#   3. identical  -> exit 0, the pinned tarball is still valid;
#      different  -> with --check, exit 1; otherwise writes ${P}-vendor.tar.xz
#                    into DISTDIR (top directory = the source's, i.e. ${S})
#                    and prints the ebuild change and the upload command.
#
# It never uploads and never edits the ebuild: publishing to R2 is a remote
# write, and the ebuild edit depends on how that ebuild names VENDOR_P.
#
# Only the simple layout is supported: exactly one source distfile plus one
# `*-vendor.tar.xz`. sci-ml/localai is out of scope on purpose -- its vendor
# step needs protoc to run first; the recipe lives in its ebuild header.
#
# Exit: 0 fresh or regenerated, 1 stale (--check), 2 usage/environment error.
# Needs the network for the Go module proxy (and for the pinned tarball when
# it is not in DISTDIR).
set -euo pipefail

die() { printf 'go-vendor: %s\n' "$*" >&2; exit 2; }

check_only=0
if [[ ${1:-} == --check ]]; then check_only=1; shift; fi
[[ $# -eq 1 && $1 == */* ]] || die "usage: $0 [--check] <category/package>"
atom=$1

repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
[[ -d $repo/$atom ]] || die "$atom: no such package in $repo"
for bin in pquery portageq go tar xz curl; do
	command -v "$bin" >/dev/null || die "missing tool: $bin"
done
distdir=$(portageq distdir)

cpv=$(pquery -r "$repo" --raw --max "$atom" --cpv 2>/dev/null) \
	|| die "$atom: pquery could not resolve the highest version"
p=${cpv#*/}

# Distfile names and their URIs, both in SRC_URI order (one URI per file:
# the layout below has no mirror lists).
mapfile -t files < <(pquery -r "$repo" --raw "=$cpv" --attr fetchables 2>/dev/null \
	| grep -oE "\('[^']+'" | cut -c3- | tr -d "'")
read -ra uris <<< "$(pquery -r "$repo" --raw "=$cpv" --one-attr uris 2>/dev/null)"
[[ ${#files[@]} -eq 2 && ${#uris[@]} -eq 2 ]] \
	|| die "$cpv: unsupported layout (${#files[@]} distfiles, ${#uris[@]} URIs; want 1 source + 1 vendor)"
if [[ ${files[1]} == *-vendor.tar.xz ]]; then i=0 j=1; else i=1 j=0; fi
src=${files[$i]} src_uri=${uris[$i]} vendor=${files[$j]}
[[ $vendor == *-vendor.tar.xz && $src != *-vendor.tar.xz ]] \
	|| die "$cpv: unsupported layout ($src, $vendor)"

work=$(mktemp -d "${TMPDIR:-/var/tmp}/go-vendor.XXXXXX")
trap 'chmod -R u+w "$work" 2>/dev/null; rm -rf "$work"' EXIT

# A bump the applier just made stages its distfiles elsewhere, so DISTDIR may
# not have the new source yet; fetch it rather than stop.
src_file=$distdir/$src
if [[ ! -f $src_file ]]; then
	src_file=$work/$src
	curl -sfL -o "$src_file" "$src_uri" || die "$src: not in $distdir and $src_uri failed"
fi

mkdir "$work/src" "$work/pinned"
tar -xf "$src_file" -C "$work/src"
mapfile -t tops < <(ls -A "$work/src")
[[ ${#tops[@]} -eq 1 && -f $work/src/${tops[0]}/go.mod ]] \
	|| die "$src: expected a single top directory holding go.mod"
top=${tops[0]}

pinned=$distdir/$vendor
if [[ ! -f $pinned ]]; then
	pinned=$work/$vendor
	curl -sfL -o "$pinned" "https://distfiles.obentoo.org/$vendor" \
		|| die "$vendor: not in $distdir and not on distfiles.obentoo.org"
fi
tar -xJf "$pinned" -C "$work/pinned" --wildcards '*vendor/modules.txt' \
	|| die "$vendor: holds no vendor/modules.txt"
pinned_txt=$(find "$work/pinned" -path '*/vendor/modules.txt' | head -n1)

printf 'go-vendor: %s: running go mod vendor in %s\n' "$cpv" "$top"
(cd "$work/src/$top" && GOFLAGS=-modcacherw GOTOOLCHAIN=local go mod vendor) \
	|| die "$cpv: go mod vendor failed"

if cmp -s "$pinned_txt" "$work/src/$top/vendor/modules.txt"; then
	printf 'go-vendor: %s: FRESH -- %s still matches go.mod\n' "$cpv" "$vendor"
	exit 0
fi

printf 'go-vendor: %s: STALE -- %s does not match go.mod:\n' "$cpv" "$vendor"
diff <(grep '^# ' "$pinned_txt") <(grep '^# ' "$work/src/$top/vendor/modules.txt") \
	| grep '^[<>]' | head -n 20 || true
[[ $check_only -eq 1 ]] && exit 1

out=$p-vendor.tar.xz
XZ_OPT=-9T0 tar --sort=name --owner=0 --group=0 --numeric-owner --mtime=@0 \
	-C "$work/src" -cJf "$work/$out" "$top/vendor"
mv "$work/$out" "$distdir/$out"
cat <<EOF
go-vendor: wrote $distdir/$out ($(stat -c %s "$distdir/$out") bytes)
next:
  1. point the ebuild at it (SRC_URI must resolve to $out; for an ebuild
     with VENDOR_P, set VENDOR_P="\${P}")
  2. pkgdev manifest $atom
  3. from the overlay checkout (wrangler profile 'bentoo'):
     npx --yes wrangler@latest r2 object put obentoo-distfiles/$out \\
       --file=$distdir/$out --content-type=application/x-xz --remote
  4. curl -sI https://distfiles.obentoo.org/$out   # expect 200
EOF
