Unity cross-platform local build orchestrator

July 12, 2026 · View on GitHub

Unity License: MIT Platforms PRs welcome Stars

One PowerShell command on your Windows PC builds and ships your Unity game to four platforms — Windows (Steam), Android (Google Play), iOS (TestFlight), and macOS (Steam) — using a Mac on the same LAN over SSH. Plus a WebGL-only submodule mode for OSS demos that ship to GitHub Pages. Open-sourced as part of a small giving-back set of Unity tools — alongside the UI Toolkit design system, the Voronoi mesh fracturer, the 3D-to-sprite baker, and the prefab-thumbnail renderer.

Leap of Legends Built for Leap of Legends, a cross-platform multiplayer game now live with cross-play on Steam, iOS, and Android, all built and shipped with this orchestrator. Play now on Steam, the App Store, and Google Play.

Using an AI coding assistant? This repo ships an AGENTS.md and an llms.txt so Copilot, Cursor, Codex, and Claude Code run and edit the scripts safely — respecting the Windows-PowerShell-5.1 / macOS-bash split and the "secrets never in the repo" rule.


========================================================================
  Leap of Legends build orchestrator
========================================================================
  [OK]   Config loaded
  [OK]   Unity: C:\Program Files\Unity\Hub\Editor\6000.3.8f1\Editor\Unity.exe
  [OK]   Keystore
  [OK]   SteamCMD present
  [OK]   Python + Play credentials present
  [OK]   D: has 2603 GB free
  [OK]   Mac reachable: Darwin 15.6

  Version bump:      patch
  Steam branch:      closed_testing
  Android:           build + Play internal
  Windows:           build + Steam [closed_testing]
  iOS:               build append + TestFlight
  macOS:             build + Steam [closed_testing]

  [/]   Android  total 02:34  |  Building Gradle project [00:45]
  [OK]   AAB: ...\YourGameAndroidBuild\YourGame.aab  (116.8 MB)
  Upload progress: 17% 34% 51% 68% 85%
  [OK]   Play Store: uploaded to 'internal' track.

  [\]   Windows  total 03:12  |  Incremental Player Build [01:08]
  [OK]   Windows build  (277.9 MB)
  [OK]   Steam (Windows) upload complete.

  [OK]   Committed + pushed: build 0.1.51 (code 52)

  [|]   iOS archive  total 05:14  |  compiling 688/1110 (62%)
  [OK]   iOS archive done
  [OK]   iOS export done
  [OK]   TestFlight: uploaded YourGame.ipa
  [OK]   macOS .app  (312.4 MB)
  [OK]   Steam (macOS) upload complete.

========================================================================
  All done in 23 min 14 sec
========================================================================

Contents

Why this exists

Shipping a cross-platform Unity game means repeating a ~10-step ritual every release: bump version, build Windows, build Android, upload AAB, build macOS on the Mac, build iOS on the Mac, archive in Xcode, upload to TestFlight, upload macOS to Steam, commit, push. One missed step and your platforms diverge.

This tool does the whole ritual with one command, in parallel where possible, with defensive preflight checks so you catch a missing keystore or an unreachable Mac before you wait 15 minutes for IL2CPP to finish.

It's deliberately local — no CI/CD, no cloud runners, no paid minutes, no opaque build logs in someone else's UI. Your Unity editor, your Mac, your LAN, your terminal.

What it does

  1. Preflight — validates Unity, keystore, SteamCMD, Python, SSH reachability, disk space.
  2. Bump versionbundleVersion +patch, AndroidBundleVersionCode +1, iOS/macOS build numbers synced.
  3. Android AAB → uploaded to Google Play Internal Testing via the Play Publishing API (service account).
  4. Windows build → uploaded to Steam via SteamCMD (branch configurable).
  5. Git commit + push of the version bump.
  6. SSH into the Mac → git hard-reset → build_mac.sh:
    • iOS build (append mode preserves your Xcode project customisations) → xcodebuild archivexcrun altool --upload-app to TestFlight.
    • macOS build (universal x64 + arm64) → uploaded to Steam via SteamCMD.
  7. Summary — every artefact path, upload status, total time.

