#!/usr/bin/env python3 """Build a guarded Zynq-7000 BOOT.bin from FSBL + PL bit + PS ELF. The output is generated as a candidate, checked for capacity and required ASCII build markers, then atomically promoted. Existing BOOT.bin/BIF files are backed up before replacement. This script does not program hardware. """ import argparse import datetime as dt import hashlib import json import os import shutil import subprocess import sys from pathlib import Path from vivado_env import resolve_sdk_dir def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest().upper() def require_file(label: str, value: str) -> Path: path = Path(value).expanduser().resolve() if not path.is_file(): raise SystemExit(f"ERROR: missing {label}: {path}") return path def bif_path(path: Path) -> str: return path.as_posix() def main() -> None: parser = argparse.ArgumentParser( description="Build guarded Zynq BOOT.bin (boot FSBL + PL bit + PS ELF)" ) parser.add_argument("--sdk-dir", help="Xilinx SDK root containing bin/bootgen.bat") parser.add_argument("--boot-fsbl", required=True, help="FSBL executed at cold boot") parser.add_argument("--bit-file", required=True, help="implemented PL .bit file") parser.add_argument("--app-elf", required=True, help="PS application ELF") parser.add_argument("--boot-bin", required=True, help="final BOOT.bin path") parser.add_argument("--bif-file", help="persistent BIF path; default beside BOOT.bin") parser.add_argument("--capacity-bytes", type=int, default=32 * 1024 * 1024) parser.add_argument( "--require-marker", action="append", default=[], help="ASCII marker that must exist in the generated image; repeatable", ) parser.add_argument( "--backup-dir", help="backup root; default /flash_backups", ) parser.add_argument("--dry-run", action="store_true", help="validate and print only") args = parser.parse_args() boot_fsbl = require_file("boot FSBL", args.boot_fsbl) bit_file = require_file("PL bitstream", args.bit_file) app_elf = require_file("PS application ELF", args.app_elf) boot_bin = Path(args.boot_bin).expanduser().resolve() bif_file = ( Path(args.bif_file).expanduser().resolve() if args.bif_file else boot_bin.with_suffix(".bif") ) if args.capacity_bytes <= 0: raise SystemExit("ERROR: --capacity-bytes must be positive") try: marker_bytes = [(marker, marker.encode("ascii")) for marker in args.require_marker] except UnicodeEncodeError: raise SystemExit("ERROR: --require-marker values must be ASCII") app_bytes = app_elf.read_bytes() missing_app_markers = [marker for marker, raw in marker_bytes if raw not in app_bytes] if missing_app_markers: raise SystemExit( "ERROR: PS application ELF lacks required markers: " + repr(missing_app_markers) ) sdk_dir = Path(resolve_sdk_dir(args.sdk_dir)).expanduser().resolve() bootgen = sdk_dir / "bin" / "bootgen.bat" if not bootgen.is_file(): raise SystemExit(f"ERROR: bootgen not found: {bootgen}") bif_text = ( "the_ROM_image:\n" "{\n" f" [bootloader]{bif_path(boot_fsbl)}\n" f" {bif_path(bit_file)}\n" f" {bif_path(app_elf)}\n" "}\n" ) candidate = boot_bin.with_name(f"{boot_bin.name}.candidate.{os.getpid()}") cmd = [ str(bootgen), "-image", str(bif_file), "-arch", "zynq", "-o", str(candidate), "-w", "on", ] print(f"[build_zynq_boot] boot FSBL: {boot_fsbl} sha256={sha256(boot_fsbl)}") print(f"[build_zynq_boot] PL bit : {bit_file} sha256={sha256(bit_file)}") print(f"[build_zynq_boot] PS ELF : {app_elf} sha256={sha256(app_elf)}") print(f"[build_zynq_boot] output : {boot_bin}") print(f"[build_zynq_boot] capacity : {args.capacity_bytes}") print("[build_zynq_boot] command : " + subprocess.list2cmdline(cmd)) if args.dry_run: print("DRY_RUN_OK: no files changed and bootgen was not executed") return boot_bin.parent.mkdir(parents=True, exist_ok=True) bif_file.parent.mkdir(parents=True, exist_ok=True) stamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S") backup_root = ( Path(args.backup_dir).expanduser().resolve() if args.backup_dir else boot_bin.parent / "flash_backups" ) existing = [path for path in (boot_bin, bif_file) if path.exists()] if existing: backup = backup_root / stamp backup.mkdir(parents=True, exist_ok=False) for path in existing: shutil.copy2(path, backup / path.name) print(f"[build_zynq_boot] backup : {backup}") bif_file.write_text(bif_text, encoding="utf-8", newline="\n") try: result = subprocess.run(cmd, check=False) if result.returncode != 0 or not candidate.is_file(): raise RuntimeError(f"bootgen failed, rc={result.returncode}") size = candidate.stat().st_size if size >= args.capacity_bytes: raise RuntimeError( f"candidate size {size} is not below capacity {args.capacity_bytes}" ) image = candidate.read_bytes() missing = [marker for marker, raw in marker_bytes if raw not in image] if missing: raise RuntimeError("candidate lacks required markers: " + repr(missing)) candidate_hash = sha256(candidate) os.replace(candidate, boot_bin) except Exception as exc: if candidate.exists(): candidate.unlink() raise SystemExit(f"ERROR: {exc}") manifest = { "format": "zynq-7000-boot-bin", "created": dt.datetime.now().astimezone().isoformat(), "capacity_bytes": args.capacity_bytes, "required_markers": args.require_marker, "components": { "boot_fsbl": {"path": str(boot_fsbl), "sha256": sha256(boot_fsbl)}, "pl_bit": {"path": str(bit_file), "sha256": sha256(bit_file)}, "ps_elf": {"path": str(app_elf), "sha256": sha256(app_elf)}, }, "boot_bin": {"path": str(boot_bin), "size": size, "sha256": candidate_hash}, } manifest_path = boot_bin.with_suffix(boot_bin.suffix + ".manifest.json") manifest_path.write_text( json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" ) print(f"ZYNQ_BOOTGEN_PASS size={size} sha256={candidate_hash}") print(f"MANIFEST={manifest_path}") if __name__ == "__main__": main()