pro3_control_panel/package_pro3_exe.py
2025-12-15 09:23:52 +08:00

224 lines
6.2 KiB
Python

#!/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()