#!/usr/bin/env python3 """Decide whether a rebuild is needed. The build bundles its own R plus every non-system library it links against, so Arch moving `r`, `icu`, `jsoncpp` and friends cannot affect an installed JASP and must not trigger a rebuild — republishing costs every user a ~1.7 GB download. What is left is Qt6, which stays a system dependency. Qt guarantees binary compatibility across Qt 6 minor releases, so only a *major* Qt transition warrants a rebuild. Writes GITHUB_OUTPUT-style key=value pairs to stdout (and to $GITHUB_OUTPUT). """ import json import os import sys import urllib.request STATE_FILE = "state.json" GH_LATEST = "https://api.github.com/repos/jasp-stats/jasp-desktop/releases/latest" # No repo filter: icu lives in core while qt6-base lives in extra, and pinning # the wrong repo silently returns zero results and disables the trigger. ARCH_PKG = "https://archlinux.org/packages/search/json/?name={}" def fetch_json(url: str) -> dict: req = urllib.request.Request(url, headers={"User-Agent": "jasp-desktop-bin-ci"}) with urllib.request.urlopen(req, timeout=30) as r: return json.load(r) def arch_version(pkg: str) -> str: results = fetch_json(ARCH_PKG.format(pkg)).get("results", []) # Ignore staging/testing/kde-unstable: users are on the stable repos. stable = [r for r in results if r["repo"] in ("core", "extra")] if not stable: raise RuntimeError(f"package {pkg} not found in Arch core/extra") return stable[0]["pkgver"] def minor(version: str) -> str: return ".".join(version.split(".")[:2]) def main() -> int: forced = os.environ.get("FORCE_BUILD", "").lower() == "true" pinned_tag = os.environ.get("JASP_TAG", "").strip() jasp_tag = pinned_tag or fetch_json(GH_LATEST)["tag_name"] qt_version = arch_version("qt6-base") qt_major = qt_version.split(".")[0] qt_minor = minor(qt_version) try: state = json.loads(open(STATE_FILE).read()) except FileNotFoundError: state = {} reasons = [] if forced: reasons.append("manually forced") if state.get("jasp_tag") != jasp_tag: reasons.append(f"JASP {state.get('jasp_tag', 'none')} -> {jasp_tag}") if state.get("qt6_major") != qt_major: reasons.append(f"Qt major {state.get('qt6_major', 'none')} -> {qt_major}") needs_build = bool(reasons) print(f"jasp_tag = {jasp_tag}") print(f"qt = {qt_version} (major {qt_major})") print(f"decision = {'BUILD' if needs_build else 'up to date'}") for reason in reasons: print(f" reason: {reason}") if not needs_build: print(" (Qt minor bumps are binary-compatible and everything else is bundled)") out = os.environ.get("GITHUB_OUTPUT") if out: with open(out, "a") as fh: fh.write(f"needs_build={'true' if needs_build else 'false'}\n") fh.write(f"jasp_tag={jasp_tag}\n") fh.write(f"qt6_major={qt_major}\n") fh.write(f"qt6_minor={qt_minor}\n") fh.write(f"reasons={'; '.join(reasons)}\n") return 0 if __name__ == "__main__": sys.exit(main())