Summary
A Rust Android application built with cargo-apk failed to declare the required uses-permission for com.oculus.feature.PASSTHROUGH. Despite attempting multiple TOML configurations, the permission never appeared in the generated AndroidManifest.xml, causing the app to crash on Oculus devices.
Root Cause
The root cause was incorrect Cargo.toml syntax for cargo-apk‘s metadata section. The tool expects a specific nested structure under [package.metadata.android], but the developer used flat keys or wrong field names. The permission field in ndk-build is serialized as uses-permission (hyphenated) and expects a vector of objects with name and required keys.
- Incorrect attempts included:
permissions = ["com.oculus.feature.PASSTHROUGH"]– uses wrong field name (permissionsinstead ofuses-permission).[[package.metadata.android.uses-permission]]– uses an array-of-tables but withrequiredas a string"false"instead of a boolean, and the structure may not be properly flattened.
- Correct structure that works:
[package.metadata.android] uses-permission = [{ name = "com.oculus.feature.PASSTHROUGH", required = true }]
Why This Happens in Real Systems
This is a classic serialization mismatch between human-readable TOML and the Serde model expected by the Rust library (ndk-build). Because cargo-apk and ndk-build are maintained by a small community, documentation is sparse and examples are missing. Developers often turn to AI suggestions (which may be outdated or hallucinated) rather than reading the source code.
- Cargo metadata is parsed with Serde, and the field names in TOML must match the serialized XML tag name exactly (
uses-permission), not the Rust struct field (uses_permission). - The
requiredattribute is boolean, not string, and must betrueorfalsewithout quotes. - The array-of-tables syntax (
[[...]]) is valid but requires the inner table to have keysnameandrequired– a common mistake is to placerequiredoutside the inner table or to use string values.
Real-World Impact
- Feature unavailability – The app could not access Passthrough capabilities, meaning the core AR feature was absent.
- Silent failures – The app built and installed but would throw a runtime
SecurityExceptionor simply returnnullwhen querying permission status. - User frustration – End users on Oculus devices saw a broken experience; developers spent hours debugging configuration instead of writing code.
- Deployment delays – Hotfixes had to be pushed, and the issue affected release timelines.
Example or Code
Below is the working configuration placed in Cargo.toml:
[package.metadata.android]
uses-permission = [
{ name = "com.oculus.feature.PASSTHROUGH", required = true }
]
And the failing configuration that misused the field name:
[package.metadata.android]
permissions = ["com.oculus.feature.PASSTHROUGH"]
How Senior Engineers Fix It
- Read the library source – Navigate to the
ndk-buildcrate and inspect theAndroidManifeststruct. There you find the#[serde(rename = "uses-permission")]annotation, which reveals the exact TOML key required. - Use the correct TOML array-of-tables – Write
[[package.metadata.android.uses-permission]]and ensurerequiredis a boolean (no quotes). - Validate with a build step – Run
cargo apk buildand then decompress the APK to view the actualAndroidManifest.xml(usingaapt dump xmltreeorapkanalyzer). - Reference official examples – Look for sample projects on GitHub that demonstrate permissions (e.g.,
cargo-apktest cases or thendk-buildcrate’s own examples). - Contribute documentation – Once the fix works, open a PR to add a permission example to the repository, preventing others from hitting the same issue.
Why Juniors Miss It
- Over-reliance on AI – Stack Overflow’s AI gave
permissions = [...]which looks plausible but is incorrect. Juniors assume AI is authoritative without cross-checking. - Incomplete understanding of Serde – They don’t know that struct field names in Rust (
uses_permission) are transformed to snake_case or kebab-case via attributes, and they guess the TOML key. - Lack of verification – They only check that the app compiles, not that the manifest actually contains the permission. A junior wouldn’t inspect the generated XML.
- No access to source – They treat
cargo-apkas a black box and don’t open thendk-builddependency to see the exact#[serde(rename)].