Files
voculinux/scripts/prepare_aur_source.py
T
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
2026-11-06 01:29:39 +05:30

173 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""Prepare the exact AUR source inputs, verifying published release provenance first.
Run with the locked release-build dependency group (provides packaging). Release
mode requires SHA256SUMS and the expected source commit. --local-unattested is
only for PR/local fixtures built by the reproducible-wheel gate, never publishing.
"""
from __future__ import annotations
import argparse
import hashlib
import re
import shutil
import subprocess
import sys
import tarfile
import tempfile
from email.parser import BytesParser
from pathlib import Path
from packaging.version import Version
ROOT = Path(__file__).resolve().parents[1]
TEMPLATE = ROOT / "packaging/aur/voculinux/PKGBUILD"
REPOSITORY = "VocaHQ/vocalinux"
def digest(path: Path) -> str:
"""Hash the original archive bytes, without extracting or repacking them."""
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def sdist_version(sdist: Path) -> str:
"""Require one canonical Voculinux sdist with matching top-level metadata."""
match = re.fullmatch(r"voculinux-([0-9][A-Za-z0-9.+!]*)\.tar\.gz", sdist.name)
if not match:
raise ValueError("expected a canonical voculinux-VERSION.tar.gz sdist")
version = match[1]
if str(Version(version)) != version:
raise ValueError("sdist filename version is not canonical")
root = f"voculinux-{version}"
with tarfile.open(sdist, "r:gz") as archive:
members = archive.getmembers()
if any(
member.name.split("/", 1)[0] != root or ".." in member.name.split("/")
for member in members
):
raise ValueError("sdist has an unexpected archive root or unsafe member path")
metadata = [member for member in members if member.name == f"{root}/PKG-INFO"]
if len(metadata) != 1 or not metadata[0].isfile():
raise ValueError("sdist must contain exactly one regular top-level PKG-INFO")
stream = archive.extractfile(metadata[0])
assert stream is not None
with stream:
info = BytesParser().parse(stream)
if info.get_all("Name") != ["voculinux"] or info.get_all("Version") != [version]:
raise ValueError("sdist metadata does not match its filename")
return version
def verify_release(sdist: Path, manifest: Path, tag: str, commit: str) -> str:
"""Fail closed on missing/mismatched hashes or untrusted release provenance."""
if not re.fullmatch(r"[0-9a-f]{40}", commit):
raise ValueError("release verification requires a full source commit SHA")
entries = []
for line in manifest.read_text(encoding="utf-8").splitlines():
match = re.fullmatch(r"([0-9a-f]{64}) (\S+)", line)
if not match:
raise ValueError("malformed SHA256SUMS entry")
if match[2] == sdist.name:
entries.append(match[1])
if len(entries) != 1:
raise ValueError("SHA256SUMS must pin the sdist exactly once")
if digest(sdist) != entries[0]:
raise ValueError("sdist checksum does not match SHA256SUMS")
subprocess.run(
[
"gh",
"attestation",
"verify",
str(sdist),
"--repo",
REPOSITORY,
"--signer-workflow",
f"{REPOSITORY}/.github/workflows/release.yml",
"--source-ref",
f"refs/tags/{tag}",
"--source-digest",
commit,
],
check=True,
)
return entries[0]
def prepare(
sdist: Path,
output: Path,
tag: str | None = None,
manifest: Path | None = None,
commit: str | None = None,
local_unattested: bool = False,
) -> None:
"""Stage an unchanged sdist and its fully pinned PKGBUILD only after validation."""
version = sdist_version(sdist)
tag = tag or (f"v{version}" if local_unattested else "")
if not re.fullmatch(r"v[0-9][A-Za-z0-9.+!-]*", tag) or Version(tag[1:]) != Version(version):
raise ValueError("release tag and sdist version do not match")
if local_unattested:
if manifest is not None or commit is not None:
raise ValueError("local-unattested mode cannot take release verification inputs")
print("UNATTESTED local/PR fixture: not authorized for AUR publication")
checksum = digest(sdist)
else:
if manifest is None or commit is None:
raise ValueError("release mode requires --manifest and --source-commit")
checksum = verify_release(sdist, manifest, tag, commit)
text = TEMPLATE.read_text(encoding="utf-8")
for key, value in {
"pkgver": tag[1:].replace("-", ""),
"_tag": tag[1:],
"_version": version,
"pkgrel": "1",
"sha256sums": f"('{checksum}')",
}.items():
text, count = re.subn(rf"^{key}=.*$", f"{key}={value}", text, flags=re.MULTILINE)
if count != 1:
raise ValueError(f"PKGBUILD template must assign {key} exactly once")
if output.exists() or output.is_symlink():
raise ValueError(f"output already exists; choose a fresh directory: {output}")
output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=".aur-source-", dir=output.parent) as temporary:
staging = Path(temporary) / "source"
staging.mkdir()
shutil.copyfile(sdist, staging / sdist.name)
if digest(staging / sdist.name) != checksum:
raise ValueError("sdist changed while staging")
(staging / "PKGBUILD").write_text(text, encoding="utf-8")
staging.rename(output)
print(f"Prepared {tag}: {checksum} {sdist.name} in {output}")
def main() -> int:
"""Prepare verified release inputs or an explicitly unattested local fixture."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sdist", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--tag", help="exact v* tag; defaults to the sdist version in local mode")
parser.add_argument("--manifest", type=Path, help="published SHA256SUMS")
parser.add_argument("--source-commit", help="expected full commit SHA of the release tag")
parser.add_argument("--local-unattested", action="store_true", help="PR/local fixture only")
args = parser.parse_args()
try:
prepare(
args.sdist,
args.output,
args.tag,
args.manifest,
args.source_commit,
args.local_unattested,
)
except (OSError, ValueError, tarfile.TarError, subprocess.CalledProcessError) as error:
print(f"ERROR: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())