Skip to content

Commit 3b4a0d1

Browse files
committed
fix: iOS compilation issues (fat binary and runtime symbols)
This commit fixes two major iOS compilation issues: 1. **Fat Binary Support**: iOS precompiled libraries (.a files) in xcframework are universal binaries containing multiple architectures (x86_64, arm64). Rust linker doesn't support linking fat binaries directly, causing "Unsupported archive identifier" errors. Solution: Added extract_thin_lib_for_ios() function to detect and extract the specific architecture needed for each target using lipo. 2. **Missing Runtime Symbols**: iOS linking failed with undefined symbols: - ___chkstk_darwin: C++ stack check function - ___isPlatformVersionAtLeast: iOS platform version check Solution: - Added ios_link_search_path() to locate clang runtime libraries - Link libclang_rt.ios.a (or libclang_rt.iossim.a for simulator) - Set minimum iOS deployment target via -platform_version linker flag Tested on: - aarch64-apple-ios (iOS device) - aarch64-apple-ios-sim (iOS simulator) Fixes #67
1 parent 8bb029e commit 3b4a0d1

File tree

1 file changed

+156
-5
lines changed

1 file changed

+156
-5
lines changed

crates/sherpa-rs-sys/build.rs

Lines changed: 156 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,68 @@ fn copy_folder(src: &Path, dst: &Path) {
9696
}
9797
}
9898

