94 lines
4.9 KiB
Python
94 lines
4.9 KiB
Python
# Copyright (c) 2026 Storm Dragon
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Build reproducible Tentacle release artifacts and a catalog for review."""
|
|
|
|
import argparse
|
|
import gzip
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tarfile
|
|
import tomllib
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
def build_package(repoRoot, architecture, version, name):
|
|
sourceRoot = repoRoot / name
|
|
manifest = tomllib.loads((sourceRoot / "tentacle.toml").read_text())
|
|
binaryName = manifest["entry-point"][0]
|
|
distRoot = repoRoot / "dist"
|
|
distRoot.mkdir(exist_ok=True)
|
|
programPath = distRoot / f"{name}-linux-{architecture}"
|
|
environment = dict(os.environ, CGO_ENABLED="0", GOOS="linux", GOARCH=architecture)
|
|
environment.setdefault("GOCACHE", "/tmp/cthulhu-cache")
|
|
subprocess.run(["go", "build", "-buildvcs=false", "-mod=readonly", "-trimpath",
|
|
"-ldflags=-s -w", "-o", str(programPath), f"./{name}/src"],
|
|
cwd=repoRoot, env=environment, check=True)
|
|
artifactPath = distRoot / f"{name}-{version}-linux-{architecture}.tar.gz"
|
|
with artifactPath.open("wb") as output:
|
|
with gzip.GzipFile(filename="", mode="wb", fileobj=output, mtime=0) as compressed:
|
|
with tarfile.open(fileobj=compressed, mode="w", format=tarfile.USTAR_FORMAT) as archive:
|
|
for name, data, mode in [
|
|
("tentacle.toml", (sourceRoot / "tentacle.toml").read_bytes(), 0o644),
|
|
("README.md", (sourceRoot / "README.md").read_bytes(), 0o644),
|
|
("LICENSE", (sourceRoot / "LICENSE").read_bytes(), 0o644),
|
|
(binaryName, programPath.read_bytes(), 0o755),
|
|
]:
|
|
member = tarfile.TarInfo(name)
|
|
member.size, member.mode, member.mtime = len(data), mode, 0
|
|
archive.addfile(member, io.BytesIO(data))
|
|
digest = hashlib.sha256(artifactPath.read_bytes()).hexdigest()
|
|
return artifactPath, digest
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--architecture", nargs="+", choices=["amd64", "arm64"], default=["amd64", "arm64"])
|
|
parser.add_argument("--tentacle", nargs="+", help="Catalog IDs to package (default: hello-world)")
|
|
parser.add_argument("--download-base", help="Immutable HTTPS directory containing the archives")
|
|
args = parser.parse_args()
|
|
if args.download_base:
|
|
parsed = urlparse(args.download_base)
|
|
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.fragment or parsed.query:
|
|
parser.error("Download base must be an HTTPS directory without credentials, query or fragment")
|
|
if len(set(args.architecture)) != len(args.architecture):
|
|
parser.error("Architectures must not be repeated")
|
|
repoRoot = Path(__file__).resolve().parents[1]
|
|
catalog = tomllib.loads((repoRoot / "catalog.toml").read_text())
|
|
names = [item["id"] for item in catalog["tentacles"]]
|
|
selected = args.tentacle or ["hello-world"]
|
|
if len(set(selected)) != len(selected) or any(name not in names for name in selected):
|
|
parser.error("Select distinct Tentacle IDs from catalog.toml")
|
|
lines = ["format-version = 1"]
|
|
checksums = []
|
|
for item in catalog["tentacles"]:
|
|
name = item["id"]
|
|
lines += ["", "[[tentacles]]", f"id = {json.dumps(name)}",
|
|
f"manifest = {json.dumps(item['manifest'])}"]
|
|
releases = item.get("releases", [])
|
|
if name in selected:
|
|
manifest = tomllib.loads((repoRoot / item["manifest"]).read_text())
|
|
version = manifest["version"]
|
|
if not version or any(character not in "0123456789abcdefghijklmnopqrstuvwxyz.-" for character in version):
|
|
raise ValueError("Version must be safe for release filenames")
|
|
releases = []
|
|
for architecture in args.architecture:
|
|
artifactPath, digest = build_package(repoRoot, architecture, version, name)
|
|
releaseURL = (args.download_base.rstrip("/") + "/" + artifactPath.name) if args.download_base else f"https://git.stormux.org/storm/cthulhu-tentacles/releases/download/{name}-v{version}/{artifactPath.name}"
|
|
releases.append(dict(platform="linux", architecture=architecture, url=releaseURL, sha256=digest))
|
|
checksums.append(f"{digest} {artifactPath.name}")
|
|
print(f"Prepared {artifactPath.name}: {digest}")
|
|
for release in releases:
|
|
lines += ["", "[[tentacles.releases]]"]
|
|
lines += [f"{key} = {json.dumps(release[key])}" for key in ("platform", "architecture", "url", "sha256")]
|
|
(repoRoot / "dist/catalog.toml").write_text("\n".join(lines) + "\n")
|
|
(repoRoot / "dist/SHA256SUMS").write_text("\n".join(checksums) + "\n")
|
|
print("Prepared dist/catalog.toml and dist/SHA256SUMS for review. Nothing was published.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|