From eff4c36456c376671f9c52d70b730aa8d6b2f54c Mon Sep 17 00:00:00 2001 From: Nordup Date: Sat, 30 Aug 2025 20:40:09 +0700 Subject: [PATCH] deployment scripts --- deployment/export_project.py | 239 +++++++++++++++++++++++++++++++++ deployment/zip_builds_linux.py | 111 +++++++++++++++ 2 files changed, 350 insertions(+) create mode 100644 deployment/export_project.py create mode 100755 deployment/zip_builds_linux.py diff --git a/deployment/export_project.py b/deployment/export_project.py new file mode 100644 index 0000000..d54340d --- /dev/null +++ b/deployment/export_project.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +""" +Automated Godot export script. + +Behavior: +- On Linux: exports release build for preset "Linux/X11" (export project) + and exports release pack for preset "Windows Desktop" (export pck). +- On macOS: exports release build for preset "macOS" (export project). +- On Windows: exports release build for preset "Windows Desktop" (export project). + +All exports use the export paths defined in app/export_presets.cfg. +For pack export, the output .pck path is derived from the preset's export_path +by replacing ".exe" with ".pck" (or appending ".pck" if no extension found). + +Special case: when exporting Windows pack from Linux host, override output path to +"/media/common/Projects/thegates-folder/AppBuilds/Windows/TheGates.pck". + +Editor binaries to use: +- Linux: godot/bin/godot.linuxbsd.editor.dev.x86_64.llvm +- macOS: godot/bin/godot.macos.editor.dev.arm64 +- Windows: godot/bin/godot.windows.editor.dev.x86_64.exe + +Only release builds are exported (uses --export-release). +""" + +from __future__ import annotations + +import os +import platform +import shlex +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional + + +REPO_ROOT = Path(__file__).resolve().parent.parent +APP_DIR = REPO_ROOT / "app" +EXPORT_PRESETS_PATH = APP_DIR / "export_presets.cfg" + + +LINUX_EDITOR_RELATIVE = Path("godot/bin/godot.linuxbsd.editor.dev.x86_64.llvm") +MACOS_EDITOR_RELATIVE = Path("godot/bin/godot.macos.editor.dev.arm64") +WINDOWS_EDITOR_RELATIVE = Path("godot/bin/godot.windows.editor.dev.x86_64.exe") + +LINUX_WINDOWS_PCK_OVERRIDE_PATH = Path("/media/common/Projects/thegates-folder/AppBuilds/Windows/TheGates.pck") + + +@dataclass +class ExportPreset: + index: int + name: str + platform: Optional[str] + export_path: Optional[str] + + +def read_export_presets(cfg_path: Path) -> Dict[str, ExportPreset]: + """Parse Godot export_presets.cfg and return presets keyed by name. + + We only care about fields under [preset.N]: name, platform, export_path. + """ + if not cfg_path.exists(): + raise FileNotFoundError(f"export presets not found: {cfg_path}") + + presets: Dict[str, ExportPreset] = {} + current: Optional[ExportPreset] = None + + with cfg_path.open("r", encoding="utf-8") as f: + for raw_line in f: + line = raw_line.strip() + if not line: + continue + if line.startswith("["): + # Starting a new section. + if line.startswith("[preset.") and line.endswith("]"): + try: + idx_str = line[len("[preset.") : -1] + idx = int(idx_str) + except ValueError: + current = None + else: + current = ExportPreset(index=idx, name="", platform=None, export_path=None) + else: + # Any non-preset section ends current preset parsing + current = None + continue + + if current is None: + continue + + if line.startswith("name="): + current.name = line.split("=", 1)[1].strip().strip('"') + elif line.startswith("platform="): + current.platform = line.split("=", 1)[1].strip().strip('"') + elif line.startswith("export_path="): + current.export_path = line.split("=", 1)[1].strip().strip('"') + + # Once we have a name, keep it indexed so later lookups are easy + if current.name: + presets[current.name] = current + + return presets + + +def ensure_editor_binary() -> Path: + system = platform.system() + if system == "Linux": + editor_rel = LINUX_EDITOR_RELATIVE + elif system == "Darwin": + editor_rel = MACOS_EDITOR_RELATIVE + elif system == "Windows": + editor_rel = WINDOWS_EDITOR_RELATIVE + else: + raise RuntimeError(f"Unsupported OS for this script: {system}") + + editor_path = (REPO_ROOT / editor_rel).resolve() + if not editor_path.exists(): + raise FileNotFoundError( + f"Godot editor binary not found: {editor_path}\n" + f"Expected relative path: {editor_rel}" + ) + return editor_path + + +def run_cmd(cmd: List[str], cwd: Path) -> None: + print(f"Running: {shlex.join(cmd)}\n in: {cwd}") + subprocess.run(cmd, cwd=str(cwd), check=True) + + +def derive_pck_path_from_export_path(export_path: str) -> Path: + path = Path(export_path) + if path.suffix.lower() == ".exe": + return path.with_suffix(".pck") + if path.suffix: + # Replace any existing extension with .pck + return path.with_suffix(".pck") + return path.with_suffix(".pck") + + +def export_linux_and_windows_pack(editor: Path, presets: Dict[str, ExportPreset]) -> None: + # Linux project export (uses export path defined in preset) + linux_preset_name = "Linux/X11" + if linux_preset_name not in presets: + raise KeyError(f"Preset not found: {linux_preset_name}") + + run_cmd( + [str(editor), "--headless", "--export-release", linux_preset_name], + cwd=APP_DIR, + ) + + # Windows pack export (override .pck path per requirement when on Linux) + windows_preset_name = "Windows Desktop" + win_preset = presets.get(windows_preset_name) + if win_preset is None: + raise KeyError(f"Preset not found: {windows_preset_name}") + + # Use the override path instead of preset-defined path + pck_out_path = LINUX_WINDOWS_PCK_OVERRIDE_PATH + + # Ensure output directory exists for the pack + pck_out_dir = Path(pck_out_path).parent + try: + pck_out_dir.mkdir(parents=True, exist_ok=True) + except Exception: + # If directory is not creatable (e.g., Windows drive on Linux), let Godot handle/raise. + pass + + run_cmd( + [ + str(editor), + "--headless", + "--export-pack", + windows_preset_name, + str(pck_out_path), + ], + cwd=APP_DIR, + ) + + +def export_macos(editor: Path, presets: Dict[str, ExportPreset]) -> None: + mac_preset_name = "macOS" + if mac_preset_name not in presets: + raise KeyError(f"Preset not found: {mac_preset_name}") + + run_cmd( + [str(editor), "--headless", "--export-release", mac_preset_name], + cwd=APP_DIR, + ) + + +def export_windows(editor: Path, presets: Dict[str, ExportPreset]) -> None: + windows_preset_name = "Windows Desktop" + if windows_preset_name not in presets: + raise KeyError(f"Preset not found: {windows_preset_name}") + + run_cmd( + [str(editor), "--headless", "--export-release", windows_preset_name], + cwd=APP_DIR, + ) + + +def import_project(editor: Path) -> None: + # Ensure all assets are imported prior to export + run_cmd([str(editor), "--headless", "--import"], cwd=APP_DIR) + + +def main() -> int: + try: + presets = read_export_presets(EXPORT_PRESETS_PATH) + editor = ensure_editor_binary() + system = platform.system() + + # Always import project before any export + import_project(editor) + + if system == "Linux": + export_linux_and_windows_pack(editor, presets) + elif system == "Darwin": + export_macos(editor, presets) + elif system == "Windows": + export_windows(editor, presets) + else: + raise RuntimeError(f"Unsupported OS: {system}") + + print("All exports completed successfully.") + return 0 + except subprocess.CalledProcessError as e: + print(f"Export command failed with exit code {e.returncode}") + return e.returncode + except Exception as e: + print(f"Error: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) + + diff --git a/deployment/zip_builds_linux.py b/deployment/zip_builds_linux.py new file mode 100755 index 0000000..e523bea --- /dev/null +++ b/deployment/zip_builds_linux.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Zip builder for TheGates builds. + +Usage: + python3 zip_builds.py 0.17.2 + +Creates, in-place: + Linux/TheGates_Linux_.zip containing: TheGates.x86_64, sandbox/ + Windows/TheGates_Windows_.zip containing: TheGates.exe, TheGates.pck, sandbox/ + +By default, refuses to overwrite existing zip files. Use --force to overwrite. +""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path +from zipfile import ZipFile, ZIP_DEFLATED + + +def validate_version(version: str) -> None: + pattern = re.compile(r"^[0-9]+(\.[0-9]+){1,3}$") + if not pattern.match(version): + raise ValueError( + f"Invalid version '{version}'. Expected format like 0.17.2" + ) + + +def ensure_exists(path: Path) -> None: + if not path.exists(): + raise FileNotFoundError(f"Missing required path: {path}") + + +def zip_entries(base_dir: Path, entries: list[str], output_zip: Path, overwrite: bool) -> None: + if output_zip.exists(): + if not overwrite: + raise FileExistsError( + f"Output already exists: {output_zip}. Use --force to overwrite." + ) + output_zip.unlink() + + # Ensure base directory exists + ensure_exists(base_dir) + + with ZipFile(output_zip, mode="w", compression=ZIP_DEFLATED) as zf: + for entry_name in entries: + entry_path = base_dir / entry_name + ensure_exists(entry_path) + + if entry_path.is_file(): + # Store at top-level inside the archive + zf.write(entry_path, arcname=entry_name) + else: + # Walk directory and add files with relative paths rooted at base_dir + for file_path in entry_path.rglob("*"): + if file_path.is_file(): + arcname = file_path.relative_to(base_dir) + zf.write(file_path, arcname=str(arcname)) + + +def build_linux_zip(root: Path, version: str, overwrite: bool) -> Path: + linux_dir = root / "Linux" + output_zip = linux_dir / f"TheGates_Linux_{version}.zip" + entries = [ + "TheGates.x86_64", + "sandbox", + ] + zip_entries(linux_dir, entries, output_zip, overwrite) + return output_zip + + +def build_windows_zip(root: Path, version: str, overwrite: bool) -> Path: + windows_dir = root / "Windows" + output_zip = windows_dir / f"TheGates_Windows_{version}.zip" + entries = [ + "TheGates.exe", + "TheGates.pck", + "sandbox", + ] + zip_entries(windows_dir, entries, output_zip, overwrite) + return output_zip + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Zip Linux and Windows builds.") + parser.add_argument("version", help="App version, e.g. 0.17.2") + parser.add_argument( + "--force", + action="store_true", + help="Overwrite existing zip files if they exist.", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + validate_version(args.version) + + root = Path(__file__).resolve().parent + + linux_zip = build_linux_zip(root, args.version, args.force) + print(f"Created: {linux_zip}") + + windows_zip = build_windows_zip(root, args.version, args.force) + print(f"Created: {windows_zip}") + + +if __name__ == "__main__": + main()