#!/usr/bin/env bash
#
# build.sh — package a component as an RPM, and put it in a repo.
#
#   build.sh                        build the console from the tree, package it
#   build.sh --binary PATH          package a binary somebody else already built
#   build.sh --component shop \
#            --binary PATH          package the terminal
#   build.sh --out DIR              write somewhere other than build/rpm/out
#
# The console is the default because it is the one component this repo can
# build. shop lives in its own repo, so there is nothing here to compile and
# --binary is the only way to package it — normally the binary the image
# shipped, taken out of a build with `podman cp`, so the hotfix is byte-for-byte
# what was tested. The image builds both itself, from these same specs, in the
# Containerfile's rust-build stage.
#
# This is the supply side of the hotfix channel (GoingsOn task d866e125). Alloy
# is distributed as a builder rather than as an image, so a machine that is
# already installed has no way to receive a fix short of rebuilding an ISO and
# writing a drive. A signed repo carrying only Make Creative's own components,
# layered with rpm-ostree, is the answer; this produces what it serves.
#
# WHAT MUST NOT HAPPEN TO THIS PACKAGE. It must never end up in a base image.
# A component the base carries cannot be replaced client-side: `rpm-ostree
# install` fails to depsolve against it, and `override replace` records a
# request that never activates and silently survives a reboot as a no-op. So a
# base that ships the console is a machine that can never be sent a console
# fix. Measured, in build/layertest — read its README before changing where
# this package lands.
#
# Unsigned so far, and the decision is in: **both repos get signed**, the
# carried one included. Ruled by Max 2026-08-17 over the recommendation, which
# was to leave the carried repo explicitly unsigned on the grounds that it
# reaches no network and is only as trustworthy as the image around it. What
# the ruling buys is one code path: the carried repo and the network repo are
# built, configured and verified the same way, so no branch exists that only
# the network path exercises, and the key and its handling exist before
# anything is under pressure to ship.
#
# So `gpgcheck=0` in the generated .repo file is unfinished work, not a
# position. What is left is the key: it is generated under the same rule as
# the sops identities (never in the repo it signs, never synced by anything),
# and where it lives is settled before it is generated. Then this script signs
# what it builds, the public key ships in the image, the .repo snippets carry
# `gpgcheck=1` and a `gpgkey`, and a test fails the build if the carried repo
# does not verify. GoingsOn alloy d866e125.
set -euo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$HERE/../.." && pwd)"

# shellcheck source=build/privilege.sh
. "$REPO_ROOT/build/privilege.sh"

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

BINARY=""
COMPONENT="alloy"
OUT="$HERE/out"
while [ $# -gt 0 ]; do
  case "$1" in
    --binary)    BINARY="${2:-}"; shift 2 ;;
    --component) COMPONENT="${2:-}"; shift 2 ;;
    --out)       OUT="${2:-}"; shift 2 ;;
    *) die "unknown argument $1" ;;
  esac
done

case "$COMPONENT" in
  alloy|shop) ;;
  *) die "unknown component $COMPONENT (alloy, shop)" ;;
esac
[ "$COMPONENT" != shop ] || [ -n "$BINARY" ] \
  || die "shop is a separate repo; package it with --binary PATH"

# Where the version comes from, and it is never an argument to this script. An
# RPM that claims a version the binary inside it does not report is invisible
# afterwards: it installs, rpm answers one thing, `--version` answers another,
# and `--version` is what a person quotes in a bug report.
#
# The console has a crate here to read, so that is the source and the binary is
# checked against it below. shop does not, so its own `--version` is the only
# statement of record — which is why it is asked before anything else happens
# rather than trusted after packaging.
if [ "$COMPONENT" = alloy ]; then
  VERSION="$(grep -m1 '^version' "$REPO_ROOT/crates/alloy/Cargo.toml" | cut -d'"' -f2)"
  [ -n "$VERSION" ] || die "no version in crates/alloy/Cargo.toml"
  # 0.0.0 was the placeholder every console build carried until 2026-08-14.
  # Kept as a guard rather than deleted with the placeholder: an RPM channel
  # ships a HIGHER version of a component, and there is nothing for a hotfix to
  # be higher than if this regresses.
  [ "$VERSION" != "0.0.0" ] || die "the console is back at 0.0.0, so no hotfix can outrank it"
