agentleFS
mirzaaghazadehmirzaaghazadeh/unity-meta-quest-skills/skills/custom/unity-quest-android-build/SKILL.md

unity-quest-android-build

Skill1 starsChanged 30 days agoMITView on GitHub

Builds Meta Quest (Horizon OS) Android APKs from a Unity project with the unity CLI, as development or release builds, for Quest and Meta VR Glasses. Offers three routes (a Meta Quest build profile with no code, a BuildPipeline script through --execute-method, or a build inside the open Editor), plus toolchain and module checks, the Quest packaging baseline (APK, ARM64, IL2CPP, API 32/34, ASTC), build-failure troubleshooting, and APK checks with aapt2, apksigner and unzip (64-bit libraries only, hand/eye-tracking declarations, no experimental flag or Meta XR Operator layer in release). Use when asked to build or export a Quest APK, make a dev build to try on the headset, pick a headless or CI build route, check a build for Meta VR Glasses, or fix an Android, Gradle or IL2CPP build failure. For uploading to Horizon Store release channels, use horizon-store-publish-unity.

The file

We split this skill into the rules it gives an agent

12 rules. About 0 are common, 3 are distinctive, and 9 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 9

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Check every row before calling a release build done.

    line 34
  2. Very specific:

    Live Editor open?: Use unity status --format json (state ready).

    line 79
  3. Distinctive:

    Leave out the four signing flags for a local development build (debug key).

    line 137
  4. Very specific:

    Use QuestBuild.BuildDevelopment for a debug-signed build (the Development build option).

    line 155
  5. Very specific:

    If the game uses Unity Localization (or any Addressables), build the content before BuildPipeline.BuildPlayer in your method: …

    line 169
  6. Very specific:

    preloaded assets: Check preloadedAssets in ProjectSettings/ProjectSettings.asset against git and restore it.

    line 198
  7. Very specific:

    Add --profileName "<Meta Quest profile name>" to build from a profile, and --options Development for a dev build.

    line 203
  8. Very specific:

    Read data.logFile from the JSON envelope.

    line 214
  9. Very specific:

    After a launch, check metavr adb shell "pidof <pkg>", or look for ForegroundAppHandler: Changing in focus immersive app ...

    line 295
  10. Very specific:

    Run unity ci init --target Android --dry-run to get Unity's generated GitHub Actions workflow (CLI install, caches, license, doctor --ci, …

    line 314
  11. Distinctive:

    Don't trust a successful exit alone: Run Step 3 on every release candidate.

    line 329
  12. Very specific:

    Don't invent flags: If a flag above is rejected, run unity build --help.

    line 330

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Unity → Meta Quest APK build

Made by Navid Mirzaaghazadeh · part of unity-meta-quest-skills · MIT

Build an APK that installs on Quest and passes the Horizon Store's packaging checks, using the unity CLI. This skill owns the Quest-specific part. For CLI install, flags, --format json and exit codes in general, read the official unity-cli skill. Its references/build-run-test.md documents unity build in full.

Hand-offs (other skills in this library):

  • OVRManager, OpenXR features, AndroidManifest permissions, Project Setup Tool → hz-unity-meta-core-sdk
  • Installing on the headset and reading logs → metavr-cli, hz-vr-debug
  • Meta-verified Player, XR and URP settings for Quest → quest-unity-project-settings
  • Developer Dashboard setup, ovr-platform-util upload to ALPHA/BETA/RC/store, moving builds between channels → horizon-store-publish-unity. It ships its own release-build script, QuestReleaseBuild.cs, which reads the same QUEST_* env vars as path B here. Use one script per project, not both.
  • Review submission and the VRC checklist → hz-store-submit, horizon-vrc-checklist
  • Editor and Android module setup on a new machine → unity-quest-dev-setup

Horizon Store packaging baseline

Check every row before calling a release build done. Re-check against the sources when a build is rejected: Meta changes these rules.

