From 49f1a54c4bd95e7106e48db91b0bcd82e9f8bcb9 Mon Sep 17 00:00:00 2001 From: Marijn Suijten Date: Tue, 21 Jul 2026 17:16:08 +0200 Subject: [PATCH] android: Compile native code against min_sdk, not target_sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NDK compile level — the `` suffix on `--target=`, i.e. `__ANDROID_API__` — gates which symbols the compiler treats as available. Setting it to `target_sdk_version` lets native code link strong references to symbols that don't exist on the oldest supported device, which then fails to load at runtime: exactly the compatibility breakage `min_sdk_version` exists to prevent. Only aapt and `android.jar` should follow `target_sdk_version`. This was never a deliberate target-over-min choice: the API level wasn't set at all until #209, and when the versioned `--target` was finally added it simply reused the pre-existing `target_sdk_version` variable that had been wired up for `android.jar`, so the target-SDK level was inherited incidentally from that earlier path. cargo-apk got this right back in 2021 — rust-mobile/ndk#197 ("cargo-apk: Use min_sdk_version to select compiler target"), pre-split from cargo-apk's own repo, per https://developer.android.com/ndk/guides/sdk-versions#minsdkversion Co-Authored-By: Claude Opus 4.8 (1M context) --- xbuild/src/cargo/mod.rs | 10 ++++++---- xbuild/src/lib.rs | 12 +++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/xbuild/src/cargo/mod.rs b/xbuild/src/cargo/mod.rs index 7b916bda..b1a3aa30 100644 --- a/xbuild/src/cargo/mod.rs +++ b/xbuild/src/cargo/mod.rs @@ -287,11 +287,13 @@ impl CargoBuild { }) } - pub fn use_android_ndk(&mut self, path: &Path, target_sdk_version: u32) -> Result<()> { + pub fn use_android_ndk(&mut self, path: &Path, min_sdk_version: u32) -> Result<()> { let path = dunce::canonicalize(path)?; let ndk_triple = self.target.ndk_triple(); assert_eq!(Some(ndk_triple), self.triple); - let ndk_versioned_triple = format!("{ndk_triple}{target_sdk_version}"); + // The `` suffix sets `__ANDROID_API__`, the compile-time symbol + // availability floor — hence min, not target, SDK (see the call site). + let ndk_versioned_triple = format!("{ndk_triple}{min_sdk_version}"); self.cfg_tool(Tool::Cc, "clang"); self.cfg_tool(Tool::Cxx, "clang++"); self.cfg_tool(Tool::Ar, "llvm-ar"); @@ -300,11 +302,11 @@ impl CargoBuild { self.add_cflag(&format!("--target={ndk_versioned_triple}")); self.add_cxxflag("-stdlib=libc++"); let lib_dir = path.join("usr").join("lib").join(ndk_triple); - let sdk_lib_dir = lib_dir.join(target_sdk_version.to_string()); + let sdk_lib_dir = lib_dir.join(min_sdk_version.to_string()); anyhow::ensure!( sdk_lib_dir.exists(), "ndk doesn't support sdk version {}", - target_sdk_version + min_sdk_version ); self.use_ld("lld"); self.add_link_arg(&format!("--target={ndk_versioned_triple}")); diff --git a/xbuild/src/lib.rs b/xbuild/src/lib.rs index 8839068a..25b4bc87 100644 --- a/xbuild/src/lib.rs +++ b/xbuild/src/lib.rs @@ -677,14 +677,20 @@ impl BuildEnv { } if target.platform() == Platform::Android { let ndk = self.android_ndk(); - let target_sdk_version = self + // Native code is compiled against the min SDK/API level, not the + // target SDK: the NDK compile level gates symbol availability, so a + // level above min_sdk links symbols absent on the oldest supported + // device. aapt and `android.jar` track the target SDK separately. + // Matches cargo-apk: https://github.com/rust-mobile/ndk/pull/197 + // https://developer.android.com/ndk/guides/sdk-versions#minsdkversion + let min_sdk_version = self .config() .android() .manifest .sdk - .target_sdk_version + .min_sdk_version .unwrap(); - cargo.use_android_ndk(&ndk, target_sdk_version)?; + cargo.use_android_ndk(&ndk, min_sdk_version)?; } if target.platform() == Platform::Windows { let sdk = self.windows_sdk();