Mobile (Android & iOS)
Package your game as a native Android WebView app or iOS WKWebView app.
Early and experimental
Mobile support is newer and less polished than the desktop path — expect rough edges. Unlike desktop, mobile games don't run on the Servo engine at all: Android uses the system's own Android WebView, and iOS uses Apple's own WKWebView. Neither container loads Servo, so mobile web-platform support tracks whatever browser engine is installed on the device, not this engine's own Servo fork.
Nothing Servo-specific — including roves-api's save/Steam/process APIs — is implemented on mobile yet. Use standard web APIs, or your own platform bridge, until that changes.
Two standard web APIs are specifically intercepted so they behave correctly rather than just silently failing, even though nothing above is roves-api itself:
- Save export/import (
<a download>on a Blob,<input type="file">) works reliably on both platforms — see Save-game storage → letting a player export/import a save as a file. You don't need to detect mobile or do anything platform-specific; the same code that works on desktop works here too. - The Fullscreen API is intentionally a no-op. Both apps always run fullscreen/edge-to-edge
already — there's no windowed mode to toggle into or out of — so
requestFullscreen()/exitFullscreen()are neutralized into a harmless resolved promise instead of doing anything. Don't build a "toggle fullscreen" control for a mobile build; there's nothing for it to do.
Android
./mach bundle --android --content-dir /absolute/path/to/dist --output /absolute/path/to/android-outputThis needs Java 17 and the Android SDK (platform 37, build tools 36.0.0) with ANDROID_HOME
set — no Rust cross-compilation, no NDK, and no mach build --android first. Under the hood
it copies your content straight into a Gradle project's assets/ folder and assembles a
debug (or, with --android-release, a signed release) .apk. Your app name, screen
orientation, status bar color and launcher icon come from your build's own
manifest.webmanifest/manifest.json by default, or can be overridden directly:
| Flag | Overrides |
|---|---|
--android-app-name <name> | Launcher label. |
--android-orientation <value> | Locked screen orientation (any, natural, portrait, landscape, portrait-primary, portrait-secondary, landscape-primary, landscape-secondary). |
--android-theme-color <color> | Status bar color (e.g. #ffffff). |
--android-release | Build a signed release .apk instead of debug — needs APK_SIGNING_KEY_STORE_PATH/_STORE_PASS/_ALIAS/_PASS set in the environment. |
The game runs at a real origin (https://appassets.androidplatform.net/), not a raw
file:// path, so root-relative asset URLs and a client-side router's root route both work
correctly. A path with no matching file falls back to your index.html (so client-side
routing works); only a genuinely missing index.html itself 404s. The app always runs
edge-to-edge — the status bar and gesture/button navigation bar are hidden from the
first frame, reappearing only for a temporary edge-swipe before auto-hiding again — matching
what a player expects from a real mobile game, not a browser tab.
iOS
macOS only (Xcode/XcodeGen are Apple-only tools), but otherwise the same command shape as Android — no separate staging step needed anymore:
./mach bundle --ios --content-dir /absolute/path/to/dist --output /absolute/path/to/ios-output --ios-app-name "My Game" --ios-bundle-id com.example.mygameThis needs Xcode and XcodeGen installed. Under the
hood it stages a WKWebView Xcode project (via support/ios/bundle.py, still usable standalone
if you'd rather poke around in Xcode directly — see below), then runs xcodegen generate +
xcodebuild for you. By default this produces an unsigned iOS Simulator build — useful to
confirm your game stages and builds, not something installable on a device.
| Flag | Overrides |
|---|---|
--ios-app-name <name> | Display name. Defaults to "Roves Game". |
--ios-bundle-id <id> | Bundle identifier (e.g. com.example.mygame). Defaults to org.roves.game. |
--ios-release | Archive and export a real, signed .ipa instead of an unsigned Simulator build. |
--ios-release needs a real Apple Distribution identity, set via environment variables:
| Variable | What it is |
|---|---|
IOS_SIGNING_CERTIFICATE_P12_PATH / _PASSWORD | Your Apple Distribution certificate + private key, exported as a .p12. |
IOS_SIGNING_PROVISIONING_PROFILE_PATH | A .mobileprovision matching that certificate and your app's bundle ID. |
IOS_SIGNING_TEAM_ID | Your Apple Developer Program Team ID. |
All three come from your own Apple Developer Program account —
unlike an Android keystore (which you can generate yourself, see above), Apple must
countersign the certificate, so there's no way to produce a real one without an enrolled
account. Without all three set, --ios-release refuses outright rather than silently
producing an unsigned build.
Prefer working in Xcode directly? Stage the project standalone, then open it yourself:
python3 support/ios/bundle.py --content-dir /absolute/path/to/dist --output /absolute/path/to/ios-project --app-name "My Game" --bundle-id com.example.mygame
cd /absolute/path/to/ios-project && xcodegen generate --spec project.json && open RovesGame.xcodeprojLike Android, the game runs at a real origin (game://content/, served by a small
WKURLSchemeHandler) rather than file://, with the same missing-path-falls-back-to-
index.html behavior, and the app runs edge-to-edge (status bar and, on Face ID devices,
the home indicator both hidden).
What CI actually verifies
Both platforms have a GitHub Actions workflow in the engine repo (android.yml/ios.yml)
that smoke-tests the real packaging path end to end — the same mach bundle commands above,
run against placeholder content — on every push. Android's produces a real, installable debug
.apk; iOS's builds an unsigned Simulator app (no real Apple signing identity exists in CI).
Android release signing is verified for real (a generated test keystore, --android-release,
then apksigner verify on the result) since a keystore is self-signed by design; iOS release
signing only has its keychain-import mechanics smoke-tested with a self-signed test
certificate — a full signed .ipa export needs a real Apple-issued provisioning profile,
which can't be verified without an actual Apple Developer Program account. Neither replaces
real device testing: things CI can't check for you include splash timing, storage
persistence, video seeking, rotation, and how your specific game behaves on the platform's
own WebView version.
Using roves-action instead of the CLI
roves-action has android/ios inputs
mirroring the CLI above (advanced-mode: true required for both, plus a macOS runner for
ios) — see its own docs for the full input list. Its ios: true output includes both the
raw Xcode project (for actually signing and shipping to a device) and an already-built,
unsigned Simulator .app in the same output, so you don't need Xcode installed just to take
a first look at your game running.
Using Packmaster instead of the CLI
Packmaster has two Mobile cards, Android and
iOS, mirroring the CLI/roves-action capabilities above (including release signing for both)
— see its own docs for the full detail. iOS packaging only works when Packmaster itself runs
on macOS with Xcode and XcodeGen installed, the same restriction as everywhere else on this
page.