else
  [ -x "$BINARY" ] || die "no shop binary at $BINARY"
  VERSION="$("$BINARY" --version 2>/dev/null | head -1 | awk '{print $2}')"
  [ -n "$VERSION" ] || die "$BINARY did not report a version"
fi

if [ -z "$BINARY" ]; then
  say "building the console $VERSION"
  # No --locked, unlike the Containerfile, and the difference is the build
  # location rather than a difference in intent. This runs on the dev host,
  # inside ~/Code, where `.cargo/config.toml`'s `[patch]` block redirects the
  # cross-repo git deps to the working copies; under it every resolve rewrites
  # the lock, so --locked fails on every machine that has the redirect rather
  # than catching anything (CLAUDE.md states this tree-wide). The Containerfile
  # builds in a container that has no such block, which is why it can pass it.
  #
  # What that costs: a hotfix built here takes whatever the sibling working
  # copies currently hold, so build it from a clean tree, and prefer handing in
  # a binary from the image build with --binary when the two must match.
  (cd "$REPO_ROOT" && cargo build --release -p alloy)
  BINARY="$REPO_ROOT/target/release/alloy"
fi
[ -x "$BINARY" ] || die "no $COMPONENT binary at $BINARY"

# Checked rather than trusted. Packaging a binary whose version disagrees with
# the spec's is the one mistake this script exists to make impossible, and it
# is invisible afterwards: the RPM installs, the machine reports the version
# rpm knows, and the binary reports another.
#
# For shop the two came from the same place a moment ago, so this is a re-read
# rather than a comparison. Left in the same path anyway: --binary can point at
# one file and the version have been taken from another only if this changes.
REPORTED="$("$BINARY" --version 2>/dev/null | head -1 | awk '{print $2}')"
[ "$REPORTED" = "$VERSION" ] \
  || die "binary reports $REPORTED, expected $VERSION"

mkdir -p "$OUT"
rm -rf "${OUT:?}/rpmbuild" "${OUT:?}/repo"
mkdir -p "$OUT/rpmbuild/SOURCES" "$OUT/repo"
install -m 0755 "$BINARY" "$OUT/rpmbuild/SOURCES/$COMPONENT"

# In a container rather than on the host: rpmbuild and createrepo_c are not on
# a dev box by default, and the package has to carry the same %{dist} as the
# base it installs into. fedora:43 matches the image; a package built as .fc42
# lands in a repo the machine will resolve and then refuses to be what anyone
# asked for.
say "packaging $COMPONENT-$VERSION"
privc podman run --rm \
  -v "$HERE:/spec:ro,z" \
  -v "$OUT:/out:z" \
  -w /out \
  registry.fedoraproject.org/fedora:43 bash -c "
    set -e
    dnf -y install rpm-build createrepo_c >/dev/null 2>&1
    rpmbuild --define '_topdir /out/rpmbuild' --define '${COMPONENT}_version $VERSION' \
      -bb /spec/$COMPONENT.spec >/dev/null
    cp /out/rpmbuild/RPMS/*/*.rpm /out/repo/
    createrepo_c /out/repo >/dev/null
  " >/dev/null

# podman writes as root through the bind mount, so the output is not the
# invoking user's without this. Left as a separate step rather than folded into
# the container command, which cannot know the uid outside it.
privc chown -R "$(id -u):$(id -g)" "$OUT"

say ""
say "built:"
find "$OUT/repo" -name '*.rpm' -printf '  %f\n'
say ""
say "repo metadata in $OUT/repo"
say "serve that directory and point a machine at it with a .repo file:"
say ""
say "  [alloy]"
say "  name=Alloy components"
say "  baseurl=https://<host>/rpm/"
say "  enabled=1"
say "  gpgcheck=0        # until signing lands; both repos are to be signed"
