#!/usr/bin/env bash # Export a single platform via Godot headless. # # Usage: tools/export-single.sh [version] [--debug] # platform: macos | linux | windows | android | ios # version: defaults to YYYYMMDD_HHMMSS # --debug: debug build (default: release) # # Output: .local/build/godot/// # # Requires: Godot editor CLI on PATH (`godot` on macOS, `flatpak run --user org.godotengine.Godot` on Linux), # and export presets configured in src/game/export_presets.cfg. # # This script is the lowest-level single-platform export. `tools/export.sh` calls it in parallel # across all platforms. # # ── Staging (scan-inflation fix, p2-06) ─────────────────────────────── # Godot's export scanner walks the ENTIRE project tree before applying # `exclude_filter`. The pnpm-managed guides (`public/games/*/guide/`) # each carry a symlinked `node_modules/` pointing at the hoisted store, # which balloons the scan to 20+ min on macOS and emits ~16MB of # `_scan_new_dir` warnings. To sidestep this, we rsync the project # into `.local/export-staging-/` excluding node_modules, # .local/build, target, .git, and run godot against the staged tree. # Artifacts still land in REPO_ROOT/.local/build/godot///. # Controlled by EXPORT_STAGED (default: 1 on macOS, 0 elsewhere). # Staging dir is cleaned on success unless KEEP_STAGING=1. set -uo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' DIM='\033[2m' NC='\033[0m' REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" GAME_DIR="$REPO_ROOT/src/game" PRESETS_FILE="$GAME_DIR/export_presets.cfg" platform="${1:-}" version="" debug=false shift 2>/dev/null || true for arg in "$@"; do case "$arg" in --debug) debug=true ;; *) version="$arg" ;; esac done version="${version:-$(date +%Y%m%d_%H%M%S)}" if [ -z "$platform" ]; then echo -e "${RED}Usage: $0 [version] [--debug]${NC}" echo " platforms: macos | linux | windows | android | ios" exit 1 fi # ── Preset & artifact resolution per platform ──────────────────────── # Preset names follow the convention "" matching export_presets.cfg entries. case "$platform" in macos) preset_name="macOS" artifact="MagicCivilization.zip" ;; linux) preset_name="Linux/X11" artifact="MagicCivilization.x86_64" ;; windows) preset_name="Windows Desktop" artifact="MagicCivilization.exe" ;; android) preset_name="Android" artifact="MagicCivilization.apk" ;; ios) preset_name="iOS" artifact="MagicCivilization.xcodeproj" # Godot emits an Xcode project, not an ipa ;; *) echo -e "${RED}Unknown platform: $platform${NC}" echo "Supported: macos, linux, windows, android, ios" exit 1 ;; esac # ── Preset existence check ─────────────────────────────────────────── if [ ! -f "$PRESETS_FILE" ]; then echo -e "${RED}Missing $PRESETS_FILE${NC}" echo "" echo -e "${YELLOW}Export presets have not been authored yet.${NC} To create them:" echo " 1. Open Godot editor: ./run editor" echo " 2. Project → Export..." echo " 3. Add presets for: macOS, Linux/X11, Windows Desktop, Android, iOS" echo " 4. Save (Godot writes export_presets.cfg to $GAME_DIR/)" echo " 5. Commit the file" echo " 6. Re-run: ./run install:$platform" exit 2 fi if ! grep -q "^name=\"$preset_name\"" "$PRESETS_FILE" 2>/dev/null; then echo -e "${RED}Preset \"$preset_name\" not found in $PRESETS_FILE${NC}" echo -e "${YELLOW}Open Godot → Project → Export → add a preset named exactly \"$preset_name\".${NC}" exit 2 fi # ── Godot binary resolution ────────────────────────────────────────── # Prefer $GODOT_BIN if set (set by scripts/run/common.sh on macOS). # Fall back to OS default. if [ -n "${GODOT_BIN:-}" ]; then godot_cmd="$GODOT_BIN" elif command -v godot &>/dev/null; then godot_cmd="godot" elif command -v flatpak &>/dev/null && flatpak info --user org.godotengine.Godot &>/dev/null; then godot_cmd="flatpak run --user org.godotengine.Godot" elif command -v flatpak &>/dev/null && flatpak info org.godotengine.Godot &>/dev/null; then godot_cmd="flatpak run org.godotengine.Godot" else echo -e "${RED}No Godot binary found. Run: ./run setup${NC}" exit 3 fi # ── Output path ────────────────────────────────────────────────────── out_dir="$REPO_ROOT/.local/build/godot/$version/$platform" out_path="$out_dir/$artifact" mkdir -p "$out_dir" # ── Export flag ────────────────────────────────────────────────────── if $debug; then flag="--export-debug" mode_label="debug" else flag="--export-release" mode_label="release" fi # ── Staging (p2-06) ────────────────────────────────────────────────── # Default on for macOS to avoid the symlinked-node_modules scan storm. # Opt-in on other platforms via EXPORT_STAGED=1. if [ -z "${EXPORT_STAGED:-}" ]; then case "$platform" in macos) EXPORT_STAGED=1 ;; *) EXPORT_STAGED=0 ;; esac fi # ── Pull fresh Linux .so from apricot before Linux export (p2-06) ──── # The Mac EDIT host can't build the Linux .so; apricot builds it. Without # this rsync the Mac may bundle a stale .so → "Cannot get class 'GdEconomy'" # at runtime. Skipped if PULL_LINUX_SO=0 or apricot is unreachable. if [ "$platform" = "linux" ] && [ "${PULL_LINUX_SO:-1}" = "1" ]; then so_local="$GAME_DIR/engine/addons/magic_civ_physics/libmagic_civ_physics.x86_64.so" if command -v rsync &>/dev/null && ssh -o ConnectTimeout=3 -o BatchMode=yes apricot true 2>/dev/null; then echo -e "${BLUE}Pulling fresh libmagic_civ_physics.x86_64.so from apricot${NC}" rsync -a apricot:Code/@projects/@magic-civilization/src/game/engine/addons/magic_civ_physics/libmagic_civ_physics.x86_64.so "$so_local" 2>/dev/null \ && echo -e "${DIM} · pulled $(du -h "$so_local" | cut -f1)${NC}" \ || echo -e "${YELLOW} · pull failed; using local copy ($(du -h "$so_local" 2>/dev/null | cut -f1))${NC}" else echo -e "${YELLOW}apricot unreachable — using local Linux .so (may be stale; set PULL_LINUX_SO=0 to silence)${NC}" fi fi # ── Build Windows .dll via cargo-xwin (p2-06b Option B) ────────────── # Cross-compile the GDExtension to MSVC ABI from Linux. cargo-xwin # downloads the MS SDK on first invocation (~1.5GB cached). Disable with # BUILD_WINDOWS_DLL=0 if a fresh dll is already in place from CI. if [ "$platform" = "windows" ] && [ "${BUILD_WINDOWS_DLL:-1}" = "1" ]; then echo -e "${BLUE}Building Windows .dll via cargo-xwin (MSVC ABI, x86_64-pc-windows-msvc)${NC}" if ! (cd "$REPO_ROOT/src/simulator" && bash build-gdext.sh x86_64-pc-windows-msvc); then echo -e "${RED} ✗ cargo-xwin build failed — see output above${NC}" echo -e "${YELLOW} Prerequisites: rustup target add x86_64-pc-windows-msvc; cargo install cargo-xwin; clang + lld${NC}" exit 9 fi dll_local="$GAME_DIR/engine/addons/magic_civ_physics/magic_civ_physics.x86_64.dll" if [ -f "$dll_local" ]; then echo -e "${DIM} · .dll at $(du -h "$dll_local" | cut -f1) → $dll_local${NC}" fi fi export_game_dir="$GAME_DIR" staging_root="" if [ "$EXPORT_STAGED" = "1" ]; then if ! command -v rsync &>/dev/null; then echo -e "${RED}EXPORT_STAGED=1 requires rsync${NC}" exit 6 fi stamp="$(date +%Y%m%d_%H%M%S)_$$" staging_root="$REPO_ROOT/.local/export-staging-$stamp" staged_game_dir="$staging_root/src/game" echo -e "${BLUE}Staging project → $staging_root (excluding node_modules, .local/build, target, .git)${NC}" mkdir -p "$staging_root" # -a preserves symlinks; --copy-unsafe-links would dereference out-of-tree # symlinks (the node_modules targets live in the repo root's hoisted store # OUTSIDE the staging copy, so they'd be dereferenced — which is exactly # what inflates the scan). --no-links drops symlinks entirely, but the # .gdextension addons under src/game/addons/ are real files, so -a is fine. # Key: --exclude on node_modules stops those symlinks from being copied # at all, so the staged tree has no node_modules to scan. rsync -a \ --exclude='.git' \ --exclude='.local' \ --exclude='node_modules' \ --exclude='target' \ --exclude='dist' \ --exclude='.vite' \ --exclude='.vite-temp' \ --exclude='*.log' \ --exclude='.godot' \ "$REPO_ROOT/" "$staging_root/" || { echo -e "${RED}rsync to staging failed${NC}" rm -rf "$staging_root" exit 7 } # Also need the compiled GDExtension under addons — sits outside .local # (it's at src/game/addons/magic_civ_physics/) so it came across via rsync. # Godot imports its own .godot/ cache on first run; point it at a writable # location inside staging so we don't touch the real src/game/.godot/. export_game_dir="$staged_game_dir" if [ ! -f "$export_game_dir/project.godot" ]; then echo -e "${RED}Staged project.godot missing at $export_game_dir — staging failed${NC}" rm -rf "$staging_root" exit 8 fi fi echo -e "${BLUE}Exporting $platform ($mode_label) → $out_path${NC}" echo -e "${DIM}preset: $preset_name${NC}" echo -e "${DIM}godot: $godot_cmd${NC}" [ -n "$staging_root" ] && echo -e "${DIM}staged: $export_game_dir${NC}" cleanup_staging() { if [ -n "$staging_root" ] && [ -d "$staging_root" ] && [ "${KEEP_STAGING:-0}" != "1" ]; then rm -rf "$staging_root" echo -e "${DIM}cleaned staging: $staging_root${NC}" elif [ -n "$staging_root" ] && [ "${KEEP_STAGING:-0}" = "1" ]; then echo -e "${DIM}staging kept (KEEP_STAGING=1): $staging_root${NC}" fi } # ── Run the export ─────────────────────────────────────────────────── # shellcheck disable=SC2086 if $godot_cmd --headless --path "$export_game_dir" "$flag" "$preset_name" "$out_path" 2>&1; then if [ -e "$out_path" ]; then size="$(du -h "$out_path" 2>/dev/null | cut -f1)" echo -e "${GREEN} ✓ Exported $artifact ($size)${NC}" # ── Relocate GDExtension .so into the addon subdir for Linux ─── # Godot's Linux export drops `lib*.so` next to the binary, but the # .gdextension references `res://engine/addons/magic_civ_physics/...` # which the runtime resolves to that exact relative path next to the # executable. Without the move, the library is never loaded and # Gd* classes never register. if [ "$platform" = "linux" ]; then so_root="$out_dir/libmagic_civ_physics.x86_64.so" so_addon_dir="$out_dir/engine/addons/magic_civ_physics" if [ -f "$so_root" ]; then mkdir -p "$so_addon_dir" mv "$so_root" "$so_addon_dir/" echo -e "${DIM} · relocated .so → engine/addons/magic_civ_physics/${NC}" fi fi if [ "$platform" = "windows" ]; then dll_root="$out_dir/magic_civ_physics.x86_64.dll" dll_addon_dir="$out_dir/engine/addons/magic_civ_physics" if [ -f "$dll_root" ]; then mkdir -p "$dll_addon_dir" mv "$dll_root" "$dll_addon_dir/" echo -e "${DIM} · relocated .dll → engine/addons/magic_civ_physics/${NC}" fi fi cleanup_staging exit 0 else echo -e "${RED} ✗ Export reported success but $out_path is missing${NC}" cleanup_staging exit 4 fi else echo -e "${RED} ✗ Godot export failed${NC}" cleanup_staging exit 5 fi