diff --git a/.github/workflows/pkgbuild-contract.yml b/.github/workflows/pkgbuild-contract.yml index 3f70af7..bd4ba4f 100644 --- a/.github/workflows/pkgbuild-contract.yml +++ b/.github/workflows/pkgbuild-contract.yml @@ -27,7 +27,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Contract tests - run: python3 -m unittest tests.test_pkgbuild_release_contract -v + run: python3 -m unittest discover -s tests -p "test_pkgbuild_release_contract.py" -v arch-package: name: arch build + install diff --git a/pkgbuilds/terraphim-clients-bin/.SRCINFO b/pkgbuilds/terraphim-clients-bin/.SRCINFO new file mode 100644 index 0000000..0c2592b --- /dev/null +++ b/pkgbuilds/terraphim-clients-bin/.SRCINFO @@ -0,0 +1,31 @@ +pkgbase = terraphim-clients-bin + pkgdesc = Terraphim AI command-line clients (prebuilt static MUSL binaries) + pkgver = 1.21.16 + pkgrel = 1 + url = https://terraphim.ai + arch = x86_64 + arch = aarch64 + license = Apache-2.0 + license = MIT + options = !strip + options = !debug + source_x86_64 = terraphim-agent-1.21.16-x86_64-unknown-linux-musl.tar.gz::https://github.com/terraphim/terraphim-clients/releases/download/v1.21.16/terraphim-agent-1.21.16-x86_64-unknown-linux-musl.tar.gz + source_x86_64 = terraphim-grep-1.21.16-x86_64-unknown-linux-musl.tar.gz::https://github.com/terraphim/terraphim-clients/releases/download/v1.21.16/terraphim-grep-1.21.16-x86_64-unknown-linux-musl.tar.gz + sha256sums_x86_64 = 71cb87455959f2eaa04fab1bd2655564635df58988dfae306c4aeb5583938151 + sha256sums_x86_64 = 872a7d7fe1167c75340807be0e3a0bb079389e28d813eb4e198b3d1757e7a583 + source_aarch64 = terraphim-agent-1.21.16-aarch64-unknown-linux-musl.tar.gz::https://github.com/terraphim/terraphim-clients/releases/download/v1.21.16/terraphim-agent-1.21.16-aarch64-unknown-linux-musl.tar.gz + source_aarch64 = terraphim-grep-1.21.16-aarch64-unknown-linux-musl.tar.gz::https://github.com/terraphim/terraphim-clients/releases/download/v1.21.16/terraphim-grep-1.21.16-aarch64-unknown-linux-musl.tar.gz + sha256sums_aarch64 = f8530d963171e3f72b52b8b729653cafa5ae5e105e219f7ecec7b170037acaa7 + sha256sums_aarch64 = 194d4cac04eccbfb998c4f060e44b286873510d354fd3a3a160bf63b2c5e025c + +pkgname = terraphim-agent-bin + pkgdesc = Terraphim AI agent CLI (prebuilt static MUSL binary) + license = Apache-2.0 + provides = terraphim-agent + conflicts = terraphim-agent + +pkgname = terraphim-grep-bin + pkgdesc = Terraphim AI grep CLI (prebuilt static MUSL binary) + license = MIT + provides = terraphim-grep + conflicts = terraphim-grep diff --git a/pkgbuilds/terraphim-clients-bin/PKGBUILD b/pkgbuilds/terraphim-clients-bin/PKGBUILD new file mode 100644 index 0000000..5ff0876 --- /dev/null +++ b/pkgbuilds/terraphim-clients-bin/PKGBUILD @@ -0,0 +1,87 @@ +# Maintainer: Terraphim Team +# Contributor: Alex Mikhalev +# +# Canonical split PKGBUILD for the prebuilt Terraphim AI client CLIs. +# +# This file is the single upstream source of truth for the AUR package +# `terraphim-clients-bin` (Gitea terraphim-clients#316). The downstream +# Omarchy directory +# `omacom/omarchy-pkgs/pkgbuilds/terraphim-clients-bin/` consumes the same +# split-package semantics (Gitea terraphim-clients#249). +# +# The release archives are self-contained (binary + both license texts) and +# are static-PIE linked, so nothing is compiled and neither output has a +# runtime library dependency. +# +# Regenerate the committed .SRCINFO after any edit; ./verify.sh fails on +# drift. See README.md for the container/CI invocation. + +pkgbase=terraphim-clients-bin +pkgname=(terraphim-agent-bin terraphim-grep-bin) +pkgver=1.21.16 +pkgrel=1 +pkgdesc='Terraphim AI command-line clients (prebuilt static MUSL binaries)' +arch=('x86_64' 'aarch64') +url='https://terraphim.ai' +license=('Apache-2.0' 'MIT') +options=('!strip' '!debug') + +_repo='terraphim/terraphim-clients' +_release="https://github.com/${_repo}/releases/download/v${pkgver}" + +# One archive per binary per architecture. Every archive extracts the CLI plus +# the repository's two license texts (identical bytes in both archives, so the +# colliding extraction is harmless). +source_x86_64=( + "terraphim-agent-${pkgver}-x86_64-unknown-linux-musl.tar.gz::${_release}/terraphim-agent-${pkgver}-x86_64-unknown-linux-musl.tar.gz" + "terraphim-grep-${pkgver}-x86_64-unknown-linux-musl.tar.gz::${_release}/terraphim-grep-${pkgver}-x86_64-unknown-linux-musl.tar.gz" +) +source_aarch64=( + "terraphim-agent-${pkgver}-aarch64-unknown-linux-musl.tar.gz::${_release}/terraphim-agent-${pkgver}-aarch64-unknown-linux-musl.tar.gz" + "terraphim-grep-${pkgver}-aarch64-unknown-linux-musl.tar.gz::${_release}/terraphim-grep-${pkgver}-aarch64-unknown-linux-musl.tar.gz" +) + +# Digests are the authoritative SHA-256 values published in the release's +# SHA256SUMS asset for v${pkgver}. tests/test_pkgbuild_release_contract.py pins +# the same values so a version bump cannot silently keep stale digests. +sha256sums_x86_64=( + '71cb87455959f2eaa04fab1bd2655564635df58988dfae306c4aeb5583938151' + '872a7d7fe1167c75340807be0e3a0bb079389e28d813eb4e198b3d1757e7a583' +) +sha256sums_aarch64=( + 'f8530d963171e3f72b52b8b729653cafa5ae5e105e219f7ecec7b170037acaa7' + '194d4cac04eccbfb998c4f060e44b286873510d354fd3a3a160bf63b2c5e025c' +) + +# Prebuilt, already-stripped static binaries: never strip or emit debug info. +# `depends` is intentionally left unset (empty) for both outputs: the MUSL +# binaries are static-PIE and pull in no runtime libraries. +# `arch`, `source*` and `sha256sums*` are shared by both split outputs. + +package_terraphim-agent-bin() { + pkgdesc='Terraphim AI agent CLI (prebuilt static MUSL binary)' + license=('Apache-2.0') + provides=('terraphim-agent') + conflicts=('terraphim-agent') + + install -Dm0755 "${srcdir}/terraphim-agent" \ + "${pkgdir}/usr/bin/terraphim-agent" + install -Dm0644 "${srcdir}/LICENSE-Apache-2.0" \ + "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE-Apache-2.0" + install -d "${pkgdir}/usr/share/terraphim/package-manager.d" + printf 'pacman\n' > "${pkgdir}/usr/share/terraphim/package-manager.d/terraphim-agent" +} + +package_terraphim-grep-bin() { + pkgdesc='Terraphim AI grep CLI (prebuilt static MUSL binary)' + license=('MIT') + provides=('terraphim-grep') + conflicts=('terraphim-grep') + + install -Dm0755 "${srcdir}/terraphim-grep" \ + "${pkgdir}/usr/bin/terraphim-grep" + install -Dm0644 "${srcdir}/LICENSE-MIT" \ + "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE-MIT" + install -d "${pkgdir}/usr/share/terraphim/package-manager.d" + printf 'pacman\n' > "${pkgdir}/usr/share/terraphim/package-manager.d/terraphim-grep" +} diff --git a/pkgbuilds/terraphim-clients-bin/README.md b/pkgbuilds/terraphim-clients-bin/README.md new file mode 100644 index 0000000..da38800 --- /dev/null +++ b/pkgbuilds/terraphim-clients-bin/README.md @@ -0,0 +1,71 @@ +# terraphim-clients-bin (Arch / AUR / Omarchy) + +Canonical split PKGBUILD for the prebuilt Terraphim AI client CLIs, published +from the public immutable GitHub release `terraphim/terraphim-clients`. + +- **pkgbase:** `terraphim-clients-bin` +- **outputs:** `terraphim-agent-bin` (Apache-2.0) and `terraphim-grep-bin` (MIT) +- **architectures:** `x86_64`, `aarch64` (static MUSL binaries, no runtime deps) +- **tracks:** Gitea terraphim-clients#316 (AUR), #249 (Omarchy downstream) + +Each split output owns exactly one executable, one license text and one +per-binary package-manager receipt: + +| Owners | `terraphim-agent-bin` | `terraphim-grep-bin` | +|---|---|---| +| executable | `/usr/bin/terraphim-agent` | `/usr/bin/terraphim-grep` | +| license | `/usr/share/licenses/terraphim-agent-bin/LICENSE-Apache-2.0` | `/usr/share/licenses/terraphim-grep-bin/LICENSE-MIT` | +| receipt | `/usr/share/terraphim/package-manager.d/terraphim-agent` = `pacman` | `/usr/share/terraphim/package-manager.d/terraphim-grep` = `pacman` | + +The receipt is what makes the packaged clients treat themselves as +package-managed: no startup update request and no self-update writing a shadow +binary under `~/.local/bin` (see `crates/terraphim_update/src/policy.rs`). + +## Verification + +`verify.sh` builds the split package as an unprivileged user, checks +`.SRCINFO` drift, runs `namcap`, asserts split ownership, then installs, +`pacman -Qkk`\ s, smoke-tests `--version` against `pkgver`, checks both +receipts and removes the packages. + +Run it in an Arch container (Docker/OrbStack on macOS): + +```bash +cd pkgbuilds/terraphim-clients-bin +docker run --rm -v "$PWD":/w -w /w archlinux:base-devel bash -lc \ + 'pacman -Sy --noconfirm namcap >/dev/null && ./verify.sh' +``` + +```bash +python3 -m unittest tests.test_pkgbuild_release_contract -v +``` + +## Regenerating .SRCINFO + +`.SRCINFO` is generated, never hand-edited: + +```bash +docker run --rm -v "$PWD":/w -w /w archlinux:base-devel bash -lc \ + 'makepkg --printsrcinfo' > .SRCINFO +``` + +## Releasing a new version + +1. Confirm the new public GitHub release exists with all four MUSL archives. +2. Bump `pkgver` in `PKGBUILD`. +3. Copy the two x86_64 and two aarch64 SHA-256 values from the release's + `SHA256SUMS` into the matching `sha256sums_*` arrays. +4. Update the pinned digests in `tests/test_pkgbuild_release_contract.py`. +5. Regenerate `.SRCINFO` and run `./verify.sh`. +6. AUR: clone `ssh://aur@aur.archlinux.org/terraphim-clients-bin.git`, + copy `PKGBUILD` and `.SRCINFO` to the repo root, commit and push + `master`. AUR is not on the Omarchy critical path. + +## Downstream Omarchy + +Omarchy consumes this package as one directory, +`omacom/omarchy-pkgs/pkgbuilds/terraphim-clients-bin/`, with +`.omarchy/package.json` using `"source": "local"`, `"release_ring": "fast"` +and an `upstream.watch.github` entry for `terraphim/terraphim-clients` +(see Gitea terraphim-clients#249). The split semantics above are the contract; +do not fork the PKGBUILD. diff --git a/pkgbuilds/terraphim-clients-bin/verify.sh b/pkgbuilds/terraphim-clients-bin/verify.sh new file mode 100755 index 0000000..0079a6b --- /dev/null +++ b/pkgbuilds/terraphim-clients-bin/verify.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Full Arch verification for the terraphim-clients-bin split package. +# +# Intended to run inside an Arch Linux container (see README.md): +# docker run --rm -v "$PWD/..":/mnt -w /mnt archlinux:base-devel \ +# bash -lc 'pacman -Sy --noconfirm namcap >/dev/null && ./pkgbuilds/terraphim-clients-bin/verify.sh' +# +# When run as root it builds as an unprivileged \`builder\` user (makepkg +# refuses to run as root) and installs/removes with pacman. +set -euo pipefail +cd "$(dirname "$0")" + +PKGVER="$(sed -n 's/^pkgver=//p' PKGBUILD)" +echo "== pkgver: $PKGVER ==" + +if [ "$(id -u)" -eq 0 ]; then + id builder >/dev/null 2>&1 || useradd -m builder + chown -R builder:builder . + run_makepkg() { runuser -u builder -- makepkg "$@"; } +else + run_makepkg() { makepkg "$@"; } +fi + +echo "== .SRCINFO drift ==" +run_makepkg --printsrcinfo > /tmp/.SRCINFO.generated +if ! diff -u .SRCINFO /tmp/.SRCINFO.generated; then + echo "ERROR: .SRCINFO is stale; regenerate with 'makepkg --printsrcinfo > .SRCINFO'" >&2 + exit 1 +fi + +echo "== build ==" +rm -f ./*.pkg.tar.zst +run_makepkg --force --noconfirm + +agent="$(ls terraphim-agent-bin-"$PKGVER"-1-*.pkg.tar.zst)" +grep_pkg="$(ls terraphim-grep-bin-"$PKGVER"-1-*.pkg.tar.zst)" +echo "artifacts: $agent $grep_pkg" + +echo "== namcap ==" +namcap "$agent" "$grep_pkg" + +echo "== split ownership ==" +list_agent="$(bsdtar -tf "$agent" | sort)" +list_grep="$(bsdtar -tf "$grep_pkg" | sort)" +printf '%s\n' "$list_agent" +printf '%s\n' "$list_grep" +for f in usr/bin/terraphim-agent usr/share/licenses/terraphim-agent-bin/LICENSE-Apache-2.0 usr/share/terraphim/package-manager.d/terraphim-agent; do + grep -qx "$f" <<<"$list_agent" || { echo "agent missing $f" >&2; exit 1; } +done +for f in usr/bin/terraphim-grep usr/share/licenses/terraphim-grep-bin/LICENSE-MIT usr/share/terraphim/package-manager.d/terraphim-grep; do + grep -qx "$f" <<<"$list_grep" || { echo "grep missing $f" >&2; exit 1; } +done +! grep -q 'terraphim-grep' <<<"$list_agent" +! grep -q 'terraphim-agent' <<<"$list_grep" + +echo "== install / Qkk / smoke / receipt / remove ==" +as_root() { if [ "$(id -u)" -eq 0 ]; then "$@"; else sudo "$@"; fi; } +as_root pacman -U --noconfirm "$agent" "$grep_pkg" +as_root pacman -Qkk terraphim-agent-bin terraphim-grep-bin +terraphim-agent --version | grep -F "$PKGVER" +terraphim-grep --version | grep -F "$PKGVER" +test "$(tr -d '\n' < /usr/share/terraphim/package-manager.d/terraphim-agent)" = pacman +test "$(tr -d '\n' < /usr/share/terraphim/package-manager.d/terraphim-grep)" = pacman +as_root pacman -R --noconfirm terraphim-agent-bin terraphim-grep-bin +test ! -e /usr/bin/terraphim-agent +test ! -e /usr/bin/terraphim-grep +test ! -e /usr/share/terraphim/package-manager.d/terraphim-agent + +echo "ALL CHECKS PASSED" diff --git a/tests/test_pkgbuild_release_contract.py b/tests/test_pkgbuild_release_contract.py new file mode 100644 index 0000000..a1f4505 --- /dev/null +++ b/tests/test_pkgbuild_release_contract.py @@ -0,0 +1,250 @@ +"""Contract tests for the canonical Arch/AUR split PKGBUILD. + +Covers Gitea terraphim-clients#316: one 'pkgbase=terraphim-clients-bin' +producing the 'terraphim-agent-bin' and 'terraphim-grep-bin' split outputs +from immutable public GitHub release assets, with per-output ownership of +exactly one binary, one license text and one pacman receipt. + +The real makepkg validation ('.SRCINFO' drift, 'namcap', install, +'pacman -Qkk', smoke, removal) lives in +'pkgbuilds/terraphim-clients-bin/verify.sh' and runs in an Arch container; +the '.SRCINFO' drift assertion here is skipped when 'makepkg' is absent. +""" + +import os +import re +import shutil +import subprocess +import tomllib +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PKGDIR = ROOT / "pkgbuilds" / "terraphim-clients-bin" +PKGBUILD = PKGDIR / "PKGBUILD" +SRCINFO = PKGDIR / ".SRCINFO" +CARGO = ROOT / "Cargo.toml" + +D = chr(36) # dollar sign, kept out of template literals in the test source + +PKGVER = "1.21.16" +REPO = "terraphim/terraphim-clients" +RELEASE_BASE = f"https://github.com/{REPO}/releases/download/v{PKGVER}" + +# Authoritative SHA-256 values from the release's SHA256SUMS asset. +ASSETS = { + "x86_64": { + "terraphim-agent": ( + f"terraphim-agent-{PKGVER}-x86_64-unknown-linux-musl.tar.gz", + "71cb87455959f2eaa04fab1bd2655564635df58988dfae306c4aeb5583938151", + ), + "terraphim-grep": ( + f"terraphim-grep-{PKGVER}-x86_64-unknown-linux-musl.tar.gz", + "872a7d7fe1167c75340807be0e3a0bb079389e28d813eb4e198b3d1757e7a583", + ), + }, + "aarch64": { + "terraphim-agent": ( + f"terraphim-agent-{PKGVER}-aarch64-unknown-linux-musl.tar.gz", + "f8530d963171e3f72b52b8b729653cafa5ae5e105e219f7ecec7b170037acaa7", + ), + "terraphim-grep": ( + f"terraphim-grep-{PKGVER}-aarch64-unknown-linux-musl.tar.gz", + "194d4cac04eccbfb998c4f060e44b286873510d354fd3a3a160bf63b2c5e025c", + ), + }, +} + +# Split-output contract: package -> (binary, license basename, SPDX) +OUTPUTS = { + "terraphim-agent-bin": ("terraphim-agent", "LICENSE-Apache-2.0", "Apache-2.0"), + "terraphim-grep-bin": ("terraphim-grep", "LICENSE-MIT", "MIT"), +} + +STRIP_COMMENT = re.compile(r"(?m)^\s*#.*$") + + +def pkgbuild_text() -> str: + return PKGBUILD.read_text() + + +def code_only() -> str: + """PKGBUILD text with comments removed (comments describe, not define).""" + return STRIP_COMMENT.sub("", pkgbuild_text()) + + +def scalar(name: str, text: str | None = None) -> str | None: + body = text if text is not None else code_only() + match = re.search(rf"(?m)^{re.escape(name)}=([^\s(][^\n]*)$", body) + if not match: + return None + return match.group(1).strip().strip("'\"") + + +def array(name: str, text: str | None = None) -> list[str]: + body = text if text is not None else code_only() + match = re.search(rf"(?m)^{re.escape(name)}=\(", body) + if not match: + return [] + index = match.end() + depth = 1 + while index < len(body) and depth: + if body[index] == "(": + depth += 1 + elif body[index] == ")": + depth -= 1 + index += 1 + inner = body[match.end() : index - 1] + raw_items = re.findall(r"'([^']*)'|\"([^\"]*)\"|([^\s()]+)", inner) + return [a or b or c for a, b, c in raw_items] + + +def expand(value: str) -> str: + return ( + value.replace(D + "{_repo}", REPO) + .replace(D + "{pkgver}", PKGVER) + .replace(D + "{_release}", RELEASE_BASE) + ) + + +def package_function(name: str) -> str: + pattern = "(?ms)^package_" + re.escape(name) + r"\(\) \{(.*?)^\}" + match = re.search(pattern, code_only()) + return match.group(1) if match else "" + + +def parse_srcinfo() -> tuple[dict[str, list[str]], dict[str, dict[str, list[str]]]]: + base: dict[str, list[str]] = {} + packages: dict[str, dict[str, list[str]]] = {} + current = base + for raw in SRCINFO.read_text().splitlines(): + if not raw.strip(): + continue + if raw.startswith("pkgbase = "): + current = base + base.setdefault("pkgbase", []).append(raw.split("=", 1)[1].strip()) + continue + if raw.startswith("pkgname = "): + name = raw.split("=", 1)[1].strip() + current = packages.setdefault(name, {}) + current.setdefault("pkgname", []).append(name) + continue + if "=" in raw: + key, value = raw.strip().split("=", 1) + current.setdefault(key.strip(), []).append(value.strip()) + return base, packages + + +class PkgbuildContractTests(unittest.TestCase): + def test_pkgbase_identity_and_version(self): + self.assertEqual(scalar("pkgbase"), "terraphim-clients-bin") + self.assertEqual(array("pkgname"), ["terraphim-agent-bin", "terraphim-grep-bin"]) + self.assertEqual(scalar("pkgver"), PKGVER) + self.assertEqual(scalar("pkgrel"), "1") + with CARGO.open("rb") as handle: + workspace = tomllib.load(handle)["workspace"]["package"]["version"] + self.assertEqual( + PKGVER, + workspace, + "pkgver must track the workspace version so the package cannot lag a release", + ) + + def test_architectures_and_options(self): + self.assertEqual(array("arch"), ["x86_64", "aarch64"]) + options = array("options") + self.assertIn("!strip", options) + self.assertIn("!debug", options) + + def test_sources_are_immutable_public_github_assets(self): + for arch, expected in ASSETS.items(): + entries = array(f"source_{arch}") + self.assertEqual( + len(entries), 2, f"source_{arch} must carry exactly the two client archives" + ) + resolved = {} + for entry in entries: + alias, _, url = entry.partition("::") + resolved[expand(alias)] = expand(url) + for binary, (alias, _sha) in expected.items(): + self.assertIn(alias, resolved, f"{arch}: missing source alias {alias}") + self.assertEqual( + resolved[alias], + f"{RELEASE_BASE}/{alias}", + f"{arch}: {binary} must come from the immutable release URL", + ) + + def test_digests_match_the_release_sha256sums(self): + for arch, expected in ASSETS.items(): + sums = array(f"sha256sums_{arch}") + self.assertEqual(len(sums), 2) + for index, (binary, (_alias, sha)) in enumerate(expected.items()): + self.assertEqual( + sums[index], + sha, + f"{arch}/{binary}: digest drifted from the release SHA256SUMS", + ) + self.assertRegex(sha, r"^[0-9a-f]{64}$") + + def test_each_output_owns_only_its_binary_license_and_receipt(self): + code = code_only() + license_prefix = D + "{pkgname}/" + for name, (binary, license_file, spdx) in OUTPUTS.items(): + with self.subTest(package=name): + body = package_function(name) + self.assertTrue(body, f"missing package function for {name}") + self.assertIn(f"provides=('{binary}')", body) + self.assertIn(f"conflicts=('{binary}')", body) + self.assertIn(f"license=('{spdx}')", body) + self.assertIn(f"/usr/bin/{binary}", body) + self.assertIn( + "/usr/share/licenses/" + license_prefix + license_file, body + ) + self.assertIn( + f"package-manager.d/{binary}", + body, + "each output must drop a per-binary pacman receipt", + ) + self.assertIn("pacman", body) + other = next(b for _n, (b, _l, _s) in OUTPUTS.items() if b != binary) + self.assertNotIn( + f"/usr/bin/{other}", + body, + "split outputs must not cross-install each other's executable", + ) + self.assertNotIn("build()", code, "prebuilt archives must not be compiled") + + def test_srcinfo_matches_pkgbuild(self): + self.assertTrue(SRCINFO.exists(), ".SRCINFO must be committed") + base, packages = parse_srcinfo() + self.assertEqual(base["pkgbase"], ["terraphim-clients-bin"]) + self.assertEqual(base["pkgver"], [PKGVER]) + self.assertEqual(base["pkgrel"], ["1"]) + self.assertEqual(sorted(base["arch"]), ["aarch64", "x86_64"]) + self.assertEqual(sorted(packages), sorted(OUTPUTS)) + for name, (_binary, _license_file, spdx) in OUTPUTS.items(): + self.assertEqual(packages[name]["license"], [spdx]) + self.assertEqual(packages[name]["provides"], [name.replace("-bin", "")]) + self.assertEqual(packages[name]["conflicts"], [name.replace("-bin", "")]) + for arch in ASSETS: + self.assertEqual(len(base[f"source_{arch}"]), 2) + self.assertEqual(len(base[f"sha256sums_{arch}"]), 2) + + @unittest.skipIf(os.geteuid() == 0, "makepkg refuses to run as root; verify.sh checks drift as the builder user") + @unittest.skipUnless(shutil.which("makepkg"), "makepkg is only available on Arch") + def test_srcinfo_is_not_stale(self): + generated = subprocess.run( + ["makepkg", "--printsrcinfo"], + cwd=PKGDIR, + check=True, + capture_output=True, + text=True, + ).stdout + self.assertEqual( + generated, + SRCINFO.read_text(), + "run 'makepkg --printsrcinfo > .SRCINFO' after editing PKGBUILD", + ) + + +if __name__ == "__main__": + unittest.main()