Skip to content

Commit

Permalink
Use llvm-readelf to detect and bundle needed dynamic libs for Android
Browse files Browse the repository at this point in the history
Dynamic libraries that a Rust library links against while not being
present on the target platform (`libc++_shared.so` and other custom
libraries) need to be bundled in the APK specifically.  This approach is
adopted frm `android-ndk-rs` while switching from `readelf` to
`llvm-readelf`.
  • Loading branch information
MarijnS95 committed Jun 13, 2022
1 parent 6ccbde9 commit b6f1e23
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 14 deletions.
8 changes: 3 additions & 5 deletions mvn/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::metadata::Metadata;
use crate::package::Artifact;
use crate::pom::Pom;
use anyhow::Result;
use anyhow::{Context, Result};
use pubgrub::error::PubGrubError;
use pubgrub::range::Range;
use pubgrub::report::{DefaultStringReporter, Reporter};
Expand Down Expand Up @@ -126,17 +126,15 @@ impl<D: Download> Maven<D> {
anyhow::ensure!(downloaded, "metadata not found for {}", package);
}
let s = std::fs::read_to_string(path)?;
let metadata =
quick_xml::de::from_str(&s).map_err(|err| anyhow::anyhow!("{}: {}", err, s))?;
let metadata = quick_xml::de::from_str(&s).context(s)?;
Ok(metadata)
}

