Skip to content

Commit

Permalink
Append runtime_libs specified in manifest to APK (#106)
Browse files Browse the repository at this point in the history
Certain apps and crates `dlopen` libraries at runtime (which are not
listed in the shared library table by design, hence not found by
`llvm-readobj`).  Facilitate this by allowing the user to specify a list
of folders that need to be scanned for libraries to include in the final
package at while building.  This folder structure includes the platform
ABI to select the desired shared libraries.
  • Loading branch information
MarijnS95 authored May 13, 2023
1 parent c8efbd8 commit 875f933
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 60 deletions.
77 changes: 62 additions & 15 deletions xbuild/src/command/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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::<HashSet<_>>();

// 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)?;
}
}
}
Expand Down
46 changes: 36 additions & 10 deletions xbuild/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: ?Sized>(
&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<PathBuf> {
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(
Expand Down Expand Up @@ -302,6 +326,8 @@ struct RawConfig {
#[derive(Clone, Debug, Default, Deserialize)]
pub struct GenericConfig {
icon: Option<PathBuf>,
#[serde(default)]
runtime_libs: Vec<PathBuf>,
}

#[derive(Clone, Debug, Default, Deserialize)]
Expand Down
51 changes: 16 additions & 35 deletions xcommon/src/llvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HashSet<PathBuf>> {
// 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<OsString>,
) -> Result<(HashSet<PathBuf>, 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() {
Expand All @@ -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 `{}`",
Expand All @@ -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
Expand Down Expand Up @@ -93,17 +77,14 @@ fn list_needed_libs(library_path: &Path) -> Result<HashSet<String>> {
}

/// List names of shared libraries inside directory
fn find_libs_in_dir(path: &Path) -> Result<HashSet<String>> {
pub fn find_libs_in_dir(path: &Path) -> Result<HashSet<OsString>> {
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)
Expand Down

0 comments on commit 875f933

Please sign in to comment.