2026-04-16 14:23:37 +08:00
|
|
|
|
#!/usr/bin/env python3
|
2026-03-19 16:00:40 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
2026-04-16 14:23:37 +08:00
|
|
|
|
GPCT-standalone build script.
|
|
|
|
|
|
- Cleans build output
|
|
|
|
|
|
- Updates version.json releaseTime
|
|
|
|
|
|
- Builds via PyInstaller (using venv Python only)
|
|
|
|
|
|
- Copies extra runtime files
|
|
|
|
|
|
- Verifies output
|
2026-03-19 16:00:40 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-04-16 14:23:37 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-03-19 16:00:40 +08:00
|
|
|
|
import json
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import subprocess
|
2026-04-16 14:23:37 +08:00
|
|
|
|
import sys
|
2026-03-19 16:00:40 +08:00
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
2026-04-16 14:23:37 +08:00
|
|
|
|
PROJECT_DIR = Path(__file__).resolve().parent
|
2026-03-19 16:00:40 +08:00
|
|
|
|
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"
|
|
|
|
|
|
|
2026-04-16 14:23:37 +08:00
|
|
|
|
|
|
|
|
|
|
def run_command(cmd: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess:
|
|
|
|
|
|
print(f"Running: {' '.join(cmd)}")
|
2026-03-19 16:00:40 +08:00
|
|
|
|
result = subprocess.run(
|
|
|
|
|
|
cmd,
|
2026-04-16 14:23:37 +08:00
|
|
|
|
cwd=str(cwd or PROJECT_DIR),
|
2026-03-19 16:00:40 +08:00
|
|
|
|
capture_output=True,
|
2026-04-16 14:23:37 +08:00
|
|
|
|
text=False,
|
2026-03-19 16:00:40 +08:00
|
|
|
|
)
|
2026-04-16 14:23:37 +08:00
|
|
|
|
# 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)
|
|
|
|
|
|
|
2026-03-19 16:00:40 +08:00
|
|
|
|
if result.returncode != 0 and check:
|
2026-04-16 14:23:37 +08:00
|
|
|
|
print("[ERROR] Command failed")
|
|
|
|
|
|
print("stdout:")
|
|
|
|
|
|
print(stdout_text)
|
|
|
|
|
|
print("stderr:")
|
|
|
|
|
|
print(stderr_text)
|
|
|
|
|
|
sys.exit(result.returncode)
|
2026-03-19 16:00:40 +08:00
|
|
|
|
return result
|
|
|
|
|
|
|
2026-04-16 14:23:37 +08:00
|
|
|
|
|
|
|
|
|
|
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.")
|
2026-03-19 16:00:40 +08:00
|
|
|
|
sys.exit(1)
|
2026-04-16 14:23:37 +08:00
|
|
|
|
return py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clean_build() -> None:
|
|
|
|
|
|
print("\n[1/6] Cleaning old build output...")
|
|
|
|
|
|
for dir_path in (DIST_DIR, BUILD_DIR):
|
2026-03-19 16:00:40 +08:00
|
|
|
|
if dir_path.exists():
|
|
|
|
|
|
shutil.rmtree(dir_path)
|
2026-04-16 14:23:37 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-03-19 16:00:40 +08:00
|
|
|
|
spec_files = list(SPEC_DIR.glob("*.spec"))
|
|
|
|
|
|
if not spec_files:
|
2026-04-16 14:23:37 +08:00
|
|
|
|
print(f"[ERROR] No .spec files in {SPEC_DIR}")
|
2026-03-19 16:00:40 +08:00
|
|
|
|
sys.exit(1)
|
2026-04-16 14:23:37 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
2026-03-19 16:00:40 +08:00
|
|
|
|
else:
|
2026-04-16 14:23:37 +08:00
|
|
|
|
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"
|
2026-03-19 16:00:40 +08:00
|
|
|
|
datafile_dir.mkdir(parents=True, exist_ok=True)
|
2026-04-16 14:23:37 +08:00
|
|
|
|
|
|
|
|
|
|
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...")
|
2026-03-19 16:00:40 +08:00
|
|
|
|
if not NSIS_SCRIPT.exists():
|
2026-04-16 14:23:37 +08:00
|
|
|
|
print(f" [SKIP] NSIS script not found: {NSIS_SCRIPT}")
|
2026-03-19 16:00:40 +08:00
|
|
|
|
return
|
2026-04-16 14:23:37 +08:00
|
|
|
|
|
|
|
|
|
|
candidates = [
|
2026-03-19 16:00:40 +08:00
|
|
|
|
r"C:\Program Files (x86)\NSIS\makensis.exe",
|
|
|
|
|
|
r"C:\Program Files\NSIS\makensis.exe",
|
|
|
|
|
|
]
|
2026-04-16 14:23:37 +08:00
|
|
|
|
makensis = next((p for p in candidates if Path(p).exists()), None)
|
2026-03-19 16:00:40 +08:00
|
|
|
|
if not makensis:
|
2026-04-16 14:23:37 +08:00
|
|
|
|
result = subprocess.run("where makensis", shell=True, capture_output=True)
|
2026-03-19 16:00:40 +08:00
|
|
|
|
if result.returncode == 0:
|
2026-04-16 14:23:37 +08:00
|
|
|
|
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]
|
|
|
|
|
|
|
2026-03-19 16:00:40 +08:00
|
|
|
|
if not makensis:
|
2026-04-16 14:23:37 +08:00
|
|
|
|
print(" [SKIP] NSIS not found (makensis.exe).")
|
2026-03-19 16:00:40 +08:00
|
|
|
|
return
|
2026-04-16 14:23:37 +08:00
|
|
|
|
|
|
|
|
|
|
cmd = [makensis, str(NSIS_SCRIPT)]
|
2026-03-19 16:00:40 +08:00
|
|
|
|
result = run_command(cmd, check=False)
|
|
|
|
|
|
if result.returncode == 0:
|
2026-04-16 14:23:37 +08:00
|
|
|
|
print(" - NSIS build completed")
|
2026-03-19 16:00:40 +08:00
|
|
|
|
installer = PROJECT_DIR / "GPCT-standalone-Setup.exe"
|
|
|
|
|
|
if installer.exists():
|
2026-04-16 14:23:37 +08:00
|
|
|
|
print(f" - Installer: {installer}")
|
2026-03-19 16:00:40 +08:00
|
|
|
|
else:
|
2026-04-16 14:23:37 +08:00
|
|
|
|
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
|
2026-03-19 16:00:40 +08:00
|
|
|
|
else:
|
2026-04-16 14:23:37 +08:00
|
|
|
|
print(f" [ERROR] Executable not found: {onedir_exe} or {onefile_exe}")
|
2026-03-19 16:00:40 +08:00
|
|
|
|
return False
|
|
|
|
|
|
|
2026-04-16 14:23:37 +08:00
|
|
|
|
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:
|
2026-03-19 16:00:40 +08:00
|
|
|
|
print("=" * 50)
|
2026-04-16 14:23:37 +08:00
|
|
|
|
print(" GPCT-standalone Build Script")
|
2026-03-19 16:00:40 +08:00
|
|
|
|
print("=" * 50)
|
2026-04-16 14:23:37 +08:00
|
|
|
|
|
2026-03-19 16:00:40 +08:00
|
|
|
|
if not PROJECT_DIR.exists():
|
2026-04-16 14:23:37 +08:00
|
|
|
|
print(f"[ERROR] Project dir not found: {PROJECT_DIR}")
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
2026-03-19 16:00:40 +08:00
|
|
|
|
clean_build()
|
|
|
|
|
|
update_version()
|
|
|
|
|
|
build_with_pyinstaller()
|
|
|
|
|
|
copy_additional_files()
|
|
|
|
|
|
build_nsis_installer()
|
|
|
|
|
|
success = verify_build()
|
2026-04-16 14:23:37 +08:00
|
|
|
|
|
2026-03-19 16:00:40 +08:00
|
|
|
|
print("\n" + "=" * 50)
|
|
|
|
|
|
if success:
|
2026-04-16 14:23:37 +08:00
|
|
|
|
print(" Build completed")
|
|
|
|
|
|
print(f" Output: {DIST_DIR / 'GPCT-standalone'}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
print(" Build failed")
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
2026-03-19 16:00:40 +08:00
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
sys.exit(main())
|
2026-04-16 14:23:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|