diff --git a/xbuild/src/command/build.rs b/xbuild/src/command/build.rs index 4a967b96..db9fd29f 100644 --- a/xbuild/src/command/build.rs +++ b/xbuild/src/command/build.rs @@ -7,6 +7,8 @@ use apk::Apk; use appbundle::AppBundle; use appimage::AppImage; use msix::Msix; +use std::collections::HashSet; +use std::ffi::OsStr; use std::path::Path; use xcommon::{Zip, ZipFileOptions}; @@ -86,7 +88,6 @@ pub fn build(env: &BuildEnv) -> Result<()> { let arch_dir = platform_dir.join(target.arch().to_string()); let cargo_dir = arch_dir.join("cargo"); let lib = env.cargo_artefact(&cargo_dir, target, CrateType::Cdylib)?; - apk.add_lib(target.android_abi(), &lib)?; let ndk = env.android_ndk(); @@ -131,21 +132,67 @@ pub fn build(env: &BuildEnv) -> Result<()> { ), ]; - let extra_libs = xcommon::llvm::list_needed_libs_recursively( - &lib, - &search_paths, - &provided_libs_paths, - ) - .with_context(|| { - format!( - "Failed to collect all required libraries for `{}` with `{:?}` available libraries and `{:?}` shippable libraries", - lib.display(), - provided_libs_paths, - search_paths + let mut explicit_libs = vec![lib]; + + // Collect the libraries the user wants to include + for runtime_lib_path in env.config().runtime_libs(env.target().platform()) { + let abi_dir = env + .cargo() + .package_root() + .join(runtime_lib_path) + .join(target.android_abi().android_abi()); + let entries = std::fs::read_dir(abi_dir)?; + for entry in entries { + let entry = entry?; + let path = entry.path(); + if !path.is_dir() && path.extension() == Some(OsStr::new("so")) { + explicit_libs.push(path); + } + } + } + + // Collect the names of libraries provided by the user, and assume these + // are available for other dependencies to link to, too. + let mut included_libs = explicit_libs + .iter() + .map(|p| p.file_name().unwrap().to_owned()) + .collect::>(); + + // Collect the names of all libraries that are available on Android + for provided_libs_path in provided_libs_paths { + included_libs + .extend(xcommon::llvm::find_libs_in_dir(provided_libs_path)?); + } + + // libc++_shared is bundled with the NDK but not available on-device + included_libs.remove(OsStr::new("libc++_shared.so")); + + let mut needs_cpp_shared = false; + + for lib in explicit_libs { + apk.add_lib(target.android_abi(), &lib)?; + + let (extra_libs, cpp_shared) = xcommon::llvm::list_needed_libs_recursively( + &lib, + &search_paths, + &included_libs, ) - })?; - for lib in &extra_libs { - apk.add_lib(target.android_abi(), lib)?; + .with_context(|| { + format!( + "Failed to collect all required libraries for `{}` with `{:?}` available libraries and `{:?}` shippable libraries", + lib.display(), + provided_libs_paths, + search_paths + ) + })?; + needs_cpp_shared |= cpp_shared; + for lib in &extra_libs { + apk.add_lib(target.android_abi(), lib)?; + } + } + if needs_cpp_shared { + let cpp_shared = ndk_sysroot_libs.join("libc++_shared.so"); + apk.add_lib(target.android_abi(), &cpp_shared)?; } } } diff --git a/xbuild/src/config.rs b/xbuild/src/config.rs index ae6b06f3..fd588daa 100644 --- a/xbuild/src/config.rs +++ b/xbuild/src/config.rs @@ -35,18 +35,42 @@ impl Config { }) } + /// Selects a generic config value from [`GenericConfig`], platform-specific + /// overrides first and otherwise falls back to a shared option in the root. + pub fn select_generic( + &self, + platform: Platform, + select: impl Fn(&GenericConfig) -> Option<&T>, + ) -> Option<&T> { + let generic = match platform { + Platform::Android => &self.android.generic, + Platform::Ios => &self.ios.generic, + Platform::Macos => &self.macos.generic, + Platform::Linux => &self.linux.generic, + Platform::Windows => &self.windows.generic, + }; + select(generic).or_else(|| select(&self.generic)) + } + pub fn icon(&self, platform: Platform) -> Option<&Path> { - let icon = match platform { - Platform::Android => self.android.generic.icon.as_deref(), - Platform::Ios => self.ios.generic.icon.as_deref(), - Platform::Macos => self.macos.generic.icon.as_deref(), - Platform::Linux => self.linux.generic.icon.as_deref(), - Platform::Windows => self.windows.generic.icon.as_deref(), + self.select_generic(platform, |g| g.icon.as_deref()) + } + + pub fn runtime_libs(&self, platform: Platform) -> Vec { + let generic = match platform { + Platform::Android => &self.android.generic, + Platform::Ios => &self.ios.generic, + Platform::Macos => &self.macos.generic, + Platform::Linux => &self.linux.generic, + Platform::Windows => &self.windows.generic, }; - if let Some(icon) = icon { - return Some(icon); - } - self.generic.icon.as_deref() + + generic + .runtime_libs + .iter() + .chain(&self.generic.runtime_libs) + .cloned() + .collect() } pub fn apply_rust_package( @@ -302,6 +326,8 @@ struct RawConfig { #[derive(Clone, Debug, Default, Deserialize)] pub struct GenericConfig { icon: Option, + #[serde(default)] + runtime_libs: Vec, } #[derive(Clone, Debug, Default, Deserialize)] diff --git a/xcommon/src/llvm.rs b/xcommon/src/llvm.rs index a97afe10..4fc2c5a1 100644 --- a/xcommon/src/llvm.rs +++ b/xcommon/src/llvm.rs @@ -2,34 +2,23 @@ use anyhow::{bail, ensure, Context, Result}; use std::collections::HashSet; +use std::ffi::{OsStr, OsString}; use std::path::{Path, PathBuf}; use std::process::Command; /// Returns the set of additional libraries that need to be bundled with /// the given library, scanned recursively. /// -/// Any libraries in `provided_libs_paths` will be treated as available, without -/// being emitted. Any other library not in `search_paths` or `provided_libs_paths` +/// Any libraries in `provided_libs` will be treated as available, without +/// being emitted. Any other library not in `search_paths` or `provided_libs` /// will result in an error. pub fn list_needed_libs_recursively( lib: &Path, search_paths: &[&Path], - provided_libs_paths: &[&Path], -) -> Result> { - // Create a view of all libraries that are available on Android - let mut provided = HashSet::new(); - for path in provided_libs_paths { - for lib in find_libs_in_dir(path).with_context(|| { - format!("Unable to list available libraries in `{}`", path.display()) - })? { - // libc++_shared is bundled with the NDK but not available on-device - if lib != "libc++_shared.so" { - provided.insert(lib); - } - } - } - + provided_libs: &HashSet, +) -> Result<(HashSet, bool)> { let mut to_copy = HashSet::new(); + let mut needs_cpp_shared = false; let mut artifacts = vec![lib.to_path_buf()]; while let Some(artifact) = artifacts.pop() { @@ -39,16 +28,11 @@ pub fn list_needed_libs_recursively( artifact.display() ) })? { - // c++_shared is available in the NDK but not on-device. - // Must be bundled with the apk if used: - // https://developer.android.com/ndk/guides/cpp-support#libc - let search_paths = if need == "libc++_shared.so" { - provided_libs_paths - } else { - search_paths - }; - - if provided.insert(need.clone()) { + if need == "libc++_shared.so" { + // c++_shared is available in the NDK but not on-device. Communicate that + // we need to copy it, once + needs_cpp_shared = true; + } else if !provided_libs.contains(OsStr::new(&need)) { if let Some(path) = find_library_path(search_paths, &need).with_context(|| { format!( "Could not iterate one or more search directories in `{:?}` while searching for library `{}`", @@ -64,7 +48,7 @@ pub fn list_needed_libs_recursively( } } - Ok(to_copy) + Ok((to_copy, needs_cpp_shared)) } /// List all required shared libraries as per the dynamic section @@ -93,17 +77,14 @@ fn list_needed_libs(library_path: &Path) -> Result> { } /// List names of shared libraries inside directory -fn find_libs_in_dir(path: &Path) -> Result> { +pub fn find_libs_in_dir(path: &Path) -> Result> { let mut libs = HashSet::new(); let entries = std::fs::read_dir(path)?; for entry in entries { let entry = entry?; - if !entry.path().is_dir() { - if let Some(file_name) = entry.file_name().to_str() { - if file_name.ends_with(".so") { - libs.insert(file_name.to_string()); - } - } + let path = entry.path(); + if !path.is_dir() && path.extension() == Some(OsStr::new("so")) { + libs.insert(entry.file_name().to_owned()); } } Ok(libs)