Roves

Mobile (Android & iOS)

Package your game as a native Android WebView app or iOS WKWebView app.

Edit on GitHub

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-output

This 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:

FlagOverrides
--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-releaseBuild 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.mygame

This 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.

FlagOverrides
--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-releaseArchive and export a real, signed .ipa instead of an unsigned Simulator build.

--ios-release needs a real Apple Distribution identity, set via environment variables:

VariableWhat it is
IOS_SIGNING_CERTIFICATE_P12_PATH / _PASSWORDYour Apple Distribution certificate + private key, exported as a .p12.
IOS_SIGNING_PROVISIONING_PROFILE_PATHA .mobileprovision matching that certificate and your app's bundle ID.
IOS_SIGNING_TEAM_IDYour 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.xcodeproj

Like 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.

On this page