#!/usr/bin/env python3 """Upload and run one PRG through the Ultimate REST runner.""" from __future__ import annotations import argparse from datetime import UTC, datetime import hashlib import json import os from pathlib import Path import sys from ultimate_rest import UltimateRestClient, UltimateRestError def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Reset an Ultimate, upload a PRG, and start it." ) parser.add_argument("host", help="device hostname or IPv4 address") parser.add_argument("prg", type=Path, help="local .prg file") parser.add_argument("--timeout", type=float, default=15.0) parser.add_argument( "--retries", type=int, default=0, help="extra launch attempts after retryable failures; each may reset (default: 0)", ) parser.add_argument("--report", type=Path, help="write machine-readable evidence here") parser.add_argument( "--password-env", default="C64U_PASSWORD", help="environment variable containing the network password", ) action = parser.add_mutually_exclusive_group(required=True) action.add_argument( "--dry-run", action="store_true", help="validate and print without sending" ) action.add_argument( "--yes-reset", action="store_true", help="confirm that resetting and replacing the running program is intended", ) return parser.parse_args() def load_prg(path: Path) -> bytes: if path.suffix.lower() != ".prg": raise ValueError("artifact must have a .prg extension") if not path.is_file(): raise ValueError(f"artifact does not exist: {path}") payload = path.read_bytes() if len(payload) < 3: raise ValueError("PRG must contain a two-byte load address and program data") return payload def write_report(path: Path, result: dict[str, object]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n") def main() -> int: args = parse_args() try: client = UltimateRestClient( args.host, timeout=args.timeout, password=os.environ.get(args.password_env), ) payload = load_prg(args.prg) except (OSError, ValueError) as error: print(f"Validation failed: {error}", file=sys.stderr) return 1 load_address = payload[0] | payload[1] << 8 sha256 = hashlib.sha256(payload).hexdigest() report: dict[str, object] = { "timestamp": datetime.now(UTC).isoformat(), "target": client.base_url, "artifact": { "path": str(args.prg.resolve()), "bytes": len(payload), "load_address": f"${load_address:04X}", "sha256": sha256, }, "action": "reset, DMA-load, and run", "result": "dry-run" if args.dry_run else "pending", "visible_result_verified": False, } print( f"Target: {client.base_url}\n" f"Artifact: {args.prg.resolve()}\n" f"Bytes: {len(payload)}\n" f"Load address: ${load_address:04X}\n" f"SHA-256: {sha256}\n" "Action: reset, DMA-load, and run" ) if args.dry_run: if args.report: write_report(args.report, report) return 0 try: response = client.json( "/v1/runners:run_prg", method="POST", data=payload, content_type="application/octet-stream", retries=args.retries, retry_404=args.retries > 0, ) report["result"] = "transport-accepted" report["response"] = response except (OSError, ValueError, UltimateRestError) as error: report["result"] = "failed" report["error"] = str(error) if args.report: write_report(args.report, report) print(f"Deployment failed: {error}", file=sys.stderr) return 1 if args.report: write_report(args.report, report) print( "Ultimate accepted the PRG at the transport layer; " "verify the visible device result." ) return 0 if __name__ == "__main__": raise SystemExit(main())