Fix missing uses-permission in Rust cargo-apk for Oculus

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 (permissions instead of uses-permission).
    • [[package.metadata.android.uses-permission]] – uses an array-of-tables but with required as 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 required attribute is boolean, not string, and must be true or false without quotes.
  • The array-of-tables syntax ([[...]]) is valid but requires the inner table to have keys name and required – a common mistake is to place required outside 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 SecurityException or simply return null when 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

  1. Read the library source – Navigate to the ndk-build crate and inspect the AndroidManifest struct. There you find the #[serde(rename = "uses-permission")] annotation, which reveals the exact TOML key required.
  2. Use the correct TOML array-of-tables – Write [[package.metadata.android.uses-permission]] and ensure required is a boolean (no quotes).
  3. Validate with a build step – Run cargo apk build and then decompress the APK to view the actual AndroidManifest.xml (using aapt dump xmltree or apkanalyzer).
  4. Reference official examples – Look for sample projects on GitHub that demonstrate permissions (e.g., cargo-apk test cases or the ndk-build crate’s own examples).
  5. 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-apk as a black box and don’t open the ndk-build dependency to see the exact #[serde(rename)].

Leave a Comment