Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ These environment variables are exported for use in build scripts and other down
| `CARGO_NDK_SYSROOT_PATH` | Path to the sysroot inside the Android NDK | `/path/to/ndk/toolchains/llvm/prebuilt/...` |
| `CARGO_NDK_SYSROOT_TARGET` | The target name for files inside the sysroot (differs from LLVM triples) | `aarch64-linux-android` |
| `CARGO_NDK_SYSROOT_LIBS_PATH` | Path to libraries in sysroot with target (`$CARGO_NDK_SYSROOT_PATH/usr/lib/$CARGO_NDK_SYSROOT_TARGET`) | `/path/to/ndk/.../usr/lib/aarch64-linux-android` |
| `LIBCLANG_PATH` | Path containing the NDK's libclang shared library, when detected | `/path/to/ndk/toolchains/llvm/prebuilt/.../lib` |
| `ANDROID_PLATFORM` | The platform version being used | `21` |
| `ANDROID_ABI` | The Android ABI name | `armeabi-v7a` |

Expand Down
159 changes: 157 additions & 2 deletions src/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,48 @@ fn clang_lib_path(ndk_home: &Path) -> PathBuf {
.join("linux")
}

#[inline]
fn contains_libclang(path: &Path) -> bool {
std::fs::read_dir(path)
.ok()
.into_iter()
.flat_map(|entries| entries.filter_map(|entry| entry.ok()))
.any(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| name.starts_with("libclang."))
})
}

#[inline]
fn libclang_search_suffixes() -> &'static [&'static str] {
if cfg!(target_env = "musl") {
&["musl/lib", "lib", "lib64", "bin"]
} else {
&["lib", "lib64", "bin", "musl/lib"]
}
}

#[inline]
fn libclang_path_with_suffixes(ndk_home: &Path, suffixes: &[&str]) -> Option<PathBuf> {
let prebuilt_path = ndk_home
.join("toolchains")
.join("llvm")
.join("prebuilt")
.join(ARCH);

suffixes
.iter()
.map(|suffix| prebuilt_path.join(suffix))
.find(|path| contains_libclang(path))
}

#[inline]
fn libclang_path(ndk_home: &Path) -> Option<PathBuf> {
libclang_path_with_suffixes(ndk_home, libclang_search_suffixes())
}

const CARGO_NDK_SYSROOT_PATH_KEY: &str = "CARGO_NDK_SYSROOT_PATH";
const CARGO_NDK_SYSROOT_TARGET_KEY: &str = "CARGO_NDK_SYSROOT_TARGET";
const CARGO_NDK_SYSROOT_LIBS_PATH_KEY: &str = "CARGO_NDK_SYSROOT_LIBS_PATH";
Expand Down Expand Up @@ -168,6 +210,10 @@ pub(crate) fn build_env(
.into_iter()
.collect::<BTreeMap<String, OsString>>();

if let Some(path) = libclang_path(ndk_home) {
envs.insert("LIBCLANG_PATH".to_string(), path.into_os_string());
}

// cmd.arg(format!("-L{builtins_path}"));

// // Extract arch from target triple (e.g., "aarch64-linux-android21" -> "aarch64")
Expand Down Expand Up @@ -370,11 +416,43 @@ fn append_target_args(

#[cfg(test)]
mod tests {
use std::{ffi::OsString, path::Path};
use std::{
ffi::OsString,
fs,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};

use cargo_metadata::semver::Version;

use super::{append_target_args, build_env};
use crate::ARCH;

use super::{
append_target_args, build_env, libclang_path, libclang_path_with_suffixes,
libclang_search_suffixes,
};

fn temp_ndk_path(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before Unix epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!("cargo-ndk-{name}-{nanos}"));
fs::remove_dir_all(&path).ok();
path
}

fn create_libclang_file(ndk_home: &Path, suffix: &str) -> PathBuf {
let path = ndk_home
.join("toolchains")
.join("llvm")
.join("prebuilt")
.join(ARCH)
.join(suffix);
fs::create_dir_all(&path).expect("failed to create fake libclang directory");
fs::write(path.join("libclang.so"), "").expect("failed to write fake libclang");
path
}

#[test]
fn build_env_exports_android_platform_and_abi() {
Expand Down Expand Up @@ -442,4 +520,81 @@ mod tests {
]
);
}

#[test]
fn libclang_path_prefers_host_ndk_layout() {
let ndk_home = temp_ndk_path("host-libclang");
let host_path = create_libclang_file(&ndk_home, "lib");
let _fallback_path = create_libclang_file(&ndk_home, "musl/lib");

assert_eq!(libclang_path(&ndk_home), Some(host_path));

fs::remove_dir_all(ndk_home).ok();
}

#[test]
fn libclang_path_prefers_musl_layout_for_musl_hosts() {
let ndk_home = temp_ndk_path("musl-host-libclang");
let _host_path = create_libclang_file(&ndk_home, "lib");
let musl_path = create_libclang_file(&ndk_home, "musl/lib");

assert_eq!(
libclang_path_with_suffixes(&ndk_home, &["musl/lib", "lib", "lib64", "bin"]),
Some(musl_path)
);

fs::remove_dir_all(ndk_home).ok();
}

#[test]
fn libclang_path_falls_back_to_lib64_ndk_layout() {
let ndk_home = temp_ndk_path("lib64-libclang");
let fallback_path = create_libclang_file(&ndk_home, "lib64");

assert_eq!(libclang_path(&ndk_home), Some(fallback_path));

fs::remove_dir_all(ndk_home).ok();
}

#[test]
fn libclang_path_uses_musl_layout_as_last_resort() {
let ndk_home = temp_ndk_path("musl-libclang");
let fallback_path = create_libclang_file(&ndk_home, "musl/lib");

assert_eq!(libclang_path(&ndk_home), Some(fallback_path));

fs::remove_dir_all(ndk_home).ok();
}

#[test]
fn libclang_search_order_matches_host_abi() {
let suffixes = libclang_search_suffixes();

if cfg!(target_env = "musl") {
assert_eq!(suffixes[0], "musl/lib");
} else {
assert_eq!(suffixes[0], "lib");
}
}

#[test]
fn build_env_exports_libclang_path_when_detected() {
let ndk_home = temp_ndk_path("build-env-libclang");
let libclang_path = create_libclang_file(&ndk_home, "musl/lib");

let env = build_env(
"aarch64-linux-android",
&ndk_home,
&Version::new(28, 0, 0),
"--target=aarch64-linux-android28",
28,
"arm64-v8a",
false,
false,
);

assert_eq!(PathBuf::from(env["LIBCLANG_PATH"].clone()), libclang_path);

fs::remove_dir_all(ndk_home).ok();
}
}
Loading