reorganized build directory
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
"""Shared PyInstaller driver for product GUI standalone folders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(_BUILD_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_BUILD_ROOT))
|
||||
|
||||
from common.paths import PYINSTALLER_WORK_DIR, REPO_ROOT, gui_dist_dir
|
||||
|
||||
|
||||
@dataclass
|
||||
class GuiBuildConfig:
|
||||
product: str
|
||||
entry_script: Path
|
||||
exe_name: str
|
||||
dist_name: str
|
||||
hidden_imports: list[str] = field(default_factory=list)
|
||||
exclude_modules: list[str] = field(default_factory=list)
|
||||
data_dirs: list[tuple[str, str]] = field(default_factory=list)
|
||||
readme_lines: list[str] = field(default_factory=list)
|
||||
windowed: bool = True
|
||||
upx_dir: str | None = r"C:\tools\upx"
|
||||
logo_name: str = "logo.png"
|
||||
work_dir: Path = PYINSTALLER_WORK_DIR
|
||||
spec_dir: Path = REPO_ROOT / "build" / "pyinstaller_work"
|
||||
|
||||
|
||||
def clean_dir(path: Path) -> None:
|
||||
if path.exists():
|
||||
shutil.rmtree(path)
|
||||
path.mkdir(parents=True)
|
||||
|
||||
|
||||
def build_with_pyinstaller(
|
||||
config: GuiBuildConfig,
|
||||
*,
|
||||
verify_fn: Callable[[Path], None] | None = None,
|
||||
) -> Path:
|
||||
"""Run PyInstaller for one GUI product and copy bundled data folders."""
|
||||
print("Using Python:", sys.executable)
|
||||
print(f"Building GUI product: {config.product}")
|
||||
|
||||
dist_dir = gui_dist_dir(config.dist_name)
|
||||
clean_dir(config.work_dir)
|
||||
clean_dir(dist_dir)
|
||||
config.spec_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
entry = config.entry_script
|
||||
if not entry.is_file():
|
||||
raise FileNotFoundError(f"GUI entry script not found: {entry}")
|
||||
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"PyInstaller",
|
||||
"--onedir",
|
||||
"--name",
|
||||
config.dist_name,
|
||||
f"--distpath={dist_dir.parent}",
|
||||
f"--workpath={config.work_dir}",
|
||||
f"--specpath={config.spec_dir}",
|
||||
"--noconfirm",
|
||||
"--clean",
|
||||
"--windowed" if config.windowed else "--console",
|
||||
]
|
||||
|
||||
for src_rel, dest_rel in config.data_dirs:
|
||||
src = (REPO_ROOT / src_rel).resolve()
|
||||
if not src.is_dir():
|
||||
raise FileNotFoundError(f"Data directory missing: {src}")
|
||||
cmd.append(f"--add-data={src};{dest_rel}")
|
||||
|
||||
logo = REPO_ROOT / config.logo_name
|
||||
if logo.is_file():
|
||||
cmd.append(f"--add-data={logo.resolve()};.")
|
||||
|
||||
for mod in config.hidden_imports:
|
||||
cmd.append(f"--hidden-import={mod}")
|
||||
for mod in config.exclude_modules:
|
||||
cmd.append(f"--exclude-module={mod}")
|
||||
|
||||
if config.upx_dir and Path(config.upx_dir).is_dir():
|
||||
cmd.append(f"--upx-dir={config.upx_dir}")
|
||||
|
||||
cmd.append(str(entry.resolve()))
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = (
|
||||
str((REPO_ROOT / "src").resolve())
|
||||
+ os.pathsep
|
||||
+ str((REPO_ROOT / "examples").resolve())
|
||||
+ os.pathsep
|
||||
+ env.get("PYTHONPATH", "")
|
||||
)
|
||||
|
||||
print("\n=== Running PyInstaller ===")
|
||||
print("Command:", " ".join(cmd))
|
||||
subprocess.check_call(cmd, env=env, cwd=REPO_ROOT)
|
||||
|
||||
final_exe = dist_dir / config.exe_name
|
||||
if not final_exe.is_file():
|
||||
raise RuntimeError(f"Executable not found at {final_exe}")
|
||||
|
||||
print("\n=== Copying data directories ===")
|
||||
for src_rel, dest_rel in config.data_dirs:
|
||||
src = REPO_ROOT / src_rel
|
||||
dest = dist_dir / dest_rel
|
||||
if not src.is_dir():
|
||||
raise FileNotFoundError(f"Data directory not found: {src}")
|
||||
if dest.exists():
|
||||
shutil.rmtree(dest)
|
||||
shutil.copytree(src, dest, dirs_exist_ok=True)
|
||||
print(f" {src} -> {dest}")
|
||||
|
||||
if logo.is_file():
|
||||
shutil.copy2(logo, dist_dir / config.logo_name)
|
||||
print(" Logo copied")
|
||||
|
||||
if config.readme_lines:
|
||||
(dist_dir / "README.txt").write_text("\n".join(config.readme_lines) + "\n", encoding="utf-8")
|
||||
|
||||
print(f"\n[SUCCESS] Standalone GUI folder: {dist_dir}")
|
||||
print(f"Executable: {final_exe}")
|
||||
|
||||
if verify_fn is not None:
|
||||
try:
|
||||
print("\n=== Smoke test ===")
|
||||
verify_fn(dist_dir)
|
||||
except Exception as exc:
|
||||
print(f"\n[WARNING] Smoke test failed: {exc}")
|
||||
|
||||
return dist_dir
|
||||
Reference in New Issue
Block a user