main
John Lauer Saif Abdessayed's diode-bridge VCA: KiCad project, Gerbers, BOM, simulations, reports, and the earlier RC filter test 45f3ff6 11d ago
#!/usr/bin/env python3
"""Build the fab subset, checksum manifest, and complete deliverables ZIP."""

from __future__ import annotations

import hashlib
from pathlib import Path
import shutil
import zipfile


ROOT = Path(__file__).resolve().parents[1]
FAB_SRC = ROOT / "manufacturing" / "gerbers"
FAB_DST = ROOT / "manufacturing" / "fabrication-release"
ZIP_PATH = ROOT.parent / "diode-vca-attenuator-complete-deliverables.zip"

FAB_FILES = (
    "diode-vca-attenuator-F_Cu.gtl",
    "diode-vca-attenuator-B_Cu.gbl",
    "diode-vca-attenuator-F_Mask.gts",
    "diode-vca-attenuator-B_Mask.gbs",
    "diode-vca-attenuator-F_Silkscreen.gto",
    "diode-vca-attenuator-Edge_Cuts.gm1",
    "diode-vca-attenuator.drl",
    "diode-vca-attenuator-drl_map.pdf",
    "diode-vca-attenuator-job.gbrjob",
    "drill-report.txt",
)

EXCLUDED_PARTS = {
    "__pycache__",
    "schematic-new",
    "schematic-final",
    "schematic-latest",
}


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for block in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()


def included_files() -> list[Path]:
    result = []
    for path in ROOT.rglob("*"):
        if not path.is_file():
            continue
        if any(part in EXCLUDED_PARTS for part in path.relative_to(ROOT).parts):
            continue
        if path == ROOT / "reports" / "SHA256SUMS.txt":
            continue
        result.append(path)
    return sorted(result, key=lambda item: item.relative_to(ROOT).as_posix())


def main() -> None:
    for name in FAB_FILES:
        source = FAB_SRC / name
        if not source.is_file() or source.stat().st_size == 0:
            raise SystemExit(f"missing or empty manufacturing output: {source}")
        shutil.copy2(source, FAB_DST / name)

    required = (
        ROOT / "diode-vca-attenuator.kicad_pro",
        ROOT / "diode-vca-attenuator.kicad_sch",
        ROOT / "diode-vca-attenuator.kicad_pcb",
        ROOT / "reports" / "erc-raw.json",
        ROOT / "reports" / "drc-raw.json",
        ROOT / "reports" / "board-lint-parity-raw.json",
        ROOT / "images" / "schematic" / "diode-vca-attenuator.png",
        ROOT / "images" / "board-top" / "diode-vca-attenuator-top.png",
        ROOT / "images" / "board-bottom" / "diode-vca-attenuator-bottom.png",
        ROOT / "simulation" / "results" / "transient_summary.csv",
        ROOT / "bom" / "diode-vca-attenuator-bom.csv",
    )
    for path in required:
        if not path.is_file() or path.stat().st_size == 0:
            raise SystemExit(f"missing or empty required deliverable: {path}")

    checksum_path = ROOT / "reports" / "SHA256SUMS.txt"
    lines = [f"{sha256(path)}  {path.relative_to(ROOT).as_posix()}" for path in included_files()]
    checksum_path.write_text("\n".join(lines) + "\n", encoding="utf-8")

    files = included_files() + [checksum_path]
    with zipfile.ZipFile(ZIP_PATH, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
        for path in files:
            archive.write(path, Path(ROOT.name) / path.relative_to(ROOT))

    print(f"wrote {ZIP_PATH} ({ZIP_PATH.stat().st_size} bytes, {len(files)} files)")


if __name__ == "__main__":
    main()