Skip to content

Commit d8e1692

Browse files
committed
Use llvm-readelf to detect and bundle needed dynamic libs for Android
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`.
1 parent 09f876d commit d8e1692

File tree

7 files changed

+202
-7
lines changed

7 files changed

+202
-7
lines changed

xbuild/src/cargo/mod.rs

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Arch, CompileTarget, Opt, Platform};
1+
use crate::{CompileTarget, Opt};
22
use anyhow::Result;
33
use std::path::{Path, PathBuf};
44
use std::process::Command;
@@ -101,7 +101,7 @@ impl Cargo {
101101
artifact: Option<Artifact>,
102102
ty: CrateType,
103103
) -> Result<PathBuf> {
104-
let arch_dir = if target.platform() == Platform::host()? && target.arch() == Arch::host()? {
104+
let arch_dir = if target.is_host()? {
105105
target_dir.to_path_buf()
106106
} else {
107107
target_dir.join(target.rust_triple()?)
@@ -119,6 +119,45 @@ impl Cargo {
119119
);
120120
Ok(bin_path)
121121
}
122+
123+
pub fn lib_search_paths(
124+
&self,
125+
target_dir: &Path,
126+
target: CompileTarget,
127+
) -> Result<Vec<PathBuf>> {
128+
let arch_dir = if target.is_host()? {
129+
target_dir.to_path_buf()
130+
} else {
131+
target_dir.join(target.rust_triple()?)
132+
};
133+
let opt_dir = arch_dir.join(target.opt().to_string());
134+
135+
let mut paths = vec![];
136+
137+
for dep_dir in opt_dir.join("build").read_dir()? {
138+
let output_file = dep_dir?.path().join("output");
139+
if output_file.is_file() {
140+
use std::{
141+
fs::File,
142+
io::{BufRead, BufReader},
143+
};
144+
for line in BufReader::new(File::open(output_file)?).lines() {
145+
let line = line?;
146+
if let Some(link_search) = line.strip_prefix("cargo:rustc-link-search=") {
147+
let (kind, path) =
148+
link_search.split_once('=').unwrap_or(("all", link_search));
149+
match kind {
150+
// FIXME: which kinds of search path we interested in
151+
"dependency" | "native" | "all" => paths.push(path.into()),
152+
_ => (),
153+
};
154+
}
155+
}
156+
}
157+
}
158+
159+
Ok(paths)
160+
}
122161
}
123162

124163
pub struct CargoBuild {
@@ -136,10 +175,10 @@ impl CargoBuild {
136175
target_dir: &Path,
137176
offline: bool,
138177
) -> Result<Self> {
139-
let triple = if target.platform() != Platform::host()? || target.arch() != Arch::host()? {
140-
Some(target.rust_triple()?)
141-
} else {
178+
let triple = if target.is_host()? {
142179
None
180+
} else {
181+
Some(target.rust_triple()?)
143182
};
144183
let mut cmd = Command::new("cargo");
145184
cmd.current_dir(root_dir);

xbuild/src/command/build.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,50 @@ pub fn build(env: &BuildEnv) -> Result<()> {
261261
if has_lib {
262262
for target in env.target().compile_targets() {
263263
let arch_dir = platform_dir.join(target.arch().to_string());
264-
let lib =
265-
env.cargo_artefact(&arch_dir.join("cargo"), target, CrateType::Cdylib)?;
264+
let cargo_dir = arch_dir.join("cargo");
265+
let lib = env.cargo_artefact(&cargo_dir, target, CrateType::Cdylib)?;
266266
apk.add_lib(target.android_abi(), &lib)?;
267+
268+
let ndk = env.android_ndk();
269+
270+
let deps_dir = {
271+
let arch_dir = if target.is_host()? {
272+
cargo_dir.to_path_buf()
273+
} else {
274+
cargo_dir.join(target.rust_triple()?)
275+
};
276+
let opt_dir = arch_dir.join(target.opt().to_string());
277+
opt_dir.join("deps")
278+
};
279+
280+
let mut search_paths = env.cargo().lib_search_paths(&cargo_dir, target)?;
281+
search_paths.push(deps_dir);
282+
let search_paths = search_paths.iter().map(AsRef::as_ref).collect::<Vec<_>>();
283+
284+
let ndk_sysroot_libs = ndk.join("usr/lib").join(target.ndk_triple());
285+
let provided_libs_paths = [
286+
ndk_sysroot_libs.as_path(),
287+
&*ndk_sysroot_libs.join(
288+
// Use libraries (symbols) from the lowest NDK that is supported by the application,
289+
// to prevent inadvertently making newer APIs available:
290+
// https://developer.android.com/ndk/guides/sdk-versions
291+
env.manifest()
292+
.android()
293+
.sdk
294+
.min_sdk_version
295+
.unwrap()
296+
.to_string(),
297+
),
298+
];
299+
300+
let extra_libs = xcommon::llvm::list_needed_libs_recursively(
301+
&lib,
302+
&search_paths,
303+
&provided_libs_paths,
304+
)?;
305+
for lib in &extra_libs {
306+
apk.add_lib(target.android_abi(), lib)?;
307+
}
267308
}
268309
}
269310

xbuild/src/command/doctor.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ impl Default for Doctor {
1818
Check::new("clang++", Some(VersionCheck::new("--version", 0, 2))),
1919
Check::new("llvm-ar", None),
2020
Check::new("llvm-lib", None),
21+
Check::new("llvm-readelf", Some(VersionCheck::new("--version", 1, 4))),
2122
Check::new("lld", Some(VersionCheck::new("-flavor ld --version", 0, 1))),
2223
Check::new("lld-link", Some(VersionCheck::new("--version", 0, 1))),
2324
Check::new("lldb", Some(VersionCheck::new("--version", 0, 2))),

xbuild/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ impl CompileTarget {
300300
(Arch::Arm64, Platform::Ios) => "aarch64-apple-ios",
301301
(Arch::Arm64, Platform::Linux) => "aarch64-unknown-linux-gnu",
302302
(Arch::Arm64, Platform::Macos) => "aarch64-apple-darwin",
303+
(Arch::X64, Platform::Android) => "x86_64-linux-android",
303304
(Arch::X64, Platform::Linux) => "x86_64-unknown-linux-gnu",
304305
(Arch::X64, Platform::Macos) => "x86_64-apple-darwin",
305306
(Arch::X64, Platform::Windows) => "x86_64-pc-windows-msvc",
@@ -310,6 +311,10 @@ impl CompileTarget {
310311
),
311312
})
312313
}
314+
315+
pub fn is_host(self) -> Result<bool> {
316+
Ok(self.platform() == Platform::host()? && self.arch() == Arch::host()?)
317+
}
313318
}
314319

315320
impl std::fmt::Display for CompileTarget {

xcommon/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ license = "Apache-2.0 OR MIT"
99
[dependencies]
1010
anyhow = "1.0.53"
1111
byteorder = "1.4.3"
12+
dunce = "1"
1213
image = { version = "0.24.0", default-features = false, features = ["png"] }
1314
pem = "1.0.2"
1415
rasn = "0.5.0"

xcommon/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
pub mod llvm;
2+
13
use anyhow::Result;
24
use byteorder::{LittleEndian, ReadBytesExt};
35
use image::imageops::FilterType;

xcommon/src/llvm.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//! LLVM utilities
2+
3+
use anyhow::{bail, ensure, Result};
4+
use std::collections::HashSet;
5+
use std::path::{Path, PathBuf};
6+
use std::process::Command;
7+
8+
/// Returns the set of additional libraries that need to be bundled with
9+
/// the given library, scanned recursively.
10+
///
11+
/// Any libraries in `provided_libs_paths` will be treated as available, without
12+
/// being emitted. Any other library not in `search_paths` or `provided_libs_paths`
13+
/// will result in an error.
14+
pub fn list_needed_libs_recursively(
15+
lib: &Path,
16+
search_paths: &[&Path],
17+
provided_libs_paths: &[&Path],
18+
) -> Result<HashSet<PathBuf>> {
19+
// Create a view of all libraries that are available on Android
20+
let mut provided = HashSet::new();
21+
for path in provided_libs_paths {
22+
for lib in find_libs_in_dir(path)? {
23+
// libc++_shared is bundled with the NDK but not available on-device
24+
if lib != "libc++_shared.so" {
25+
provided.insert(lib);
26+
}
27+
}
28+
}
29+
30+
let mut to_copy = HashSet::new();
31+
32+
let mut artifacts = vec![lib.to_path_buf()];
33+
while let Some(artifact) = artifacts.pop() {
34+
for need in list_needed_libs(&artifact)? {
35+
// c++_shared is available in the NDK but not on-device.
36+
// Must be bundled with the apk if used:
37+
// https://developer.android.com/ndk/guides/cpp-support#libc
38+
let search_paths = if need == "libc++_shared.so" {
39+
provided_libs_paths
40+
} else {
41+
search_paths
42+
};
43+
44+
if provided.insert(need.clone()) {
45+
if let Some(path) = find_library_path(search_paths, &need)? {
46+
to_copy.insert(path.clone());
47+
artifacts.push(path);
48+
} else {
49+
bail!("Shared library `{}` not found", need);
50+
}
51+
}
52+
}
53+
}
54+
55+
Ok(to_copy)
56+
}
57+
58+
/// List all required shared libraries as per the dynamic section
59+
fn list_needed_libs(library_path: &Path) -> Result<HashSet<String>> {
60+
let mut readelf = Command::new("llvm-readelf");
61+
let output = readelf.arg("--needed-libs").arg(library_path).output()?;
62+
ensure!(
63+
output.status.success(),
64+
"Failed to run `{:?}`: {}",
65+
readelf,
66+
output.status
67+
);
68+
let output = std::str::from_utf8(&output.stdout).unwrap();
69+
let output = output.strip_prefix("NeededLibraries [\n").unwrap();
70+
let output = output.strip_suffix("]\n").unwrap();
71+
let mut needed = HashSet::new();
72+
73+
for line in output.lines() {
74+
let lib = line.trim_start();
75+
needed.insert(lib.to_string());
76+
}
77+
Ok(needed)
78+
}
79+
80+
/// List names of shared libraries inside directory
81+
fn find_libs_in_dir(path: &Path) -> Result<HashSet<String>> {
82+
let mut libs = HashSet::new();
83+
let entries = std::fs::read_dir(path)?;
84+
for entry in entries {
85+
let entry = entry?;
86+
if !entry.path().is_dir() {
87+
if let Some(file_name) = entry.file_name().to_str() {
88+
if file_name.ends_with(".so") {
89+
libs.insert(file_name.to_string());
90+
}
91+
}
92+
}
93+
}
94+
Ok(libs)
95+
}
96+
97+
/// Resolves native library using search paths
98+
fn find_library_path(paths: &[&Path], library: &str) -> Result<Option<PathBuf>> {
99+
for path in paths {
100+
let lib_path = path.join(library);
101+
if lib_path.exists() {
102+
return Ok(Some(dunce::canonicalize(lib_path)?));
103+
}
104+
}
105+
Ok(None)
106+
}

0 commit comments

Comments
 (0)