fn pom(&self, artifact: Artifact) -> Result<Pom> {
match self.artifact(artifact, "pom") {
Ok(path) => {
let s = std::fs::read_to_string(path)?;
let pom =
quick_xml::de::from_str(&s).map_err(|err| anyhow::anyhow!("{}: {}", err, s))?;
let pom = quick_xml::de::from_str(&s).context(s)?;
Ok(pom)
}
Err(err) => {
Expand Down
57 changes: 51 additions & 6 deletions xbuild/src/cargo/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{Arch, CompileTarget, Opt, Platform};
use anyhow::Result;
use crate::{CompileTarget, Opt};
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;

Expand Down Expand Up @@ -101,7 +101,7 @@ impl Cargo {
artifact: Option<Artifact>,
ty: CrateType,
) -> Result<PathBuf> {
let arch_dir = if target.platform() == Platform::host()? && target.arch() == Arch::host()? {
let arch_dir = if target.is_host()? {
target_dir.to_path_buf()
} else {
target_dir.join(target.rust_triple()?)
Expand All @@ -119,6 +119,51 @@ impl Cargo {
);
Ok(bin_path)
}

pub fn lib_search_paths(
&self,
target_dir: &Path,
target: CompileTarget,
) -> Result<Vec<PathBuf>> {
let arch_dir = if target.is_host()? {
target_dir.to_path_buf()
} else {
target_dir.join(target.rust_triple()?)
};
let opt_dir = arch_dir.join(target.opt().to_string());
let build_deps_dir = opt_dir.join("build");

let mut paths = vec![];

for dep_dir in build_deps_dir.read_dir().with_context(|| {
format!(
"Scanning crate directories in `{}`",
build_deps_dir.display()
)
})? {
let output_file = dep_dir?.path().join("output");
if output_file.is_file() {
use std::{
fs::File,
io::{BufRead, BufReader},
};
for line in BufReader::new(File::open(output_file)?).lines() {
let line = line?;
if let Some(link_search) = line.strip_prefix("cargo:rustc-link-search=") {
let (kind, path) =
link_search.split_once('=').unwrap_or(("all", link_search));
match kind {
// FIXME: which kinds of search path we interested in
"dependency" | "native" | "all" => paths.push(path.into()),
_ => (),
};
}
}
}
}

Ok(paths)
}
}

pub struct CargoBuild {
Expand All @@ -136,10 +181,10 @@ impl CargoBuild {
target_dir: &Path,
offline: bool,
) -> Result<Self> {
let triple = if target.platform() != Platform::host()? || target.arch() != Arch::host()? {
Some(target.rust_triple()?)
} else {
let triple = if target.is_host()? {
None
} else {
Some(target.rust_triple()?)
};
let mut cmd = Command::new("cargo");
cmd.current_dir(root_dir);
Expand Down
64 changes: 61 additions & 3 deletions xbuild/src/command/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::download::DownloadManager;
use crate::flutter::depfile::depfile_is_dirty;
use crate::task::TaskRunner;
use crate::{BuildEnv, Format, Opt, Platform};
use anyhow::Result;
use anyhow::{Context, Result};
use apk::Apk;
use appbundle::AppBundle;
use appimage::AppImage;
Expand Down Expand Up @@ -261,9 +261,67 @@ pub fn build(env: &BuildEnv) -> Result<()> {
if has_lib {
for target in env.target().compile_targets() {
let arch_dir = platform_dir.join(target.arch().to_string());
let lib =
env.cargo_artefact(&arch_dir.join("cargo"), target, CrateType::Cdylib)?;
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();

let deps_dir = {
let arch_dir = if target.is_host()? {
cargo_dir.to_path_buf()
} else {
cargo_dir.join(target.rust_triple()?)
};
let opt_dir = arch_dir.join(target.opt().to_string());
opt_dir.join("deps")
};

let mut search_paths = env
.cargo()
.lib_search_paths(&cargo_dir, target)
.with_context(|| {
format!(
"Finding libraries in `{}` for {:?}",
cargo_dir.display(),
target
)
})?;
search_paths.push(deps_dir);
let search_paths = search_paths.iter().map(AsRef::as_ref).collect::<Vec<_>>();

let ndk_sysroot_libs = ndk.join("usr/lib").join(target.ndk_triple());
let provided_libs_paths = [
ndk_sysroot_libs.as_path(),
&*ndk_sysroot_libs.join(
// Use libraries (symbols) from the lowest NDK that is supported by the application,
// to prevent inadvertently making newer APIs available:
// https://developer.android.com/ndk/guides/sdk-versions
env.manifest()
.android()
.sdk
.min_sdk_version
.unwrap()
.to_string(),
),
];

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
)
})?;
for lib in &extra_libs {
apk.add_lib(target.android_abi(), lib)?;
}
}
}

Expand Down
1 change: 1 addition & 0 deletions xbuild/src/command/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ impl Default for Doctor {
Check::new("clang++", Some(VersionCheck::new("--version", 0, 2))),
Check::new("llvm-ar", None),
Check::new("llvm-lib", None),
Check::new("llvm-readelf", Some(VersionCheck::new("--version", 1, 4))),
Check::new("lld", Some(VersionCheck::new("-flavor ld --version", 0, 1))),
Check::new("lld-link", Some(VersionCheck::new("--version", 0, 1))),
Check::new("lldb", Some(VersionCheck::new("--version", 0, 2))),
Expand Down
5 changes: 5 additions & 0 deletions xbuild/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ impl CompileTarget {
(Arch::Arm64, Platform::Ios) => "aarch64-apple-ios",
(Arch::Arm64, Platform::Linux) => "aarch64-unknown-linux-gnu",
(Arch::Arm64, Platform::Macos) => "aarch64-apple-darwin",
(Arch::X64, Platform::Android) => "x86_64-linux-android",
(Arch::X64, Platform::Linux) => "x86_64-unknown-linux-gnu",
(Arch::X64, Platform::Macos) => "x86_64-apple-darwin",
(Arch::X64, Platform::Windows) => "x86_64-pc-windows-msvc",
Expand All @@ -310,6 +311,10 @@ impl CompileTarget {
),
})
}

pub fn is_host(self) -> Result<bool> {
Ok(self.platform() == Platform::host()? && self.arch() == Arch::host()?)
}
}

impl std::fmt::Display for CompileTarget {
Expand Down
1 change: 1 addition & 0 deletions xcommon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ license = "Apache-2.0 OR MIT"
[dependencies]
anyhow = "1.0.53"
byteorder = "1.4.3"
dunce = "1"
image = { version = "0.24.0", default-features = false, features = ["png"] }
pem = "1.0.2"
rasn = "0.5.0"
Expand Down
2 changes: 2 additions & 0 deletions xcommon/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod llvm;

use anyhow::Result;
use byteorder::{LittleEndian, ReadBytesExt};
use image::imageops::FilterType;
Expand Down
121 changes: 121 additions & 0 deletions xcommon/src/llvm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//! LLVM utilities

use anyhow::{bail, ensure, Context, Result};
use std::collections::HashSet;
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`
/// 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);
}
}
}

let mut to_copy = HashSet::new();

let mut artifacts = vec![lib.to_path_buf()];
while let Some(artifact) = artifacts.pop() {
for need in list_needed_libs(&artifact).with_context(|| {
format!(
"Unable to read needed libraries from `{}`",
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 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 `{}`",
search_paths, need
)
})? {
to_copy.insert(path.clone());
artifacts.push(path);
} else {
bail!("Shared library `{}` not found", need);
}
}
}
}

Ok(to_copy)
}

/// List all required shared libraries as per the dynamic section
fn list_needed_libs(library_path: &Path) -> Result<HashSet<String>> {
let mut readelf = Command::new("llvm-readelf");
let readelf = readelf.arg("--needed-libs").arg(library_path);
let output = readelf
.output()
.with_context(|| format!("Failed to run `{:?}`", readelf))?;
ensure!(
output.status.success(),
"Failed to run `{:?}`: {}",
readelf,
output.status
);
let output = std::str::from_utf8(&output.stdout).unwrap();
let output = output.strip_prefix("NeededLibraries [\n").unwrap();
let output = output.strip_suffix("]\n").unwrap();
let mut needed = HashSet::new();

for line in output.lines() {
let lib = line.trim_start();
needed.insert(lib.to_string());
}
Ok(needed)
}

/// List names of shared libraries inside directory
fn find_libs_in_dir(path: &Path) -> Result<HashSet<String>> {
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());
}
}
}
}
Ok(libs)
}

/// Resolves native library using search paths
fn find_library_path(paths: &[&Path], library: &str) -> Result<Option<PathBuf>> {
for path in paths {
let lib_path = path.join(library);
if lib_path.exists() {
return Ok(Some(dunce::canonicalize(lib_path)?));
}
}
Ok(None)
}

0 comments on commit b6f1e23

Please sign in to comment.