SettingRequired valueSource
Package formatAPK (buildAppBundle = false). Unity documents AAB as "only applicable to builds for Google Play". Meta uploads APK plus optional OBB/asset filesUnity EditorUserBuildSettings.buildAppBundle; Meta ps-assets
ABIARM64 only (no ARMv7, no x86). Meta VR Glasses can't run a binary whose native libraries are only 32-bit (a Mono build), even though Quest still canMeta hz-store-submit; Unity Meta Quest profile default; Meta app-compatibility (2026-10-13)
Scripting backendIL2CPPUnity Meta Quest profile default (ARM64 needs IL2CPP)
minSdkVersion32 recommended (store floor 29)Meta manifest requirements page (updated 2026-10-15)
targetSdkVersion34. Required for apps created in the Developer Dashboard after 2026-03-01. Older apps may use 32–34Meta blog "Apps must target Android 14", same page
Texture compressionASTCMeta hz-new-project-creation
Graphics APIVulkan (Unity's Meta Quest profile default)Unity Meta Quest profile docs
Color spaceLinearMeta Unity setup reference
SigningRelease keystore, the same one for the app's whole life. APK Signature Scheme v2Meta hz-store-submit
versionCodeStrictly higher than every uploaded buildMeta hz-store-submit
ManifestinstallLocation="auto", not debuggable, com.oculus.intent.category.VR on the launcher activity, com.oculus.supportedDevices meta-dataMeta manifest page. The Meta XR SDK generates these. Never hand-edit them (see hz-unity-meta-core-sdk)
Input declarationMatches the app: no oculus.software.handtracking = controllers only; required="false" + com.oculus.permission.HAND_TRACKING = both; required="true" = hands only. Eye tracking: oculus.software.eye_tracking + com.oculus.permission.EYE_TRACKING only if gaze or ETFR is usedMeta app-compatibility (2026-10-13); Unity support-meta-vr-glasses (2026-09-30)
No test-only featuresDevelopment Build off; no com.oculus.experimental.enabled (Experimental Features Enabled marks a build experimental); no Meta XR Operator layer (libXrApiLayer_METAX_operator)Unity in-headset-fov-simulation (2026-09-24); meta-xr-operator/quest (2026-09-10)

Trap: Unity's Meta Quest build profile defaults to target API 32 (Unity 6.3 to 6.6 docs). New store apps need 34. Override it in the profile or Player settings, or pass --android-target-sdk-version 34.

Step 0: Check the toolchain (read-only)

unity --version                                   # missing? see unity-quest-dev-setup
unity projects info <project> --format json       # data has the project's editorVersion
unity editors --installed --format json           # is that version installed? note its "location"
unity editors verify <editorVersion>              # are module files present? It prints the repair command
unity license status --format json                # batch builds need an active license

The Editor needs the android module and its children: Android SDK & NDK Tools (android-sdk-ndk-tools) and OpenJDK (android-open-jdk). If any are missing, ask the user first (several GB, and it accepts Android SDK EULAs), then run:

unity install-modules -e <editorVersion> -m android --cm

Modules can only be added to Editors that the Hub or CLI installed.

Step 1: Check the project settings

Live Editor open? Use unity status --format json (state ready). A batch build can't open a project that an Editor already has open, so either build inside that Editor (Step 2, path C) or ask the user to save and close it. unity close does not save. Read the settings without changing anything:

mkdir -p <project>/AgentScripts
cp <this-skill>/templates/QuestPreflight.cs <project>/AgentScripts/
unity command script run --project-path <project> --file AgentScripts/QuestPreflight.cs \
  --entry QuestPreflight.Run --format json
unity command build list_profiles --project-path <project> --format json

These are Pipeline 0.9 names (<group> <name>). Pipeline 0.8 calls them run_script and list_build_profiles, and path C's build run / build status were build / build_status (table in skills/meta-official/ERRATA.md).

No Editor: unity build <project> --list-profiles --format json lists the build profiles. Then read the settings with the build script's check, which runs in Step 2 path B.

Fix problems at the source. Prefer Meta's Project Setup Tool (OVRProjectSetup.FixAllAsync, see hz-unity-meta-core-sdk). For anything it leaves, either run templates/QuestBuild.cs → QuestBuild.ApplyQuestSettings once:

unity run <project> -- -executeMethod QuestBuild.ApplyQuestSettings

or set the values in the Meta Quest build profile's Player Settings Overrides. Commit the ProjectSettings/ diff after either one.

Step 2: Build

Pick one path. All three produce Builds/Quest/<App>.apk. Keep Builds/ out of git.

A. Build profile, no code (Unity 6.1+, recommended)

Unity's release notes say Meta Quest builds "only through --profile" because it isn't in the classic --target list. Create the profile once in File > Build Profiles > Add Build Profile > Meta Quest, or let Meta's setup script switch to it. It lands under Assets/Settings/Build Profiles/.

unity build <project> --list-profiles --format json          # find the Meta Quest profile's name/path
unity build <project> \
  --profile "Assets/Settings/Build Profiles/<MetaQuestProfile>.asset" \
  --output-path Builds/Quest/<App>.apk \
  --android-export-type apk \
  --android-target-sdk-version 34 \
  --android-version-code <N> \
  --android-keystore-base64 "$QUEST_KEYSTORE_B64" \
  --android-keystore-password "$QUEST_KEYSTORE_PASS" \
  --android-key-alias "$QUEST_KEY_ALIAS" \
  --android-key-alias-password "$QUEST_KEY_ALIAS_PASS" \
  --format json
  • The CLI writes the keystore to a temp file and deletes it afterwards, including on Ctrl-C. The expanded secrets still appear in argv, so keep them in env vars and mask them in CI logs.
  • Leave out the four signing flags for a local development build (debug key). Never upload one.
  • Unity's docs don't say whether the --android-* flags override values a profile sets. Verify the APK (Step 3) instead of assuming they do.
  • --create-profile <target> takes a classic target. A Meta Quest value for it is not documented, so run unity build --help before trying.

B. --execute-method with the bundled script (Unity 6.0+, full control)

cp <this-skill>/templates/QuestBuild.cs <project>/Assets/Editor/QuestBuild.cs   # commit it
export QUEST_VERSION_CODE=<N> QUEST_KEYSTORE_PATH=/secure/quest-release.keystore \
       QUEST_KEYSTORE_PASS=... QUEST_KEY_ALIAS=... QUEST_KEY_ALIAS_PASS=...    # from a secret store
# optional: export QUEST_BUILD_PROFILE="Assets/Settings/Build Profiles/<MetaQuestProfile>.asset"
unity build <project> --target Android --execute-method QuestBuild.BuildRelease \
  --output-path Builds/Quest/<App>.apk --format json
  • --output-path reaches the method as -buildOutput, which the script reads.
  • Use QuestBuild.BuildDevelopment for a debug-signed build (the Development build option).
  • Without a profile, the script fails the build on any baseline problem and lists each one. With a profile it skips that check, because profile overrides aren't visible to PlayerSettings. Step 3 verifies the APK instead.
  • For an --execute-method build, the CLI takes its result from the Editor's exit code. The script calls EditorApplication.Exit(1) on failure, so exit 6 means a real failure.
  • Observed 2026-10-25 (unity CLI 1.0.0-beta.13, Unity 6000.6.6f1, Core 207.0.1):
    • unity build spawns its own batch-mode Editor, so an Editor that has the project open must be closed first: unity close <project> (the path is positional; --project-path was rejected). Reopen afterwards with unity open <project> and unity status --until-ready.
    • Twice, Unity aborted with SIGABRT on exit after logging the build's success, and the CLI reported "Build failed: Unity process stopped with signal SIGABRT". The APK was complete both times. Log your own result line from the build method (size, version, errors) and read it, then verify the APK (Step 3), rather than trusting the exit status alone.
    • If the game uses Unity Localization (or any Addressables), build the content before BuildPipeline.BuildPlayer in your method: AddressableAssetSettings.BuildPlayerContent(out var result), failing on result.Error.
    • Everything under Resources/ ships, git-ignored or not. Meta's Immersive Debugger keeps its AI assistant's settings (Assets/Resources/DevAgentSettings.asset, an accessToken and a LAN address) there. In a release build method, move it (and its .meta) out of Assets/, for example into Library/, build, and move it back in a finally; an IPreprocessBuildWithReport that throws BuildFailedException for any non-development build while it's still in Resources/ catches the other routes. Check the APK afterwards (no match for the file name or the address in assets/bin/Data).

C. Inside the open Editor (no restart, no second license seat)

unity command build run --project-path <project> -- --target Android \
  --outputPath Builds/Quest/<App>.apk --dry_run true                  # validate first
unity command build run --project-path <project> -- --target Android \
  --outputPath Builds/Quest/<App>.apk --confirm true                  # queue it
unity command build status --project-path <project> --format json     # poll until status "completed"

Modal dialogs stall this path (observed 2026-10-25): a build in the open Editor waited 25 minutes on a decision dialog (DisplayDecisionDialogWithOptOut in PostprocessBuildPlayer.PrepareForBuild) that the Pipeline doesn't report: editor status just times out. On macOS sample <Unity pid> shows NSAlert runModal. Ask the user to answer it, or close the Editor and use path B, where batch mode can't show dialogs.

  • Killing a stuck Editor leaves Temp/__Backupscenes/0.backup, and the next open stalls on a restore prompt. If the scene is committed, delete the backup before reopening.
  • An interrupted build can leave OpenXR, XR General and Localization settings in Player Settings' preloaded assets. Check preloadedAssets in ProjectSettings/ProjectSettings.asset against git and restore it.
  • build run straight after an Addressables BuildPlayerContent eval once returned "No Pipeline instance found" and queued nothing (build status idle); retrying it once worked.
  • Add --profileName "<Meta Quest profile name>" to build from a profile, and --options Development for a dev build. The -- fence keeps those parameters away from the CLI's own flags. Parameter names come from the project's com.unity.pipeline version, so check them first with unity command --query build --format json.
  • The build uses the Editor's current signing settings. Unity doesn't save keystore passwords, so a release-signed build from a fresh Editor session needs them entered in Player Settings first. Use path A or B for release builds.
  • build status returns result (Succeeded/Failed), errors[], outputPath and files[].

Build failed?

Read data.logFile from the JSON envelope. The default is <project>/Logs/build-<target>-<timestamp>.log. Grep the log, don't dump it:

grep -nE "error CS[0-9]{4}|Build Failed|BuildFailedException|CommandInvokationFailure|FAILURE:|What went wrong|Unable to|Exception:" <log> | head -60

Exit codes: 2 bad flags (fix the command), 3 sign-in, 4 precondition such as no license, 6 the build failed (read the log), 7 service unreachable (retry), 143 killed or timed out. Common causes and fixes: references/troubleshooting.md.

Step 3: Verify the APK

The build tools ship inside the Editor's Android module. On macOS the layout is /Applications/Unity/Hub/Editor/<ver>/PlaybackEngines/AndroidPlayer/SDK/build-tools/<n>/{aapt2,apksigner} and .../SDK/platform-tools/adb. That was checked on a 6000.6.6f1 install, where <n> was 36.0.0. On Windows and Linux the Android module sits under <Editor dir>/Editor/Data/PlaybackEngines/AndroidPlayer/SDK, which was not checked. The build-tools version changes, so search for the tools:

SDK="<Editor install dir>/PlaybackEngines/AndroidPlayer/SDK"      # adjust per OS
AAPT2=$(find "$SDK" -name aapt2 -type f | sort | tail -1)
APKSIGNER=$(find "$SDK" -name apksigner -type f | sort | tail -1)
# apksigner is a shell wrapper that runs `java` from PATH (it ignores JAVA_HOME).
# With no working system JDK, use the Editor's own:
java -version >/dev/null 2>&1 || export PATH="$SDK/../OpenJDK/bin:$PATH"
APK=Builds/Quest/<App>.apk

"$AAPT2" dump badging "$APK" | grep -E "^package:|[sS]dkVersion|native-code"
#   expect: versionCode='<N>'  minSdkVersion:'32'  targetSdkVersion:'34'  native-code: 'arm64-v8a' (only)
#   (aapt2 prints minSdkVersion:; the old aapt printed sdkVersion:)
"$AAPT2" dump xmltree --file AndroidManifest.xml "$APK" \
  | grep -E "installLocation|debuggable|com.oculus.intent.category.VR|com.oculus.supportedDevices"
"$APKSIGNER" verify --verbose --print-certs "$APK" | grep -E "Verified using v2|Signer #1 certificate DN"
#   expect: "Verified using v2 scheme (APK Signature Scheme v2): true"
#   and a certificate DN that is NOT "CN=Android Debug" for a release build
ls -l "$APK"                                                       # size sanity check

# Input declarations and test-only features (matters most for Meta VR Glasses)
"$AAPT2" dump xmltree --file AndroidManifest.xml "$APK" \
  | grep -E -A2 "handtracking|eye_tracking|HAND_TRACKING|EYE_TRACKING|com.oculus.experimental.enabled"
#   expect: the hand/eye entries the app's input promise needs; no com.oculus.experimental.enabled
unzip -l "$APK" | awk '$4 ~ /^lib\// {split($4, p, "/"); print p[2]}' | sort -u   # expect only arm64-v8a
unzip -l "$APK" | grep -i "METAX_operator" && echo "FAIL: Meta XR Operator layer in a release APK"

Report each check as pass or fail with the actual value. A failure means the build isn't releasable. Go back to Step 1, not straight to upload.

com.oculus.supportedDevices and Meta VR Glasses. Report the value as found; don't edit it. With the Unity OpenXR plugin, the APK lists Unity's names (quest, quest2, cambria, eureka, quest3s; there is no VR Glasses entry in OpenXR 1.18.0 or 1.19.0-pre.2). If Meta's own manifest patch writes the list and its VR Glasses target is on, Core 207.0.1 adds the internal key stanley (package source). Meta's pages disagree on the public id (vrglasses on the manifest page, "not a supported public value" on the compatibility-mode page), and delivery to VR Glasses is set by ticking Future devices in the Dashboard (horizon-store-publish-unity). Details: quest-unity-project-settings §1 and meta-vr-glasses-unity.

Step 4: Run it on the headset, then hand off

metavr app install Builds/Quest/<App>.apk        # Meta's CLI (see metavr-cli). Or: adb install -r <apk>
metavr app launch <applicationId>
metavr log --tag Unity                           # first-launch errors

Unattended checks on a Quest 3S (observed 2026-10-25, metavr 1.8): with nobody wearing it, the headset sleeps within seconds of a launch, the app gets APP_CMD_PAUSE/STOP, and metavr capture screenshot -o shot.png comes back black (it warns that metacam is missing and falls back to screencap). Run metavr device proximity --disable, relaunch, capture, then metavr device proximity --enable again. Logcat's VrApi FPS= lines give the frame rate, the App (GPU) time in ms and GPU %. In the same line, Fov=0D means dynamic foveation is on (D) and currently at level 0.

A launch that never happened (observed 2026-10-25, metavr 1.8):

  • metavr app launch <pkg> --cold-start --verify printed "Launched" and "no crash signal". But pidof <pkg> was empty, because Horizon OS had held the launch back. Logcat said Launch is blocked because: a Reprojected OS dialog is currently showing. Caching launch, then Interstitial session (APP_RESUME) took more than 15s and appLaunchFailureBroadcastReceived.
  • The app starts once someone closes the dialog in the headset.
  • After a launch, check metavr adb shell "pidof <pkg>", or look for ForegroundAppHandler: Changing in focus immersive app ... to (<pkg>).

Recording logcat for a whole session:

  • Piping metavr adb logcat -f ... or metavr adb shell "logcat -T 1 ..." into a file on the Mac wrote nothing, even after minutes.
  • What worked was letting the headset write the file: metavr adb shell "nohup logcat -v time -f /sdcard/Download/session.log -r 8192 -n 2 Unity:V VrApi:I AndroidRuntime:E '*:S' > /dev/null 2>&1 &", then metavr adb pull /sdcard/Download/session.log when needed.
  • metavr log --tag <tag> -n <lines> --format plain is fine for a snapshot.
  • metavr adb -- logcat ... is rejected ("unexpected argument"): write metavr adb logcat or metavr adb shell "logcat ...".

adb comes with the Unity SDK module under .../SDK/platform-tools/adb. For store upload (ovr-platform-util upload-quest-build), OBB and asset files, and release channels, use horizon-store-publish-unity. For review, use hz-store-submit.

CI

Run unity ci init --target Android --dry-run to get Unity's generated GitHub Actions workflow (CLI install, caches, license, doctor --ci, tests, build, artifacts, seat return). Then change its build step to path A or B. The full recipe, secrets layout and licensing caveats are in references/ci.md.

Rules

  • Keystores and passwords never go in the repo, the prompt, a command literal or a log. Read them from env vars or a secret store. Back up the release keystore: losing it means a new store listing. See references/signing-and-versioning.md.
  • Ask before installing modules (multi-GB, accepts EULAs), closing an Editor (unsaved work), or changing ProjectSettings/. Show the diff afterwards.
  • Never hand-edit AndroidManifest.xml for features managed by OVRProjectConfig. Regenerate it through the Meta SDK (see hz-unity-meta-core-sdk).
  • Increase versionCode on every upload. In CI, use the run number or a value derived from a git tag.
  • Don't trust a successful exit alone. Run Step 3 on every release candidate.
  • Don't invent flags. If a flag above is rejected, run unity build --help. The CLI is in beta and changes weekly.

Sources

Last verified: 2026-10-25 (the observed notes in path B and C and Step 4, from headless dev and release builds of a Unity 6000.6.6f1 + Core 207.0.1 game installed on a Quest 3S); 2026-10-17 (VR Glasses rows and APK checks; the unzip lines were run on a test archive, not a real APK); 2026-10-16 (the rest). templates/QuestBuild.cs and templates/QuestPreflight.cs compile with 0 errors and 0 warnings against Unity 6000.6.6f1's managed DLLs, using Unity's bundled Roslyn; neither has been run in a project. The macOS SDK tool paths were checked on a 6000.6.6f1 install. Not verified against a live CLI: whether --android-* flags apply on top of a --profile build, and --create-profile with a Meta Quest value.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.