magicciv/tools/export-single.sh
Natalie a5a232a660 feat(@projects/@magic-civilization): add warcouncil cycle-1 batch validation handoff
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-04-25 17:13:10 -07:00

258 lines
11 KiB
Bash
Executable file

#!/usr/bin/env bash
# Export a single platform via Godot headless.
#
# Usage: tools/export-single.sh <platform> [version] [--debug]
# platform: macos | linux | windows | android | ios
# version: defaults to YYYYMMDD_HHMMSS
# --debug: debug build (default: release)
#
# Output: .local/build/godot/<version>/<platform>/<artifact>
#
# 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-<stamp>/` excluding node_modules,
# .local/build, target, .git, and run godot against the staged tree.
# Artifacts still land in REPO_ROOT/.local/build/godot/<version>/<platform>/.
# 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 <platform> [version] [--debug]${NC}"
echo " platforms: macos | linux | windows | android | ios"
exit 1
fi
# ── Preset & artifact resolution per platform ────────────────────────
# Preset names follow the convention "<Platform>" 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
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' \
"$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
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