Files
Jyotisha/scripts/desktop_packaging_preflight.py
T
2026-06-24 13:37:33 +08:00

148 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""Check whether the Jyotish web app is ready for PWA/Pake/Tauri packaging."""
from __future__ import annotations
import json
import re
import shutil
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "jyotish-app"
def read(path: Path) -> str:
return path.read_text(encoding="utf-8")
def require(condition: bool, label: str, failures: list[str]) -> None:
if not condition:
failures.append(label)
def probe_command(binary: str, args: list[str] | None = None) -> dict:
path = shutil.which(binary)
result = {
"binary": binary,
"available": bool(path),
"path": path,
"version": None,
}
if not path:
return result
try:
completed = subprocess.run(
[path, *(args or ["--version"])],
capture_output=True,
text=True,
timeout=4,
check=False,
)
result["version"] = (completed.stdout or completed.stderr).strip().splitlines()[0] if (completed.stdout or completed.stderr).strip() else ""
except Exception as exc:
result["version"] = f"probe_failed: {exc}"
return result
def toolchain_probe() -> dict:
probes = {
"node": probe_command("node"),
"npm": probe_command("npm"),
"rustc": probe_command("rustc"),
"cargo": probe_command("cargo"),
"xcodebuild": probe_command("xcodebuild", ["-version"]),
"pake": probe_command("pake"),
"tauri": probe_command("tauri"),
}
xcode_version = probes["xcodebuild"].get("version") or ""
xcode_ready = bool(probes["xcodebuild"]["available"] and "requires Xcode" not in xcode_version)
return {
"non_destructive": True,
"note": "Probe only checks local CLI presence/version; it does not run npm build, pake, tauri build, signing, notarization, or generate packages.",
"license_gate": {
"pake": "Pake upstream is GPL-3.0; review distribution compatibility before shipping a bundled desktop app.",
"tauri": "Tauri app distribution still needs platform permissions, sidecar lifecycle design, signing_notarization, and Apple Developer decisions on macOS.",
},
"commands": probes,
"readiness": {
"pwa": True,
"pake": bool(probes["node"]["available"] and probes["npm"]["available"] and probes["rustc"]["available"] and probes["cargo"]["available"] and probes["pake"]["available"]),
"tauri": bool(probes["node"]["available"] and probes["npm"]["available"] and probes["rustc"]["available"] and probes["cargo"]["available"] and probes["tauri"]["available"]),
"macos_signing_notarization": xcode_ready,
},
"warnings": [
"xcodebuild exists but full Xcode is not selected; macOS signing_notarization is not ready."
] if probes["xcodebuild"]["available"] and not xcode_ready else [],
}
def main() -> int:
failures: list[str] = []
package = json.loads(read(APP / "package.json"))
manifest = json.loads(read(APP / "public" / "manifest.webmanifest"))
sw = read(APP / "public" / "sw.js")
html = read(APP / "index.html")
api_server = read(ROOT / "scripts" / "jyotish_api_server.py")
main_js = read(APP / "main.js")
click_smoke = read(ROOT / "tests" / "run_frontend_click_smoke.py")
scripts = package.get("scripts", {})
require("build" in scripts, "jyotish-app/package.json missing build script", failures)
require("preview" in scripts, "jyotish-app/package.json missing preview script", failures)
require(manifest.get("name") == "Jyotish Vedic Astrology", "manifest name mismatch", failures)
require(manifest.get("display") == "standalone", "manifest display must be standalone", failures)
require(manifest.get("scope") == "/", "manifest scope must be /", failures)
require(manifest.get("start_url") == "/", "manifest start_url must be /", failures)
require(bool(manifest.get("theme_color")), "manifest theme_color missing", failures)
require(any(icon.get("src") == "/pwa-icon.svg" for icon in manifest.get("icons", [])), "manifest icon missing", failures)
require("CACHE_NAME = 'jyotish-shell-v1'" in sw, "service worker cache name missing", failures)
require("url.pathname.startsWith('/api/')" in sw, "service worker must bypass API requests", failures)
require("caches.match('/index.html')" in sw, "service worker fallback missing", failures)
require('rel="manifest"' in html and "/manifest.webmanifest" in html, "index missing manifest link", failures)
require("/pwa-icon.svg" in html, "index missing app icon", failures)
require("JYOTISH_API_HOST', '127.0.0.1'" in api_server, "API host default must stay loopback", failures)
require("Trust Center" in main_js and "Local-first" in main_js, "Trust Center status missing", failures)
require("pwa-install" in main_js and "promptPWAInstall" in main_js, "PWA install action missing", failures)
require(bool(re.search(r"127\.0\.0\.1:5200", main_js)), "Trust Center must show loopback API boundary", failures)
require("tests/run_frontend_click_smoke.py" in read(ROOT / "scripts" / "run_quality_gate.py"), "quality gate must run browser click smoke", failures)
require("--mode" in click_smoke and "all" in click_smoke, "click smoke must support --mode all", failures)
require("offline_recovery_guidance_visible" in click_smoke, "click smoke must verify offline recovery guidance", failures)
require("manifest.webmanifest" in click_smoke and "serviceWorker" in click_smoke, "click smoke must verify PWA installed shell", failures)
if failures:
print(json.dumps({"valid": False, "failures": failures}, ensure_ascii=False, indent=2))
return 1
first_launch_checks = [
{
"path": "PWA installed shell",
"command": "python3 tests/run_frontend_click_smoke.py --mode all",
"expected": "manifest.webmanifest, serviceWorker, mobile shell, online workflow, and offline recovery guidance",
},
{
"path": "Pake first launch",
"command": "cd jyotish-app && npm run build && npm run preview -- --host 127.0.0.1 --port 4173",
"expected": "URL shell can open the built app; local API still needs python3 scripts/jyotish_api_server.py on 127.0.0.1:5200",
},
{
"path": "Tauri sidecar readiness",
"command": "python3 scripts/desktop_packaging_preflight.py",
"expected": "loopback API boundary, sidecar route, manifest, service worker, and Trust Center remain visible before scaffolding",
},
]
print(json.dumps({
"valid": True,
"packaging_paths": ["pwa", "pake-url-shell", "tauri-sidecar-spike"],
"app_dir": str(APP),
"api_default": "127.0.0.1:5200",
"first_launch_checks": first_launch_checks,
"toolchain_probe": toolchain_probe(),
}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())