#!/usr/bin/env python3 """ Helper script to bundle pro3_uart.py into an executable together with the files that flash.py expects. Usage: python package_pro3_exe.py Prerequisites: pip install pyinstaller """ from __future__ import annotations import shutil import sys from pathlib import Path import re try: from PyInstaller import __main__ as pyinstaller_main except ImportError as exc: print("PyInstaller is required. Install it with 'pip install pyinstaller'.") raise SystemExit(1) from exc APP_ROOT = Path(__file__).resolve().parent PROJECT_BASE_NAME = "pro3_uart" def derive_version_from_folder() -> str: root_name = APP_ROOT.name match = re.search(r"_v([0-9][0-9.\-_]*)$", root_name, re.IGNORECASE) if not match: return "" return match.group(1) def read_version_file() -> tuple[str, str]: version_file = APP_ROOT / "version_info.py" if not version_file.exists(): return "", "" namespace: dict[str, str] = {} try: with version_file.open("r", encoding="utf-8") as handle: exec(handle.read(), namespace) except Exception: return "", "" canonical = str(namespace.get("version", "")).strip() display = str(namespace.get("display_version", "")).strip() return canonical, display def write_version_file(version: str, display: str) -> None: version_file = APP_ROOT / "version_info.py" template = f'''#! /usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2024 Realtek Semiconductor Corp. # SPDX-License-Identifier: Apache-2.0 version = "{version}" display_version = "{display}" ''' with version_file.open("w", encoding="utf-8") as handle: handle.write(template) def determine_version_tag() -> str: folder_version = derive_version_from_folder() stored_canonical, stored_display = read_version_file() chosen = folder_version or stored_display or stored_canonical sanitized = re.sub(r"[^0-9A-Za-z._-]", "_", chosen) if not sanitized: sanitized = "unknown" canonical = canonicalize_version_number(sanitized) if canonical != stored_canonical or sanitized != stored_display: write_version_file(canonical, sanitized) return sanitized def canonicalize_version_number(tag: str) -> str: cleaned = tag.replace("-", ".").replace("_", ".") parts = [re.sub(r"\D+", "", chunk) or "0" for chunk in cleaned.split(".") if chunk] if not parts: parts = ["0"] while len(parts) < 4: parts.append("0") return ".".join(parts[:4]) VERSION_TAG = determine_version_tag() RESOURCE_VERSION = canonicalize_version_number(VERSION_TAG) DIST_NAME = f"{PROJECT_BASE_NAME}_v{VERSION_TAG}" DIST_DIR = APP_ROOT / "dist" / DIST_NAME BUILD_DIR = APP_ROOT / "build" FLASH_SPEC = APP_ROOT / "flash.spec" FLASH_EXE_NAME = "flash.exe" FLASH_OUTPUT_PATHS = [ APP_ROOT / "dist" / FLASH_EXE_NAME, APP_ROOT / "dist" / "flash" / FLASH_EXE_NAME, ] RESOURCE_ITEMS = [ ("base", "base"), ("devices", "devices"), ("fw", "fw"), ("Reburn.cfg", "Reburn.cfg"), ("Reset.cfg", "Reset.cfg"), ("Settings.json", "Settings.json"), ("pylink", "pylink"), ] PYLINK_DIR = APP_ROOT / "pylink" JLINK_DLL_NAME = "JLinkARM.dll" JLINK_DLL_SOURCE = PYLINK_DIR / JLINK_DLL_NAME def clean_path(path: Path) -> None: if path.exists(): shutil.rmtree(path) def cleanup_old_version_specs() -> None: pattern = f"{PROJECT_BASE_NAME}_v*.spec" for spec_path in APP_ROOT.glob(pattern): try: spec_path.unlink() print(f"Removed outdated spec: {spec_path.name}") except Exception as exc: print(f"Warning: unable to remove {spec_path}: {exc}") def run_pyinstaller_for_pro3() -> None: if DIST_DIR.exists(): shutil.rmtree(DIST_DIR) clean_path(BUILD_DIR) args = [ "--noconfirm", "--clean", "--windowed", "--onedir", "--name", DIST_NAME, str(APP_ROOT / "pro3_uart.py"), ] print("Running PyInstaller...") pyinstaller_main.run(args) def run_pyinstaller_for_flash() -> None: if not FLASH_SPEC.exists(): raise SystemExit(f"flash.spec not found at {FLASH_SPEC}") clean_path(BUILD_DIR) print("Building flash.exe via flash.spec...") pyinstaller_main.run(["--noconfirm", str(FLASH_SPEC)]) def copy_resources() -> None: if not DIST_DIR.exists(): raise SystemExit(f"PyInstaller output not found: {DIST_DIR}") for src, dest in RESOURCE_ITEMS: src_path = APP_ROOT / src dest_path = DIST_DIR / dest if not src_path.exists(): print(f"Warning: resource '{src}' not found, skipping.") continue if src_path.is_dir(): if dest_path.exists(): shutil.rmtree(dest_path) shutil.copytree(src_path, dest_path) else: dest_path.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src_path, dest_path) print(f"Copied {src_path} -> {dest_path}") if src == "pylink": dll_path = dest_path / JLINK_DLL_NAME if not dll_path.exists(): raise SystemExit(f"{JLINK_DLL_NAME} was not found in '{src_path}'.") def copy_flash_executable() -> None: source = None for candidate in FLASH_OUTPUT_PATHS: if candidate.exists(): source = candidate break if not source: print("Warning: flash.exe build not found, skipping copy.") return dest = DIST_DIR / FLASH_EXE_NAME shutil.copy2(source, dest) print(f"Copied {source} -> {dest}") def verify_jlink_dll() -> None: if not JLINK_DLL_SOURCE.exists(): raise SystemExit( f"{JLINK_DLL_NAME} is required but missing from '{PYLINK_DIR}'. " "Copy the DLL into the pylink folder before packaging." ) def main() -> None: cleanup_old_version_specs() verify_jlink_dll() run_pyinstaller_for_pro3() run_pyinstaller_for_flash() copy_resources() copy_flash_executable() exe_path = DIST_DIR / f"{DIST_NAME}.exe" if exe_path.exists(): print(f"\nBuild complete: {exe_path}") else: print("\nBuild complete. Exe is located inside:", DIST_DIR) if __name__ == "__main__": main()