Skip to content

Commit d7284c6

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 79df3d6 commit d7284c6

File tree

8 files changed

+242
-14
lines changed

8 files changed

+242
-14
lines changed

mvn/src/lib.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::metadata::Metadata;
22
use crate::package::Artifact;
33
use crate::pom::Pom;
4-
use anyhow::Result;
4+
use anyhow::{Context, Result};
55
use pubgrub::error::PubGrubError;
66
use pubgrub::range::Range;
77
use pubgrub::report::{DefaultStringReporter, Reporter};
@@ -126,17 +126,15 @@ impl<D: Download> Maven<D> {
126126
anyhow::ensure!(downloaded, "metadata not found for {}", package);
127127
}
128128
let s = std::fs::read_to_string(path)?;
129-
let metadata =
130-
quick_xml::de::from_str(&s).map_err(|err| anyhow::anyhow!("{}: {}", err, s))?;
129+
let metadata = quick_xml::de::from_str(&s).context(s)?;
131130
Ok(metadata)
132131
}
133132

134133
fn pom(&self, artifact: Artifact) -> Result<Pom> {
135134
match self.artifact(artifact, "pom") {
136135
Ok(path) => {
137136
let s = std::fs::read_to_string(path)?;
138-
let pom =
139-
quick_xml::de::from_str(&s).map_err(|err| anyhow::anyhow!("{}: {}", err, s))?;
137+
let pom = quick_xml::de::from_str(&s).context(s)?;
140138
Ok(pom)
141139
}
142140
Err(err) => {

xbuild/src/cargo/mod.rs

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use crate::{Arch, CompileTarget, Opt, Platform};
2-
use anyhow::Result;
1+
use crate::{CompileTarget, Opt};
2+
use anyhow::{Context, Result};
33
use std::path::{Path, PathBuf};
44
use std::process::Command;
55

@@ -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,51 @@ 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+
let build_deps_dir = opt_dir.join("build");
135+
136+
let mut paths = vec![];
137+
138+
for dep_dir in build_deps_dir.read_dir().with_context(|| {
139+
format!(
140+
"Scanning crate directories in `{}`",
141+
build_deps_dir.display()
142+
)
143+
})? {
144+
let output_file = dep_dir?.path().join("output");
145+
if output_file.is_file() {
146+
use std::{
147+
fs::File,
148+
io::{BufRead, BufReader},
149+
};
150+
for line in BufReader::new(File::open(output_file)?).lines() {
151+
let line = line?;
152+
if let Some(link_search) = line.strip_prefix("cargo:rustc-link-search=") {
153+
let (kind, path) =
154+
link_search.split_once('=').unwrap_or(("all", link_search));
155+
match kind {
156+
// FIXME: which kinds of search path we interested in
157+
"dependency" | "native" | "all" => paths.push(path.into()),
158+
_ => (),
159+
};
160+
}
161+
}
162+
}
163+
}
164+
165+
Ok(paths)
166+
}
122167
}
123168

124169
pub struct CargoBuild {
@@ -136,10 +181,10 @@ impl CargoBuild {
136181
target_dir: &Path,
137182
offline: bool,
138183
) -> Result<Self> {
139-
let triple = if target.platform() != Platform::host()? || target.arch() != Arch::host()? {
140-
Some(target.rust_triple()?)
141-
} else {
184+
let triple = if target.is_host()? {
142185
None
186+
} else {
187+
Some(target.rust_triple()?)
143188
};
144189
let mut cmd = Command::new("cargo");
145190
cmd.current_dir(root_dir);

xbuild/src/command/build.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::download::DownloadManager;
33
use crate::flutter::depfile::depfile_is_dirty;
44
use crate::task::TaskRunner;
55
use crate::{BuildEnv, Format, Opt, Platform};
6-
use anyhow::Result;
6+
use anyhow::{Context, Result};
77
use apk::Apk;
88
use appbundle::AppBundle;
99
use appimage::AppImage;
@@ -261,9 +261,67 @@ 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
281+
.cargo()
282+
.lib_search_paths(&cargo_dir, target)
283+
.with_context(|| {
284+
format!(
285+
"Finding libraries in `{}` for {:?}",
286+
cargo_dir.display(),
287+
target
288+
)
289+
})?;
290+
search_paths.push(deps_dir);
291+
let search_paths = search_paths.iter().map(AsRef::as_ref).collect::<Vec<_>>();
292+
293+
let ndk_sysroot_libs = ndk.join("usr/lib").join(target.ndk_triple());
294+
let provided_libs_paths = [
295+
ndk_sysroot_libs.as_path(),
296+
&*ndk_sysroot_libs.join(
297+
// Use libraries (symbols) from the lowest NDK that is supported by the application,
298+
// to prevent inadvertently making newer APIs available:
299+
// https://developer.android.com/ndk/guides/sdk-versions
300+
env.manifest()
301+
.android()
302+
.sdk
303+
.min_sdk_version
304+
.unwrap()
305+
.to_string(),
306+
),
307+
];
308+
309+
let extra_libs = xcommon::llvm::list_needed_libs_recursively(
310+
&lib,
311+
&search_paths,
312+
&provided_libs_paths,
313+
)
314+
.with_context(|| {
315+
format!(
316+
"Failed to collect all required libraries for `{}` with `{:?}` available libraries and `{:?}` shippable libraries",
317+
lib.display(),
318+
provided_libs_paths,
319+
search_paths
320+
)
321+
})?;
322+
for lib in &extra_libs {
323+
apk.add_lib(target.android_abi(), lib)?;
324+
}
267325
}
268326
}
269327

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: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//! LLVM utilities
2+
3+
use anyhow::{bail, ensure, Context, 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).with_context(|| {
23+
format!("Unable to list available libraries in `{}`", path.display())
24+
})? {
25+
// libc++_shared is bundled with the NDK but not available on-device
26+
if lib != "libc++_shared.so" {
27+
provided.insert(lib);
28+
}
29+
}
30+
}
31+
32+
let mut to_copy = HashSet::new();
33+
34+
let mut artifacts = vec![lib.to_path_buf()];
35+
while let Some(artifact) = artifacts.pop() {
36+
for need in list_needed_libs(&artifact).with_context(|| {
37+
format!(
38+
"Unable to read needed libraries from `{}`",
39+
artifact.display()
40+
)
41+
})? {
42+
// c++_shared is available in the NDK but not on-device.
43+
// Must be bundled with the apk if used:
44+
// https://developer.android.com/ndk/guides/cpp-support#libc
45+
let search_paths = if need == "libc++_shared.so" {
46+
provided_libs_paths
47+
} else {
48+
search_paths
49+
};
50+
51+
if provided.insert(need.clone()) {
52+
if let Some(path) = find_library_path(search_paths, &need).with_context(|| {
53+
format!(
54+
"Could not iterate one or more search directories in `{:?}` while searching for library `{}`",
55+
search_paths, need
56+
)
57+
})? {
58+
to_copy.insert(path.clone());
59+
artifacts.push(path);
60+
} else {
61+
bail!("Shared library `{}` not found", need);
62+
}
63+
}
64+
}
65+
}
66+
67+
Ok(to_copy)
68+
}
69+
70+
/// List all required shared libraries as per the dynamic section
71+
fn list_needed_libs(library_path: &Path) -> Result<HashSet<String>> {
72+
let mut readelf = Command::new("llvm-readelf");
73+
let output = readelf.arg("--needed-libs").arg(library_path).output()?;
74+
ensure!(
75+
output.status.success(),
76+
"Failed to run `{:?}`: {}",
77+
readelf,
78+
output.status
79+
);
80+
let output = std::str::from_utf8(&output.stdout).unwrap();
81+
let output = output.strip_prefix("NeededLibraries [\n").unwrap();
82+
let output = output.strip_suffix("]\n").unwrap();
83+
let mut needed = HashSet::new();
84+
85+
for line in output.lines() {
86+
let lib = line.trim_start();
87+
needed.insert(lib.to_string());
88+
}
89+
Ok(needed)
90+
}
91+
92+
/// List names of shared libraries inside directory
93+
fn find_libs_in_dir(path: &Path) -> Result<HashSet<String>> {
94+
let mut libs = HashSet::new();
95+
let entries = std::fs::read_dir(path)?;
96+
for entry in entries {
97+
let entry = entry?;
98+
if !entry.path().is_dir() {
99+
if let Some(file_name) = entry.file_name().to_str() {
100+
if file_name.ends_with(".so") {
101+
libs.insert(file_name.to_string());
102+
}
103+
}
104+
}
105+
}
106+
Ok(libs)
107+
}
108+
109+
/// Resolves native library using search paths
110+
fn find_library_path(paths: &[&Path], library: &str) -> Result<Option<PathBuf>> {
111+
for path in paths {
112+
let lib_path = path.join(library);
113+
if lib_path.exists() {
114+
return Ok(Some(dunce::canonicalize(lib_path)?));
115+
}
116+
}
117+
Ok(None)
118+
}

0 commit comments

Comments
 (0)