"""Convert a CCS C2000 OUT to SCI8 HEX/BIN and seal its pre-build identity. Uses only the standard library, TI hex2000 and the shared identity module. No network, SETGUI, or project-specific paths are required. """ from __future__ import annotations import argparse import json from pathlib import Path import re import subprocess import tempfile from identity import archive_build, seal, verify def postbuild(compiler: Path, build_dir: Path, artifact: str, metadata: Path, output: Path, export: Path = None) -> Path: # CCS may include the configured output directory in this macro. # Resolve the input separately; output filenames must use only its basename. artifact_path = Path(artifact) if not artifact or artifact_path.name in ("", ".", ".."): raise ValueError("artifact must name an OUT file without its extension") artifact = artifact_path.name if any(c in artifact for c in '\\/:*?"<>|'): raise ValueError("invalid artifact file base name") source_base = (artifact_path if artifact_path.is_absolute() else build_dir / artifact_path) source = Path(str(source_base) + ".out").resolve() tool = compiler.resolve() / "bin" / "hex2000.exe" if not tool.is_file(): raise FileNotFoundError(f"C2000 Hex Utility not found: {tool}") if not source.is_file(): raise FileNotFoundError(f"compiled OUT not found: {source}") metadata = metadata.resolve() settings = json.loads(metadata.read_text(encoding="utf-8")) build_id = settings["build_id"] if not isinstance(build_id, str) or not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", build_id): raise ValueError("invalid pre-build identity; run precompile before building") if build_id == "NOGIT000": print("[firmware-build] Git identity unavailable: using NOGIT000") output = output.resolve() output.mkdir(parents=True, exist_ok=True) # Keep failed conversions and incomplete manifests out of the final folder. with tempfile.TemporaryDirectory(prefix=".firmware-", dir=output) as temporary: stage = Path(temporary) hex_image = stage / (artifact + ".hex") bin_image = stage / (artifact + "_" + build_id + ".bin") for image, mode in ((hex_image, "-i"), (bin_image, "-b")): subprocess.run([str(tool), str(source), "-boot", "-sci8", "-map", str(image.with_suffix(image.suffix + ".map")), "-o", str(image), mode], check=True) if not image.is_file() or not image.stat().st_size: raise ValueError(f"hex2000 produced no image: {image}") seal(bin_image, metadata) verify(bin_image, bin_image.with_suffix(bin_image.suffix + ".manifest.json")) for item in stage.iterdir(): item.replace(output / item.name) result = output / bin_image.name archive_build(result, metadata, output, ( output / hex_image.name, output / (hex_image.name + ".map"), output / (bin_image.name + ".map")), export=export) print(f"[firmware-build] BIN: {result}") print(f"[firmware-build] Manifest: {result}.manifest.json") print(f"[firmware-build] SHA-256: {result}.sha256") return result def main() -> int: parser = argparse.ArgumentParser(description=__doc__) for name in ("compiler", "build-dir", "metadata", "output"): parser.add_argument("--" + name, type=Path, required=True) parser.add_argument("--artifact", required=True) parser.add_argument("--export", type=Path) try: postbuild(**vars(parser.parse_args())) except (OSError, ValueError, KeyError, TypeError, subprocess.CalledProcessError) as error: parser.exit(1, f"[firmware-build] ERROR: {error}\n") return 0 if __name__ == "__main__": raise SystemExit(main())