99+
/// Extract a specific architecture from a fat binary for iOS
100+
/// Returns the path to the extracted single-architecture library
101+
fn extract_thin_lib_for_ios(lib_path: &Path, target: &str, out_dir: &Path) -> PathBuf {
102+
debug_log!("Checking if {} is a fat binary", lib_path.display());
103+
104+
// Check if it's a fat binary using lipo
105+
let lipo_info = Command::new("lipo")
106+
.arg("-info")
107+
.arg(lib_path)
108+
.output();
109+
110+
if let Ok(output) = lipo_info {
111+
let info_str = String::from_utf8_lossy(&output.stdout);
112+
debug_log!("lipo info: {}", info_str);
113+
114+
// If it contains "Architectures in the fat file", it's a fat binary
115+
if info_str.contains("Architectures in the fat file") || info_str.contains("universal binary") {
116+
// Determine the target architecture
117+
let arch = if target.contains("x86_64") {
118+
"x86_64"
119+
} else if target.contains("aarch64") || target.contains("arm64") {
120+
"arm64"
121+
} else {
122+
debug_log!("Unknown target architecture for {}", target);
123+
return lib_path.to_path_buf();
124+
};
125+
126+
debug_log!("Extracting {} architecture from fat binary", arch);
127+
128+
// Create output path for thin library
129+
// Use the original filename to avoid library name conflicts
130+
let file_name = lib_path.file_name().unwrap();
131+
let thin_lib_path = out_dir.join(file_name);
132+
133+
// Extract the specific architecture
134+
let extract_result = Command::new("lipo")
135+
.arg(lib_path)
136+
.arg("-thin")
137+
.arg(arch)
138+
.arg("-output")
139+
.arg(&thin_lib_path)
140+
.status();
141+
142+
match extract_result {
143+
Ok(status) if status.success() => {
144+
debug_log!("Successfully extracted {} to {}", arch, thin_lib_path.display());
145+
return thin_lib_path;
146+
}
147+
Ok(status) => {
148+
debug_log!("lipo failed with status: {}", status);
149+
}
150+
Err(e) => {
151+
debug_log!("Failed to run lipo: {}", e);
152+
}
153+
}
154+
}
155+
}
156+
157+
// If not a fat binary or extraction failed, return original path
158+
lib_path.to_path_buf()
159+
}
160+
99161
fn extract_lib_names(out_dir: &Path, is_dynamic: bool, target_os: &str) -> Vec<String> {
100162
let lib_pattern = if target_os == "windows" {
101163
"*.lib"
@@ -197,6 +259,38 @@ fn macos_link_search_path() -> Option<String> {
197259
None
198260
}
199261

262+
fn ios_link_search_path() -> Option<String> {
263+
// Get the clang version
264+
let output = Command::new("clang")
265+
.arg("--version")
266+
.output()
267+
.ok()?;
268+
269+
if !output.status.success() {
270+
debug_log!("failed to run 'clang --version'");
271+
return None;
272+
}
273+
274+
let version_str = String::from_utf8_lossy(&output.stdout);
275+
// Extract version number (e.g., "Apple clang version 17.0.0" -> "17")
276+
let version = version_str
277+
.lines()
278+
.next()?
279+
.split_whitespace()
280+
.find_map(|s| {
281+
if s.chars().next()?.is_digit(10) {
282+
Some(s.split('.').next()?.to_string())
283+
} else {
284+
None
285+
}
286+
})?;
287+
288+
Some(format!(
289+
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/{}/lib/darwin",
290+
version
291+
))
292+
}
293+
200294
fn rerun_on_env_changes(vars: &[&str]) {
201295
for env in vars {
202296
println!("cargo::rerun-if-env-changed={env}");
@@ -407,10 +501,26 @@ fn main() {
407501

408502
debug_log!("dist libs: {:?}", dist.libs);
409503
if let Some(libs) = dist.libs {
410-
for lib in libs.iter() {
411-
let lib_path = cache_dir.join(lib);
412-
let lib_parent = lib_path.parent().unwrap();
413-
add_search_path(lib_parent);
504+
// For iOS, we need to handle fat binaries (universal binaries with multiple architectures)
505+
// Extract the specific architecture needed for the target
506+
if target.contains("ios") {
507+
debug_log!("Processing iOS libraries, checking for fat binaries");
508+
for lib in libs.iter() {
509+
let lib_path = cache_dir.join(lib);
510+
511+
// Extract thin library if it's a fat binary
512+
let processed_lib_path = extract_thin_lib_for_ios(&lib_path, &target, &out_dir);
513+
514+
// Add the directory containing the processed library to search path
515+
let lib_parent = processed_lib_path.parent().unwrap();
516+
add_search_path(lib_parent);
517+
}
518+
} else {
519+
for lib in libs.iter() {
520+
let lib_path = cache_dir.join(lib);
521+
let lib_parent = lib_path.parent().unwrap();
522+
add_search_path(lib_parent);
523+
}
414524
}
415525

416526
sherpa_libs = libs
@@ -510,13 +620,54 @@ fn main() {
510620
link_lib("msvcrtd", true);
511621
}
512622

513-
// macOS
623+
// macOS and iOS common frameworks
514624
if target_os == "macos" || target_os == "ios" {
515625
link_framework("CoreML");
516626
link_framework("Foundation");
517627
link_lib("c++", true);
518628
}
519629

630+
// iOS specific configuration
631+
if target_os == "ios" {
632+
// Set minimum iOS deployment target to avoid linker errors
633+
// This prevents "___isPlatformVersionAtLeast" undefined symbol errors
634+
println!("cargo:rustc-link-arg=-Wl,-platform_version,ios,13.0,13.0");
635+
636+
// Add rpath for finding dependencies
637+
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
638+
639+
// Link clang runtime for iOS to resolve symbols like ___chkstk_darwin
640+
if let Some(clang_path) = ios_link_search_path() {
641+
debug_log!("iOS clang runtime path: {}", clang_path);
642+
643+
// Choose the correct runtime library based on target
644+
let clang_rt_lib = if target.contains("sim") {
645+
"libclang_rt.iossim.a"
646+
} else {
647+
"libclang_rt.ios.a"
648+
};
649+
650+
let clang_rt_path = Path::new(&clang_path).join(clang_rt_lib);
651+
652+
// Extract thin library from fat binary
653+
let thin_clang_rt = extract_thin_lib_for_ios(&clang_rt_path, &target, &out_dir);
654+
655+
// Add the directory containing the thin library to search path
656+
let clang_rt_parent = thin_clang_rt.parent().unwrap();
657+
add_search_path(clang_rt_parent);
658+
659+
// Extract lib name for linking
660+
let lib_name = thin_clang_rt
661+
.file_stem()
662+
.and_then(|s| s.to_str())
663+
.and_then(|s| s.strip_prefix("lib"))
664+
.unwrap_or("clang_rt.ios");
665+
666+
debug_log!("Linking {}", lib_name);
667+
link_lib(lib_name, false);
668+
}
669+
}
670+
520671
// Linux
521672
if target_os == "linux" || target == "android" {
522673
link_lib("stdc++", true);

0 commit comments

Comments
 (0)