All output is colour-coded and progress-animated — real phase names from Unity's DisplayProgressbar: markers and IL2CPP's [N/M elapsed] compile counter, not an indeterminate hourglass.

WebGL submodule mode (OSS demos → GitHub Pages)

If you have a single-platform WebGL demo — like an OSS Unity package's build → serve → deploy to gh-pages loop — you don't need keystores, SteamCMD, or a Mac. Drop this repo in as a git submodule and call Build-WebGL.ps1 from a 12-line shim.

# In your OSS repo:
git submodule add https://github.com/sinanata/unity-cross-platform-local-build-orchestrator Tools/.orchestrator

Your repo's Tools/Build/Build-Demo.ps1 becomes a forwarding shim:

#Requires -Version 5.1
param([Parameter(ValueFromRemainingArguments=$true)]$Args)
$ErrorActionPreference = "Stop"
$RepoRoot   = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$ConfigPath = Join-Path $PSScriptRoot "config.local.json"
$Orch       = Join-Path $RepoRoot "Tools\.orchestrator\Tools\Build\Build-WebGL.ps1"
& $Orch -Title "Your Demo - WebGL Build" `
        -UnityMethod "YourNamespace.BuildTools.BuildCli.BuildWebGL" `
        -LiveUrl "https://you.github.io/your-repo/" `
        -RepoRoot $RepoRoot -ConfigPath $ConfigPath @Args

The orchestrator handles: stale-lockfile cleanup, process-tree kill on Ctrl+C, live progress display, native-crash labelling, Burst-cache auto-retry, JSON build report, optional npx serve smoke test, optional gh-pages force-push (single-commit orphan branch via worktree). Same hardening as the multi-platform flow, scoped to WebGL.

Every consumer keeps its own Assets/Editor/BuildCli.cs (project-local: scene path, WebGL template name, namespace) and its own Tools/Build/config.local.json (gitignored: Unity path, build dir, deploy creds).

Reference consumers using this submodule:

# Daily usage from a consumer:
.\Tools\Build\Build-Demo.ps1                # build only
.\Tools\Build\Build-Demo.ps1 -Serve         # build + npx serve for smoke test
.\Tools\Build\Build-Demo.ps1 -Deploy        # build + force-push to gh-pages
.\Tools\Build\Build-Demo.ps1 -Deploy -Yes   # CI-style (no prompts)
.\Tools\Build\Build-Demo.ps1 -ClearCache    # nuke Library/BurstCache + Bee + Temp
.\Tools\Build\Build-Demo.ps1 -DryRun        # print every command, execute nothing

Build-WebGL.ps1 parameters:

-Title           Banner shown at startup ("Your Demo - WebGL Build")
-UnityMethod     Fully-qualified static method (-executeMethod target)
-LiveUrl         Public URL printed in the summary when -Deploy is used
-RepoRoot        Consumer's repo root (where Library/, Temp/, .git live)
-ConfigPath      Path to consumer's config.local.json
-Serve           After build, npx serve build/WebGL on the configured port
-Deploy          After build, push artefacts to gh-pages (force, single-commit)
-ClearCache      Wipe Library/BurstCache, Library/Bee, ScriptAssemblies, Temp
-DryRun          Print every command without executing it
-Yes             Skip the plan-confirmation + force-push prompts

The consumer's config.local.json for WebGL mode is small:

{
    "unity": {
        "windowsEditorPath": "C:\\Program Files\\Unity\\Hub\\Editor\\6000.3.8f1\\Editor\\Unity.exe",
        "clearCacheBeforeBuild": false
    },
    "paths": { "buildDir": "build/WebGL" },
    "deploy": {
        "remoteName": "origin",
        "branchName": "gh-pages",
        "commitMessage": "deploy demo ({sha})"
    },
    "serve": { "port": 3000 }
}

Requirements

Windows PC

RequirementNotes
Unity with Android + Windows Standalone modulesUnity Hub → Installs → your version → Add Modules
PowerShell 5.1+Ships with Windows 10/11
SteamCMDDefault path: C:\steamcmd\steamcmd.exe (configurable)
Python 3.8+Only needed for Play Store upload
pip install google-auth google-api-python-clientOnly needed for Play Store upload
OpenSSH clientBuilt into Windows 10/11 (Settings → Apps → Optional features → OpenSSH Client)
gitIn PATH

Mac (for iOS + macOS builds)

RequirementNotes
Unity with iOS + macOS Standalone modulesSame version as Windows
Xcode + command-line toolsxcode-select --install
Apple Developer Account signed in to XcodePreferences → Accounts
SteamCMDDefault path: ~/Steam/steamcmd
jqbrew install jq
Remote Login: ONSystem Settings → General → Sharing → Remote Login
Same git repo clonedMust contain this orchestrator + your Unity project

First-time project requirements

  • Your Unity project is a git repo that both machines can clone (GitHub, GitLab, self-hosted — whatever).
  • You have publisher accounts for the stores you want to ship to (Steam partner, Google Play developer, Apple Developer Program). Each is a paid membership with its own setup — see docs/CREDENTIALS.md for the step-by-step.

Installation

The orchestrator files live in two folders plus one Unity Editor script:

your-unity-project/
├── Assets/
│   └── Editor/
│       └── BuildCli.cs                 ← drop in
├── Tools/
│   ├── Build/                          ← drop in
│   │   ├── Build-All.ps1
│   │   ├── build_mac.sh
│   │   ├── upload_play.py
│   │   ├── check_play_auth.py
│   │   └── config.example.json
│   └── Steam/                          ← drop in
│       ├── deploy.ps1
│       └── deploy_mac.sh
└── .gitignore                          ← add the entries from this repo's .gitignore

Option A — copy files into your project:

# From your Unity project root, on Windows:
git clone https://github.com/sinanata/unity-cross-platform-local-build-orchestrator ..\orchestrator-src
xcopy /E /I ..\orchestrator-src\Assets\Editor\BuildCli.cs Assets\Editor\
xcopy /E /I ..\orchestrator-src\Tools Tools\
Copy-Item ..\orchestrator-src\.gitignore .gitignore_orchestrator  # review and merge

Option B — git submodule (recommended if you plan to pull updates):

cd your-unity-project
git submodule add https://github.com/sinanata/unity-cross-platform-local-build-orchestrator Tools/.orchestrator
# Then symlink or copy the pieces into place; submodule keeps them versioned.

Commit + push so the Mac can pull the same layout.

Quick start

After installation and credential gathering (see below):

# From your Unity project root
Copy-Item Tools\Build\config.example.json Tools\Build\config.local.json
notepad Tools\Build\config.local.json        # fill in values — see CREDENTIALS.md

# Same on the Mac (from your Mac terminal):
cp Tools/Build/config.example.json Tools/Build/config.local.json
# edit with your preferred editor

# First run:
.\Tools\Build\Build-All.ps1 -Bump patch

The orchestrator will walk you through any preflight failures before it starts the slow work.

Credential gathering

This is the part that costs a day the first time. Full step-by-step in docs/CREDENTIALS.md — covers:

  • Android keystore (Unity → Keystore Manager → where the password goes in config)
  • Google Play service account JSON (Cloud Console → Play Console linking → app-level permission → .json download → pip install → first upload limitation)
  • Apple App Store Connect API key (.p8 download, Issuer ID, Key ID, Team ID, path on Mac)
  • Steam partner account + SteamCMD (first-time Steam Guard dance, branch setup, depot layout)
  • Mac headless codesign (login keychain password, why xcodebuild fails with errSecInternalComponent over SSH and how to fix it)
  • SSH key-based auth (Windows → Mac, Mac → your git remote, macOS TCC / Full Disk Access for sshd)

Daily usage

# Patch-bump and ship everything to closed testing
.\Tools\Build\Build-All.ps1

# Minor-bump with a description shown in Steam + TestFlight
.\Tools\Build\Build-All.ps1 -Bump minor -Description "Endless mode + cosmetics"

# Quick Windows+Android hotfix (skip Mac entirely)
.\Tools\Build\Build-All.ps1 -SkipiOS -SkipMacOS

# Dry run — see every command, execute nothing
.\Tools\Build\Build-All.ps1 -DryRun

# Build everything but don't upload (local QA pass)
.\Tools\Build\Build-All.ps1 -SkipStoreUploads

# Ship to Steam's public 'default' branch for a release
.\Tools\Build\Build-All.ps1 -Branch default -Description "v0.2.0"

# Re-upload current version without bumping (e.g. after a VDF tweak)
.\Tools\Build\Build-All.ps1 -SkipVersionBump

All flags

-Bump              none | patch | minor | major     (default: patch)
-Branch            Steam branch                      (default: closed_testing)
-Description       "Free-form build note"            (applied to Steam + Play release notes)
-SkipAndroid       Skip Android build + Play upload
-SkipWindows       Skip Windows build + Steam upload
-SkipiOS           Skip iOS build + TestFlight upload
-SkipMacOS         Skip macOS build + Steam upload
-SkipStoreUploads  Keep artefacts local; upload nothing
-SkipVersionBump   Reuse current version
-SkipCommit        Don't git-commit the bump
-SkipPush          Don't git-push (Mac will fail to pull — use only for local tests)
-DryRun            Print every command; execute nothing
-Yes               Skip the initial confirmation prompt
-ClearCache        Nuke Library/BurstCache + Bee + ScriptAssemblies + Temp
                   before building (full reimport). Use to recover from
                   stale-cache issues like NetCode RPC wire-format desyncs.
                   Persistent alternative: unity.clearCacheBeforeBuild=true
                   in config.local.json.
-ConfigPath path   Use an alternate config.*.json

What makes this robust

Every run verifies these before touching Unity:

  • Config parses as JSON; all required fields present.
  • Unity editor path exists on Windows (and on Mac inside build_mac.sh).
  • Keystore file exists; password + alias are set.
  • SteamCMD is installed when Steam upload is planned.
  • Python + Play JSON are present when Play upload is planned.
  • 10+ GB free on the build drive (warning below that threshold).
  • Mac reachable with a 6-second SSH probe. Fails fast with a fix-checklist if Remote Login isn't on yet.
  • Sleeping Mac auto-wake — if the preflight TCP probe times out, the orchestrator sends a Wake-on-LAN magic packet to mac.macAddress and polls SSH until it responds (configurable timeout). Once the build starts on the Mac, caffeinate -dims -w $$ keeps it awake for the full run and exits with the shell. Works on Wi-Fi with a couple of caveats — see TROUBLESHOOTING.md § Mac is asleep.
  • BuildCli.cs writes a JSON report after every Unity step; orchestrator aborts if success != true.
  • Clean-Dir refuses to delete a path shorter than 8 characters or a bare drive root — protects you from a typo like windowsBuildDir: "D:\".
  • Android keystore password is wiped from in-memory PlayerSettings after the AAB build so it can never leak into ProjectSettings.asset.
  • Mac is a pure build slave — the SSH step does git fetch && git reset --hard @{u} && git clean -fd on the Mac before building, so a stray Unity-touched file can never block a pull.

Architecture

┌────────────────────────────────────────────┐
│ Windows PC                                 │
│ ┌────────────────────────────────────────┐ │
│ │  Tools/Build/Build-All.ps1             │ │
│ │   • preflight + confirmation           │ │
│ │   • Unity -batchmode (Android, Win)    │ │
│ │   • upload_play.py                     │ │
│ │   • Tools/Steam/deploy.ps1             │ │
│ │   • git commit + push                  │ │
│ └────────────────────┬───────────────────┘ │
└──────────────────────┼─────────────────────┘
                       │ ssh -t

┌────────────────────────────────────────────┐
│ MacBook                                    │
│ ┌────────────────────────────────────────┐ │
│ │  Tools/Build/build_mac.sh              │ │
│ │   • git reset --hard origin            │ │
│ │   • Unity -batchmode (iOS, OSXUniv.)   │ │
│ │   • security unlock-keychain           │ │
│ │   • xcodebuild archive + exportArchive │ │
│ │   • xcrun altool --upload-app          │ │
│ │   • Tools/Steam/deploy_mac.sh          │ │
│ └────────────────────────────────────────┘ │
└────────────────────────────────────────────┘

Tools/Build/Build-All.ps1 — the Windows entry point and single source of truth. Reads config.local.json, runs preflight, drives Unity, pushes to stores, SSHes into the Mac.

Tools/Build/build_mac.sh — the Mac-side twin. Invoked only over SSH by the orchestrator (or manually if you prefer). Reads the Mac's copy of config.local.json.

Assets/Editor/BuildCli.cs — Unity batchmode entry points. Exposes BuildOrchestrator.Cli.BuildCli.{Windows, MacOS, iOS, Android, BumpVersion, PrintVersion}. Reads scenes from Unity's EditorBuildSettings, so whatever you configured in File → Build Settings is what ships.

Tools/Build/upload_play.py — Google Play Publishing API v3 client. Resumable upload, configurable track, auto-falls-back to status=draft if the app is in draft state (fresh apps reject status=completed until their first review).

Tools/Steam/deploy.ps1 / deploy_mac.sh — SteamCMD wrappers. VDFs generated on the fly from the app/depot IDs in your config, so no committed files contain your Steam app ID.

Troubleshooting

See docs/TROUBLESHOOTING.md. Covers:

  • SSH probe fails
  • macOS SSH can't read ~/Documents (TCC / Full Disk Access)
  • xcodebuild errSecInternalComponent
  • Play API 401 / 403 / SERVICE_DISABLED
  • "App is in draft state" on first Play upload
  • Unity "license already in use" / batchmode collisions
  • Steam Guard prompting every run
  • Mac git pull rejecting modified/untracked files

Security

  • Tools/Build/config.local.json contains your keystore password, Mac login password, Steam username. Never commit it (gitignored already).
  • Store the Play service account JSON and Apple .p8 outside the repo. Paths in config point to external locations.
  • The orchestrator never logs the values of -cliKeystorePass, -cliKeyaliasPass, or keychainPassword.
  • BuildCli.cs wipes PlayerSettings.Android.keystorePass / keyaliasPass in a finally block so even a crashing build can't leave the password in ProjectSettings.asset.
  • All SSH traffic is key-based — no passwords sent over the wire.

Contributing

Issues and PRs welcome. The tool is intentionally small (~1500 lines total across PowerShell, bash, Python, and C#) — readable in an afternoon, hackable in a weekend.

If you run into a store API quirk this tool doesn't handle, the fix usually lives in one of:

  • upload_play.py — Play Publishing API
  • build_mac.sh — xcodebuild + altool + codesign
  • Tools/Steam/* — SteamCMD + VDF generation

Credits & support

Made for Leap of Legends, a cross-platform physics-heavy multiplayer game now live with cross-play on Steam, iOS, Android, and Mac. If this tool saved you time:

Licence

MIT — see LICENSE. Free for commercial use. No warranty.


Leap of Legends · physics · multiplayer · cross-platform · in development · built weekly with this tool.