Skip to content

Commit 4f71c07

Browse files
committed
feat(android): enhance Android target support and validate NDK installation
1 parent c0c626d commit 4f71c07

File tree

1 file changed

+251
-29
lines changed

1 file changed

+251
-29
lines changed

llama-cpp-sys-2/build.rs

Lines changed: 251 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ fn parse_target_os() -> Result<(TargetOs, String), String> {
4545
} else {
4646
Ok((TargetOs::Apple(AppleVariant::Other), target))
4747
}
48-
} else if target.contains("android") {
48+
} else if target.contains("android")
49+
|| target == "aarch64-linux-android"
50+
|| target == "armv7-linux-androideabi"
51+
|| target == "i686-linux-android"
52+
|| target == "x86_64-linux-android"
53+
{
54+
// Handle both full android targets and short names like arm64-v8a that cargo ndk might use
4955
Ok((TargetOs::Android, target))
5056
} else if target.contains("linux") {
5157
Ok((TargetOs::Linux, target))
@@ -162,6 +168,28 @@ fn macos_link_search_path() -> Option<String> {
162168
None
163169
}
164170

171+
fn validate_android_ndk(ndk_path: &str) -> Result<(), String> {
172+
let ndk_path = Path::new(ndk_path);
173+
174+
if !ndk_path.exists() {
175+
return Err(format!(
176+
"Android NDK path does not exist: {}",
177+
ndk_path.display()
178+
));
179+
}
180+
181+
let toolchain_file = ndk_path.join("build/cmake/android.toolchain.cmake");
182+
if !toolchain_file.exists() {
183+
return Err(format!(
184+
"Android NDK toolchain file not found: {}\n\
185+
This indicates an incomplete NDK installation.",
186+
toolchain_file.display()
187+
));
188+
}
189+
190+
Ok(())
191+
}
192+
165193
fn is_hidden(e: &DirEntry) -> bool {
166194
e.file_name()
167195
.to_str()
@@ -233,7 +261,7 @@ fn main() {
233261
);
234262

235263
// Bindings
236-
let bindings = bindgen::Builder::default()
264+
let mut bindings_builder = bindgen::Builder::default()
237265
.header("wrapper.h")
238266
.clang_arg(format!("-I{}", llama_src.join("include").display()))
239267
.clang_arg(format!("-I{}", llama_src.join("ggml/include").display()))
@@ -244,7 +272,149 @@ fn main() {
244272
.allowlist_type("ggml_.*")
245273
.allowlist_function("llama_.*")
246274
.allowlist_type("llama_.*")
247-
.prepend_enum_name(false)
275+
.prepend_enum_name(false);
276+
277+
// Configure Android-specific bindgen settings
278+
if matches!(target_os, TargetOs::Android) {
279+
// Detect Android NDK from environment variables
280+
let android_ndk = env::var("ANDROID_NDK")
281+
.or_else(|_| env::var("ANDROID_NDK_ROOT"))
282+
.or_else(|_| env::var("NDK_ROOT"))
283+
.or_else(|_| env::var("CARGO_NDK_ANDROID_NDK"))
284+
.or_else(|_| {
285+
// Try to auto-detect NDK from Android SDK
286+
if let Some(home) = env::home_dir() {
287+
let android_home = env::var("ANDROID_HOME")
288+
.or_else(|_| env::var("ANDROID_SDK_ROOT"))
289+
.unwrap_or_else(|_| format!("{}/Android/Sdk", home.display()));
290+
291+
let ndk_dir = format!("{}/ndk", android_home);
292+
if let Ok(entries) = std::fs::read_dir(&ndk_dir) {
293+
let mut versions: Vec<_> = entries
294+
.filter_map(|e| e.ok())
295+
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
296+
.filter_map(|e| e.file_name().to_str().map(|s| s.to_string()))
297+
.collect();
298+
versions.sort();
299+
if let Some(latest) = versions.last() {
300+
return Ok(format!("{}/{}", ndk_dir, latest));
301+
}
302+
}
303+
}
304+
Err(env::VarError::NotPresent)
305+
})
306+
.unwrap_or_else(|_| {
307+
panic!(
308+
"Android NDK not found. Please set one of: ANDROID_NDK, NDK_ROOT, ANDROID_NDK_ROOT\n\
309+
Current target: {}\n\
310+
Download from: https://developer.android.com/ndk/downloads",
311+
target_triple
312+
);
313+
});
314+
315+
// Get Android API level
316+
let android_api = env::var("ANDROID_API_LEVEL")
317+
.or_else(|_| env::var("ANDROID_PLATFORM").map(|p| p.replace("android-", "")))
318+
.or_else(|_| env::var("CARGO_NDK_ANDROID_PLATFORM").map(|p| p.replace("android-", "")))
319+
.unwrap_or_else(|_| "28".to_string());
320+
321+
// Determine host platform
322+
let host_tag = if cfg!(target_os = "macos") {
323+
"darwin-x86_64"
324+
} else if cfg!(target_os = "linux") {
325+
"linux-x86_64"
326+
} else if cfg!(target_os = "windows") {
327+
"windows-x86_64"
328+
} else {
329+
panic!("Unsupported host platform for Android NDK");
330+
};
331+
332+
// Map Rust target to Android architecture
333+
let android_target_prefix = if target_triple.contains("aarch64") {
334+
"aarch64-linux-android"
335+
} else if target_triple.contains("armv7") {
336+
"arm-linux-androideabi"
337+
} else if target_triple.contains("x86_64") {
338+
"x86_64-linux-android"
339+
} else if target_triple.contains("i686") {
340+
"i686-linux-android"
341+
} else {
342+
panic!("Unsupported Android target: {}", target_triple);
343+
};
344+
345+
// Setup Android toolchain paths
346+
let toolchain_path = format!("{}/toolchains/llvm/prebuilt/{}", android_ndk, host_tag);
347+
let sysroot = format!("{}/sysroot", toolchain_path);
348+
349+
// Validate toolchain existence
350+
if !std::path::Path::new(&toolchain_path).exists() {
351+
panic!(
352+
"Android NDK toolchain not found at: {}\n\
353+
Please ensure you have the correct Android NDK for your platform.",
354+
toolchain_path
355+
);
356+
}
357+
358+
// Find clang builtin includes
359+
let clang_builtin_includes = {
360+
let clang_lib_path = format!("{}/lib/clang", toolchain_path);
361+
std::fs::read_dir(&clang_lib_path).ok().and_then(|entries| {
362+
entries
363+
.filter_map(|e| e.ok())
364+
.find(|entry| {
365+
entry.file_type().map(|t| t.is_dir()).unwrap_or(false)
366+
&& entry
367+
.file_name()
368+
.to_str()
369+
.map(|name| name.chars().next().unwrap_or('0').is_ascii_digit())
370+
.unwrap_or(false)
371+
})
372+
.and_then(|entry| {
373+
let include_path =
374+
format!("{}/{}/include", clang_lib_path, entry.file_name().to_str()?);
375+
if std::path::Path::new(&include_path).exists() {
376+
Some(include_path)
377+
} else {
378+
None
379+
}
380+
})
381+
})
382+
};
383+
384+
// Configure bindgen for Android
385+
bindings_builder = bindings_builder
386+
.clang_arg(format!("--target={}", target_triple))
387+
.clang_arg(format!("--sysroot={}", sysroot))
388+
.clang_arg(format!("-D__ANDROID_API__={}", android_api))
389+
.clang_arg("-D__ANDROID__");
390+
391+
// Add include paths in correct order
392+
if let Some(ref builtin_includes) = clang_builtin_includes {
393+
bindings_builder = bindings_builder
394+
.clang_arg("-isystem")
395+
.clang_arg(builtin_includes);
396+
}
397+
398+
bindings_builder = bindings_builder
399+
.clang_arg("-isystem")
400+
.clang_arg(format!("{}/usr/include/{}", sysroot, android_target_prefix))
401+
.clang_arg("-isystem")
402+
.clang_arg(format!("{}/usr/include", sysroot))
403+
.clang_arg("-include")
404+
.clang_arg("stdbool.h")
405+
.clang_arg("-include")
406+
.clang_arg("stdint.h");
407+
408+
// Set additional clang args for cargo ndk compatibility
409+
if env::var("CARGO_SUBCOMMAND").as_deref() == Ok("ndk") {
410+
std::env::set_var(
411+
"BINDGEN_EXTRA_CLANG_ARGS",
412+
format!("--target={}", target_triple),
413+
);
414+
}
415+
}
416+
417+
let bindings = bindings_builder
248418
.generate()
249419
.expect("Failed to generate bindings");
250420

@@ -300,40 +470,92 @@ fn main() {
300470
config.static_crt(static_crt);
301471

302472
if matches!(target_os, TargetOs::Android) {
303-
// build flags for android taken from this doc
304-
// https://github.com/ggerganov/llama.cpp/blob/master/docs/android.md
473+
// Android NDK Build Configuration
305474
let android_ndk = env::var("ANDROID_NDK")
306-
.expect("Please install Android NDK and ensure that ANDROID_NDK env variable is set");
307-
308-
println!("cargo::rerun-if-env-changed=ANDROID_NDK");
475+
.or_else(|_| env::var("NDK_ROOT"))
476+
.or_else(|_| env::var("ANDROID_NDK_ROOT"))
477+
.unwrap_or_else(|_| {
478+
panic!(
479+
"Android NDK not found. Please set one of: ANDROID_NDK, NDK_ROOT, ANDROID_NDK_ROOT\n\
480+
Download from: https://developer.android.com/ndk/downloads"
481+
);
482+
});
309483

310-
config.define(
311-
"CMAKE_TOOLCHAIN_FILE",
312-
format!("{android_ndk}/build/cmake/android.toolchain.cmake"),
313-
);
314-
if env::var("ANDROID_PLATFORM").is_ok() {
315-
println!("cargo::rerun-if-env-changed=ANDROID_PLATFORM");
316-
} else {
317-
config.define("ANDROID_PLATFORM", "android-28");
484+
// Validate NDK installation
485+
if let Err(error) = validate_android_ndk(&android_ndk) {
486+
panic!("Android NDK validation failed: {}", error);
318487
}
319-
if target_triple.contains("aarch64") || target_triple.contains("armv7") {
320-
config.cflag("-march=armv8.7a");
321-
config.cxxflag("-march=armv8.7a");
488+
489+
// Rerun build script if NDK environment variables change
490+
println!("cargo:rerun-if-env-changed=ANDROID_NDK");
491+
println!("cargo:rerun-if-env-changed=NDK_ROOT");
492+
println!("cargo:rerun-if-env-changed=ANDROID_NDK_ROOT");
493+
494+
// Set CMake toolchain file for Android
495+
let toolchain_file = format!("{}/build/cmake/android.toolchain.cmake", android_ndk);
496+
config.define("CMAKE_TOOLCHAIN_FILE", &toolchain_file);
497+
498+
// Configure Android platform (API level)
499+
let android_platform = env::var("ANDROID_PLATFORM").unwrap_or_else(|_| {
500+
env::var("ANDROID_API_LEVEL")
501+
.map(|level| format!("android-{}", level))
502+
.unwrap_or_else(|_| "android-28".to_string())
503+
});
504+
505+
println!("cargo:rerun-if-env-changed=ANDROID_PLATFORM");
506+
println!("cargo:rerun-if-env-changed=ANDROID_API_LEVEL");
507+
config.define("ANDROID_PLATFORM", &android_platform);
508+
509+
// Map Rust target to Android ABI
510+
let android_abi = if target_triple.contains("aarch64") {
511+
"arm64-v8a"
512+
} else if target_triple.contains("armv7") {
513+
"armeabi-v7a"
322514
} else if target_triple.contains("x86_64") {
323-
config.cflag("-march=x86-64");
324-
config.cxxflag("-march=x86-64");
515+
"x86_64"
325516
} else if target_triple.contains("i686") {
326-
config.cflag("-march=i686");
327-
config.cxxflag("-march=i686");
517+
"x86"
328518
} else {
329-
// Rather than guessing just fail.
330-
panic!("Unsupported Android target {target_triple}");
519+
panic!(
520+
"Unsupported Android target: {}\n\
521+
Supported targets: aarch64-linux-android, armv7-linux-androideabi, i686-linux-android, x86_64-linux-android",
522+
target_triple
523+
);
524+
};
525+
526+
config.define("ANDROID_ABI", android_abi);
527+
528+
// Configure architecture-specific compiler flags
529+
match android_abi {
530+
"arm64-v8a" => {
531+
config.cflag("-march=armv8-a");
532+
config.cxxflag("-march=armv8-a");
533+
}
534+
"armeabi-v7a" => {
535+
config.cflag("-march=armv7-a");
536+
config.cxxflag("-march=armv7-a");
537+
config.cflag("-mfpu=neon");
538+
config.cxxflag("-mfpu=neon");
539+
config.cflag("-mthumb");
540+
config.cxxflag("-mthumb");
541+
}
542+
"x86_64" => {
543+
config.cflag("-march=x86-64");
544+
config.cxxflag("-march=x86-64");
545+
}
546+
"x86" => {
547+
config.cflag("-march=i686");
548+
config.cxxflag("-march=i686");
549+
}
550+
_ => {}
331551
}
552+
553+
// Android-specific CMake configurations
332554
config.define("GGML_LLAMAFILE", "OFF");
333-
if cfg!(feature = "shared-stdcxx") {
334-
println!("cargo:rustc-link-lib=dylib=stdc++");
335-
println!("cargo:rustc-link-lib=c++_shared");
336-
}
555+
556+
// Link Android system libraries
557+
println!("cargo:rustc-link-lib=log");
558+
println!("cargo:rustc-link-lib=android");
337559
}
338560

339561
if matches!(target_os, TargetOs::Linux)

0 commit comments

Comments
 (0)