2026-04-17 01:15:00 -07:00
|
|
|
#!/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.
|
2026-04-17 23:24:54 -07:00
|
|
|
#
|
|
|
|
|
# ── 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.
|
2026-04-17 01:15:00 -07:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-04-17 23:24:54 -07:00
|
|
|
# ── 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
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-04-17 01:15:00 -07:00
|
|
|
echo -e "${BLUE}Exporting $platform ($mode_label) → $out_path${NC}"
|
|
|
|
|
echo -e "${DIM}preset: $preset_name${NC}"
|
|
|
|
|
echo -e "${DIM}godot: $godot_cmd${NC}"
|
2026-04-17 23:24:54 -07:00
|
|
|
[ -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
|
|
|
|
|
}
|
2026-04-17 01:15:00 -07:00
|
|
|
|
|
|
|
|
# ── Run the export ───────────────────────────────────────────────────
|
|
|
|
|
# shellcheck disable=SC2086
|
2026-04-17 23:24:54 -07:00
|
|
|
if $godot_cmd --headless --path "$export_game_dir" "$flag" "$preset_name" "$out_path" 2>&1; then
|
2026-04-17 01:15:00 -07:00
|
|
|
if [ -e "$out_path" ]; then
|
|
|
|
|
size="$(du -h "$out_path" 2>/dev/null | cut -f1)"
|
|
|
|
|
echo -e "${GREEN} ✓ Exported $artifact ($size)${NC}"
|
2026-04-17 23:24:54 -07:00
|
|
|
cleanup_staging
|
2026-04-17 01:15:00 -07:00
|
|
|
exit 0
|
|
|
|
|
else
|
|
|
|
|
echo -e "${RED} ✗ Export reported success but $out_path is missing${NC}"
|
2026-04-17 23:24:54 -07:00
|
|
|
cleanup_staging
|
2026-04-17 01:15:00 -07:00
|
|
|
exit 4
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
echo -e "${RED} ✗ Godot export failed${NC}"
|
2026-04-17 23:24:54 -07:00
|
|
|
cleanup_staging
|
2026-04-17 01:15:00 -07:00
|
|
|
exit 5
|
|
|
|
|
fi
|