Skip to content

Commit

Permalink
fix: mobile linking (#56)
Browse files Browse the repository at this point in the history
* fix: linking on android and ios
  • Loading branch information
thewh1teagle authored Dec 25, 2024
1 parent 2406430 commit 298450f
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ vits-*
sherpa-onnx-kws-*
.tmp/
jniLibs/
build/
5 changes: 3 additions & 2 deletions BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" `

### Debug build

For debug the build process of sherpa-onnx, please set `BUILD_DEBUG=1` environment variable before build.
For debug the build process of sherpa-onnx, please set `SHERPA_BUILD_DEBUG=1` environment variable before build.

## Release new version

Expand Down Expand Up @@ -220,7 +220,7 @@ You must install NDK from Android Studio settings.
rustup target add aarch64-linux-android
cargo install cargo-ndk
export NDK_HOME="$HOME/Library/Android/sdk/ndk/27.0.12077973"
cargo ndk -t arm64-v8a -o ./jniLibs build --release
cargo ndk -t arm64-v8a build --release
```

## Build for IOS
Expand All @@ -245,5 +245,6 @@ rustup target add aarch64-apple-ios-sim
Build

```console
export RUSTFLAGS="-C link-arg=-fapple-link-rtlib" # See https://github.com/bmrlab/gendam/issues/96
cargo build --release --target aarch64-apple-ios
```
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Rust bindings to [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx)
- Windows
- Linux
- macOS
- Android
- IOS

## Install

Expand Down
4 changes: 2 additions & 2 deletions crates/sherpa-rs-sys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ include = [
bindgen = "0.69.4"
cmake = "0.1"
glob = "0.3.1"
lazy_static = { version = "1.5.0" }

# Download binaries
ureq = { version = "2.1", optional = true, default-features = false, features = [
"tls",
Expand All @@ -49,7 +51,6 @@ sha2 = { version = "0.10", optional = true }
dirs = { version = "5.0.1", optional = true }
serde_json = { version = "1.0.134", optional = true }
serde = { version = "1.0.216", features = ["derive"], optional = true }
lazy_static = { version = "1.5.0", optional = true }


[features]
Expand All @@ -63,7 +64,6 @@ download-binaries = [
"dep:dirs",
"dep:serde_json",
"dep:serde",
"dep:lazy_static",
]
static = []
tts = []
Expand Down
120 changes: 89 additions & 31 deletions crates/sherpa-rs-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,28 @@ lazy_static::lazy_static! {
static ref RUST_CLANG_TARGET_MAP: HashMap<String, String> = {
let mut m = HashMap::new();
m.insert("aarch64-linux-android".to_string(), "armv8-linux-androideabi".to_string());
m.insert("aarch64-apple-ios-sim".to_string(), "arm64-apple-ios-sim".to_string());
m
};
}

fn hard_link(src: PathBuf, dst: PathBuf) {
fn link_lib(lib: &str, is_dynamic: bool) {
let lib_kind = if is_dynamic { "dylib" } else { "static" };
debug_log!("cargo:rustc-link-lib={}={}", lib_kind, lib);
println!("cargo:rustc-link-lib={}={}", lib_kind, lib);
}

fn link_framework(framework: &str) {
debug_log!("cargo:rustc-link-lib=framework={}", framework);
println!("cargo:rustc-link-lib=framework={}", framework);
}

fn add_search_path<P: AsRef<Path>>(path: P) {
debug_log!("cargo:rustc-link-search={}", path.as_ref().display());
println!("cargo:rustc-link-search={}", path.as_ref().display());
}

fn copy_file(src: PathBuf, dst: PathBuf) {
if let Err(err) = std::fs::hard_link(&src, &dst) {
debug_log!("Failed to hardlink {:?}. fallback to copy.", err);
fs::copy(&src, &dst)
Expand Down Expand Up @@ -215,6 +232,7 @@ fn main() {
]);

let target = env::var("TARGET").unwrap();
let is_mobile = target.contains("android") || target.contains("ios");
debug_log!("TARGET: {:?}", target);
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

Expand Down Expand Up @@ -281,12 +299,15 @@ fn main() {
bindings_builder = bindings_builder.clang_arg(format!("--target={}", clang_target));
}

debug_log!("Generating bindings...");
let bindings_builder = bindings_builder
.generate()
.expect("Failed to generate bindings");

// Write the generated bindings to an output file
let bindings_path = out_dir.join("bindings.rs");

debug_log!("Writing bindings to {:?}", bindings_path);
bindings_builder
.write_to_file(bindings_path)
.expect("Failed to write bindings");
Expand Down Expand Up @@ -339,7 +360,6 @@ fn main() {
.always_configure(false);

let mut sherpa_libs: Vec<String> = Vec::new();
let sherpa_libs_kind = if is_dynamic { "dylib" } else { "static" };

// Download libraries, cache and set SHERPA_LIB_PATH

Expand All @@ -352,7 +372,8 @@ fn main() {
debug_log!("Download binaries enabled");
// debug_log!("Dist table: {:?}", DIST_TABLE.targets);
// Try download sherpa libs and set SHERPA_LIB_PATH
if let Some(dist) = DIST_TABLE.get(&target, is_dynamic) {
if let Some(dist) = DIST_TABLE.get(&target, &mut is_dynamic) {
debug_log!("is_dynamic after: {}", is_dynamic);
optional_dist = Some(dist.clone());
let mut cache_dir = if let Some(dir) = get_cache_dir() {
dir.join(target.clone()).join(&dist.checksum)
Expand All @@ -365,8 +386,16 @@ fn main() {
cache_dir = env::var("OUT_DIR").unwrap().into();
}
debug_log!("Cache dir: {}", cache_dir.display());

let lib_dir = cache_dir.join(&dist.name);
if !lib_dir.exists() {
// if is mobile then check if cache dir not empty
let cache_dir_empty = cache_dir
.read_dir()
.map(|mut entries| entries.next().is_none())
.unwrap_or(true);
// Check if cache directory exists
// Sherpa uses special directory structure for mobile
if (!lib_dir.exists() && !is_mobile) || (is_mobile && cache_dir_empty) {
let downloaded_file = fetch_file(&dist.url);
let hash = sha256(&downloaded_file);
// verify checksum
Expand All @@ -382,15 +411,39 @@ fn main() {
}

// In Android, we need to set SHERPA_LIB_PATH to the cache directory sincie it has jniLibs
if target.contains("android") || target.contains("ios") {
env::set_var("SHERPA_LIB_PATH", cache_dir);
if is_mobile {
env::set_var("SHERPA_LIB_PATH", &cache_dir);
} else {
env::set_var("SHERPA_LIB_PATH", cache_dir.join(&dist.name));
}

debug_log!("dist libs: {:?}", dist.libs);
if let Some(libs) = dist.libs {
sherpa_libs = libs;
for lib in libs.iter() {
let lib_path = cache_dir.join(lib);
let lib_parent = lib_path.parent().unwrap();
add_search_path(lib_parent);
}

sherpa_libs = libs
.iter()
.map(|p| {
Path::new(p)
.file_name()
.unwrap()
.to_string_lossy()
// Remove lib prefix
.strip_prefix("lib")
.unwrap_or_else(|| p)
// Remove .so
.replace(".so", "")
// Remove .dylib
.replace(".dylib", "")
// Remove .a
.replace(".a", "")
.to_string()
})
.collect();
} else {
sherpa_libs = extract_lib_names(&lib_dir, is_dynamic, &target_os);
}
Expand All @@ -403,70 +456,75 @@ fn main() {
// Skip build if SHERPA_LIB_PATH specified
debug_log!("Skpping build with Cmake...");
debug_log!("SHERPA_LIB_PATH: {}", sherpa_lib_path);
println!(
"cargo:rustc-link-search={}",
Path::new(&sherpa_lib_path).join("lib").display()
);
sherpa_libs = extract_lib_names(Path::new(&sherpa_lib_path), is_dynamic, &target_os);
add_search_path(Path::new(&sherpa_lib_path).join("lib"));
if sherpa_libs.is_empty() {
sherpa_libs = extract_lib_names(Path::new(&sherpa_lib_path), is_dynamic, &target_os);
}
} else {
// Build with CMake
let bindings_dir = config.build();
println!("cargo:rustc-link-search={}", bindings_dir.display());
add_search_path(&bindings_dir);

// Extract libs on desktop platforms
if !target.contains("android") && !target.contains("ios") {
if !is_mobile {
sherpa_libs = extract_lib_names(&bindings_dir, is_dynamic, &target_os);
}
}

// Search paths
println!("cargo:rustc-link-search={}", out_dir.join("lib").display());
debug_log!("Sherpa libs: {:?}", sherpa_libs);
add_search_path(out_dir.join("lib"));

for lib in sherpa_libs {
if lib.contains("cxx") {
continue;
}
debug_log!(
"LINK {}",
format!("cargo:rustc-link-lib={}={}", sherpa_libs_kind, lib)
);
println!("cargo:rustc-link-lib={}={}", sherpa_libs_kind, lib);
link_lib(&lib, is_dynamic);
}

// Windows debug
if cfg!(all(debug_assertions, windows)) {
println!("cargo:rustc-link-lib=dylib=msvcrtd");
link_lib("msvcrtd", true);
}

// macOS
if target_os == "macos" {
println!("cargo:rustc-link-lib=framework=Foundation");
println!("cargo:rustc-link-lib=c++");
if target_os == "macos" || target_os == "ios" {
link_framework("CoreML");
link_framework("Foundation");
link_lib("c++", true);
}

// Linux
if target_os == "linux" || target == "android" {
println!("cargo:rustc-link-lib=dylib=stdc++");
link_lib("stdc++", true);
}

if target.contains("apple") {
// macOS
if target_os == "macos" {
// On (older) OSX we need to link against the clang runtime,
// which is hidden in some non-default path.
//
// More details at https://github.com/alexcrichton/curl-rust/issues/279.
if let Some(path) = macos_link_search_path() {
println!("cargo:rustc-link-lib=clang_rt.osx");
println!("cargo:rustc-link-search={}", path);
add_search_path(path);
link_lib("clang_rt.osx", is_dynamic);
}
}

// TODO: add rpath for Android and iOS so it can find its dependencies in the same directory of executable
// if is_mobile {
// // Add rpath for Android and iOS so that the shared library can find its dependencies in the same directory as well
// println!("cargo:rustc-link-arg=-Wl,-rpath,'$ORIGIN'");
// }

// copy DLLs to target
if is_dynamic {
let mut libs_assets = extract_lib_assets(&out_dir, &target_os);
if let Ok(sherpa_lib_path) = env::var("SHERPA_LIB_PATH") {
libs_assets.extend(extract_lib_assets(Path::new(&sherpa_lib_path), &target_os));
}

#[cfg(feature = "download-binaries")]
if let Some(dist) = optional_dist {
if let Some(assets) = dist.libs {
if let Ok(sherpa_lib_path) = env::var("SHERPA_LIB_PATH") {
Expand All @@ -483,23 +541,23 @@ fn main() {
let dst = target_dir.join(filename);
// debug_log!("HARD LINK {} TO {}", asset.display(), dst.display());
if !dst.exists() {
hard_link(asset.clone(), dst);
copy_file(asset.clone(), dst);
}

// Copy DLLs to examples as well
if target_dir.join("examples").exists() {
let dst = target_dir.join("examples").join(filename);
// debug_log!("HARD LINK {} TO {}", asset.display(), dst.display());
if !dst.exists() {
hard_link(asset.clone(), dst);
copy_file(asset.clone(), dst);
}
}

// Copy DLLs to target/profile/deps as well for tests
let dst = target_dir.join("deps").join(filename);
// debug_log!("HARD LINK {} TO {}", asset.display(), dst.display());
if !dst.exists() {
hard_link(asset.clone(), dst);
copy_file(asset.clone(), dst);
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/sherpa-rs-sys/dist.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"android": {
"archive": "sherpa-onnx-{tag}-android.tar.bz2",
"is_dynamic": true,
"arch": {
"aarch64-linux-android": "arm64-v8a",
"x86_64-linux-android": "x86_64",
Expand All @@ -32,6 +33,7 @@
},
"ios": {
"archive": "sherpa-onnx-{tag}-ios.tar.bz2",
"is_dynamic": false,
"arch": {
"aarch64-apple-ios": "ios-arm64",
"aarch64-apple-ios-sim": "ios-arm64-simulator"
Expand Down
15 changes: 13 additions & 2 deletions crates/sherpa-rs-sys/src/download_binaries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ impl DistTable {
table
}

pub fn get(&self, target: &str, is_dynamic: bool) -> Option<Dist> {
pub fn get(&self, target: &str, is_dynamic: &mut bool) -> Option<Dist> {
debug_log!("Extracting dist for target: {}", target);
// debug_log!("dist table: {:?}", self);
let target_dist = if target.contains("android") {
Expand All @@ -151,10 +151,14 @@ impl DistTable {
serde_json::to_string(target_dist).unwrap()
);
let archive = if target_dist.get("archive").is_some() {
// archive name
// static/dynamic located in 'is_dynamic' field
target_dist.get("archive").unwrap().as_str().unwrap()
} else if is_dynamic {
} else if *is_dynamic {
// dynamic archive name
target_dist.get("dynamic").unwrap().as_str().unwrap()
} else {
// static archive name
target_dist.get("static").unwrap().as_str().unwrap()
};
let name = archive.replace(".tar.bz2", "");
Expand All @@ -181,6 +185,13 @@ impl DistTable {
let url = self.url.replace("{archive}", archive);
let checksum = DIST_CHECKSUM.get(archive).unwrap();

// modify is_dynamic
debug_log!("checking is_dynamic");
if let Some(target_dist) = target_dist.get("is_dynamic") {
*is_dynamic = target_dist.as_bool().unwrap();
debug_log!("is_dynamic: {}", *is_dynamic);
}

let dist = Dist {
url,
name,
Expand Down

0 comments on commit 298450f

Please sign in to comment.