Skip to content

recipe: export ANDROID_API_LEVEL so maturin can build Rust wheels - #3396

Open
EDMND-SRC wants to merge 1 commit into
kivy:developfrom
EDMND-SRC:fix/rust-android-api-level
Open

EDMND-SRC wants to merge 1 commit into
kivy:developfrom
EDMND-SRC:fix/rust-android-api-level

Conversation

@EDMND-SRC

Copy link
Copy Markdown

Problem

Any Rust recipe that builds a wheel through maturin fails on armeabi-v7a with:

    Finished `release` profile [optimized] target(s) in 11m 40s
💥 maturin failed
  Caused by: Failed to determine Android API level. Please set the ANDROID_API_LEVEL environment variable.
Error: command ['maturin', 'pep517', 'build-wheel', '-i', '.../bin/python', '--compatibility', 'off'] returned non-zero exit status 1

Note the ordering, because it is what makes this expensive: cargo compiles the entire crate first, and only then does the PEP 517 backend reject the build.
On a slow machine that is roughly twelve minutes of work discarded on every attempt, and it reads like an unrelated failure at the very end of the build.

Why this belongs in the base class

RustCompiledComponentsRecipe.get_recipe_env() exports RUSTFLAGS and PYO3_CROSS_LIB_DIR, but never ANDROID_API_LEVEL:

https://github.com/kivy/python-for-android/blob/develop/pythonforandroid/recipe.py#L1652

Five in-tree recipes subclass it: pydantic-core, cryptography, tiktoken, primp and watchfiles.
Only watchfiles works around the problem, by overriding get_recipe_env() inside the recipe itself:

https://github.com/kivy/python-for-android/blob/develop/pythonforandroid/recipes/watchfiles/__init__.py#L9-L13

That workaround computes the correct value, but it places it one level too low, so every other Rust recipe still fails.
This PR moves the same assignment up into the base class, which leaves the watchfiles override harmless and redundant.

The change

         realpython_dir = self.ctx.python_recipe.get_build_dir(arch.arch)
+
+        # maturin (used by every Rust recipe here) refuses to build without the
+        # target Android API level: "Failed to determine Android API level."
+        env["ANDROID_API_LEVEL"] = str(self.ctx.ndk_api)
 
         env["RUSTFLAGS"] = "-Clink-args=-L{} -L{}".format(

ctx.ndk_api is the correct value rather than ctx.android_api.
maturin tags the wheel with the minimum supported API level, which is the NDK API, so the artifact is correctly labelled android_28_arm instead of claiming compatibility it does not have.

Verification

I built pydantic-core 2.41.4 for armeabi-v7a with --minsdk 28 --ndk-api 28 on top of develop with exactly this one-line change:

  • Wheel produced: pydantic_core-2.41.4-cp314-cp314-android_28_arm.whl
  • _pydantic_core.cpython-314-arm-linux-androideabi.so checked with file and reported as ELF 32-bit LSB shared object, ARM, EABI5, for Android 28, built by NDK r28c
  • The resulting APK installed and ran on a real 32-bit Android 12 device (armeabi-v7a)

Without the change, the identical build stops at the maturin step with the error quoted above.

Safety of ctx.ndk_api

It is always an int by the time a recipe builds.
prepare_build() does ndk_api = int(ndk_api) and check_ndk_api() validates it before any recipe is constructed, and the property raises a clear ValueError if it were ever accessed unset.

Follow-up, deliberately not in this PR

Once this lands, the per-recipe override in recipes/watchfiles/__init__.py can be deleted.
I left it alone to keep this change to the single line that fixes the class of bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants