"""Build the SETProtocol shared library without imposing one host toolchain. CMake remains the primary build. On a Windows workstation where CMake is absent, the script locates Visual Studio and invokes MSVC directly. Unix-like hosts fall back to the system C compiler. """ from __future__ import annotations import argparse import os import platform import shutil import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parent.parent INCLUDE = ROOT / "include" JNI_INCLUDES: list[Path] = [] SOURCES = [ ROOT / "src" / name for name in ( "set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c", "balsam_can.c", "set_crc.c", "periph28335.c", "tms2812.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c", "pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c", "pcan_gas.c", ) ] def _visual_studio_vcvars() -> Path | None: installer = Path(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")) vswhere = installer / "Microsoft Visual Studio" / "Installer" / "vswhere.exe" if vswhere.exists(): result = subprocess.run( [str(vswhere), "-latest", "-products", "*", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-property", "installationPath"], text=True, capture_output=True, check=False, ) if result.returncode == 0 and result.stdout.strip(): candidate = (Path(result.stdout.strip()) / "VC" / "Auxiliary" / "Build" / "vcvars64.bat") if candidate.exists(): return candidate return None def _build_msvc(output: Path, build_dir: Path) -> None: vcvars = _visual_studio_vcvars() if vcvars is None: raise SystemExit("Visual Studio C++ tools were not found") quoted_sources = " ".join(f'"{source}"' for source in SOURCES) import_library = build_dir / "setprotocol.lib" jni_flags = " ".join(f'/I"{path}"' for path in JNI_INCLUDES) command = ( f'call "{vcvars}" && cl /nologo /W4 /std:c11 ' f'/DPCAN_ABI_BUILD_DLL /I"{INCLUDE}" {jni_flags} /LD {quoted_sources} ' f'/Fe"{output}" /link /IMPLIB:"{import_library}"' ) # shell=True is intentional on Windows: ``call`` must run the batch file # in the same cmd.exe process that subsequently launches cl.exe. subprocess.run(command, cwd=build_dir, check=True, shell=True) def _build_cc(output: Path, build_dir: Path) -> None: compiler = shutil.which("cc") or shutil.which("clang") or shutil.which("gcc") if compiler is None: raise SystemExit("C compiler was not found") command = [compiler, "-std=c99", "-Wall", "-Wextra", "-Wpedantic", "-fPIC", "-shared", f"-I{INCLUDE}", *(f"-I{path}" for path in JNI_INCLUDES), *map(str, SOURCES), "-o", str(output), "-lm"] subprocess.run(command, cwd=build_dir, check=True) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--output", type=Path, required=True) parser.add_argument("--java-home", type=Path, help="Include the plot JNI adapter for host JVM tests") args = parser.parse_args() if args.java_home: include = args.java_home.resolve() / "include" if not (include / "jni.h").is_file(): parser.error("--java-home must contain include/jni.h") JNI_INCLUDES.extend([include, include / {"Windows": "win32", "Darwin": "darwin"}.get(platform.system(), "linux")]) SOURCES.extend([ ROOT / "ports" / "android" / "set_plot_jni.c", ROOT / "ports" / "android" / "setprotocol_jni.c", ]) output = args.output.resolve() output.parent.mkdir(parents=True, exist_ok=True) build_dir = output.parent / ".setprotocol-build" build_dir.mkdir(parents=True, exist_ok=True) if platform.system() == "Windows": _build_msvc(output, build_dir) else: _build_cc(output, build_dir) print(output) return 0 if __name__ == "__main__": raise SystemExit(main())