#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ GPCT-standalone build script. - Cleans build output - Updates version.json releaseTime - Builds via PyInstaller (using venv Python only) - Copies extra runtime files - Verifies output """ from __future__ import annotations import json import shutil import subprocess import sys from datetime import datetime from pathlib import Path PROJECT_DIR = Path(__file__).resolve().parent VENV_DIR = PROJECT_DIR / "venv" DIST_DIR = PROJECT_DIR / "dist" BUILD_DIR = PROJECT_DIR / "build" SPEC_DIR = PROJECT_DIR / "spec" NSIS_SCRIPT = PROJECT_DIR / "GPCT-standalone.nsi" VERSION_FILE = PROJECT_DIR / "version.json" def run_command(cmd: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess: print(f"Running: {' '.join(cmd)}") result = subprocess.run( cmd, cwd=str(cwd or PROJECT_DIR), capture_output=True, text=False, ) # Decode safely to avoid UnicodeDecodeError on Windows tool output stdout_text = result.stdout.decode("utf-8", errors="replace") if isinstance(result.stdout, (bytes, bytearray)) else str(result.stdout) stderr_text = result.stderr.decode("utf-8", errors="replace") if isinstance(result.stderr, (bytes, bytearray)) else str(result.stderr) if result.returncode != 0 and check: print("[ERROR] Command failed") print("stdout:") print(stdout_text) print("stderr:") print(stderr_text) sys.exit(result.returncode) return result def venv_python() -> Path: py = VENV_DIR / "Scripts" / "python.exe" if not py.exists(): print(f"[ERROR] venv python not found: {py}") print("Please create venv and install dependencies.") sys.exit(1) return py def clean_build() -> None: print("\n[1/6] Cleaning old build output...") for dir_path in (DIST_DIR, BUILD_DIR): if dir_path.exists(): shutil.rmtree(dir_path) print(f" - Removed {dir_path}") def update_version() -> None: print("\n[2/6] Updating version.json releaseTime...") if not VERSION_FILE.exists(): print(f" [WARN] version.json not found: {VERSION_FILE}") return with VERSION_FILE.open("r", encoding="utf-8") as f: version_data = json.load(f) version_data["releaseTime"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with VERSION_FILE.open("w", encoding="utf-8") as f: json.dump(version_data, f, ensure_ascii=False, indent=4) print(f" - version: {version_data.get('version', 'N/A')}") print(f" - releaseTime: {version_data['releaseTime']}") def build_with_pyinstaller() -> None: print("\n[3/6] Building with PyInstaller...") python_exe = venv_python() spec_files = list(SPEC_DIR.glob("*.spec")) if not spec_files: print(f"[ERROR] No .spec files in {SPEC_DIR}") sys.exit(1) spec_file = SPEC_DIR / "GPCT-standalone.spec" if not spec_file.exists(): spec_file = spec_files[0] cmd = [str(python_exe), "-m", "PyInstaller", str(spec_file), "--clean", "--noconfirm"] run_command(cmd) print(" - PyInstaller build finished") def copy_additional_files() -> None: print("\n[4/6] Copying extra files...") onefile_exe = DIST_DIR / "GPCT-standalone.exe" onedir_dir = DIST_DIR / "GPCT-standalone" if onefile_exe.exists() and not onedir_dir.exists(): output_dir = DIST_DIR else: output_dir = onedir_dir internal_dir = output_dir / "_internal" target_dir = internal_dir if internal_dir.exists() else output_dir datafile_dir = target_dir / "dataFile" datafile_dir.mkdir(parents=True, exist_ok=True) for filename in ("config.json", "data.db"): src = PROJECT_DIR / "dataFile" / filename if src.exists(): shutil.copy2(src, datafile_dir / filename) print(f" - Copied {filename}") for filename in ("Syunew3D_x64.dll", "influxd.exe"): src = PROJECT_DIR / filename if src.exists(): shutil.copy2(src, target_dir / filename) print(f" - Copied {filename}") def build_nsis_installer() -> None: print("\n[5/6] Building NSIS installer...") if not NSIS_SCRIPT.exists(): print(f" [SKIP] NSIS script not found: {NSIS_SCRIPT}") return candidates = [ r"C:\Program Files (x86)\NSIS\makensis.exe", r"C:\Program Files\NSIS\makensis.exe", ] makensis = next((p for p in candidates if Path(p).exists()), None) if not makensis: result = subprocess.run("where makensis", shell=True, capture_output=True) if result.returncode == 0: stdout_text = result.stdout.decode("utf-8", errors="replace") if isinstance(result.stdout, (bytes, bytearray)) else str(result.stdout) makensis = stdout_text.strip().splitlines()[0] if not makensis: print(" [SKIP] NSIS not found (makensis.exe).") return cmd = [makensis, str(NSIS_SCRIPT)] result = run_command(cmd, check=False) if result.returncode == 0: print(" - NSIS build completed") installer = PROJECT_DIR / "GPCT-standalone-Setup.exe" if installer.exists(): print(f" - Installer: {installer}") else: print(" [WARN] NSIS build failed") print(result.stderr) def verify_build() -> bool: print("\n[6/6] Verifying build...") onedir_exe = DIST_DIR / "GPCT-standalone" / "GPCT-standalone.exe" onefile_exe = DIST_DIR / "GPCT-standalone.exe" if onedir_exe.exists(): exe_path = onedir_exe output_dir = DIST_DIR / "GPCT-standalone" elif onefile_exe.exists(): exe_path = onefile_exe output_dir = DIST_DIR else: print(f" [ERROR] Executable not found: {onedir_exe} or {onefile_exe}") return False size_mb = exe_path.stat().st_size / (1024 * 1024) print(f" - Executable: {exe_path}") print(f" - Size: {size_mb:.2f} MB") internal_dir = output_dir / "_internal" target_dir = internal_dir if internal_dir.exists() else output_dir print(" - Critical files check") for filename in ("config.json", "data.db", "Syunew3D_x64.dll"): file_path = target_dir / filename if file_path.exists(): print(f" [OK] {filename}") else: print(f" [MISSING] {filename}") return True def main() -> int: print("=" * 50) print(" GPCT-standalone Build Script") print("=" * 50) if not PROJECT_DIR.exists(): print(f"[ERROR] Project dir not found: {PROJECT_DIR}") return 1 clean_build() update_version() build_with_pyinstaller() copy_additional_files() build_nsis_installer() success = verify_build() print("\n" + "=" * 50) if success: print(" Build completed") print(f" Output: {DIST_DIR / 'GPCT-standalone'}") return 0 print(" Build failed") return 1 if __name__ == "__main__": sys.exit(main())