ndk_build/
apk.rs

1use crate::error::NdkError;
2use crate::manifest::AndroidManifest;
3use crate::ndk::{Key, Ndk};
4use crate::target::Target;
5use std::collections::HashMap;
6use std::collections::HashSet;
7use std::ffi::OsStr;
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::process::Command;
11
12/// The options for how to treat debug symbols that are present in any `.so`
13/// files that are added to the APK.
14///
15/// Using [`strip`](https://doc.rust-lang.org/cargo/reference/profiles.html#strip)
16/// or [`split-debuginfo`](https://doc.rust-lang.org/cargo/reference/profiles.html#split-debuginfo)
17/// in your cargo manifest(s) may cause debug symbols to not be present in a
18/// `.so`, which would cause these options to do nothing.
19#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum StripConfig {
22    /// Does not treat debug symbols specially
23    Default,
24    /// Removes debug symbols from the library before copying it into the APK
25    Strip,
26    /// Splits the library into into an ELF (`.so`) and DWARF (`.dwarf`). Only the
27    /// `.so` is copied into the APK
28    Split,
29}
30
31impl Default for StripConfig {
32    fn default() -> Self {
33        Self::Default
34    }
35}
36
37pub struct ApkConfig {
38    pub ndk: Ndk,
39    pub build_dir: PathBuf,
40    pub apk_name: String,
41    pub assets: Option<PathBuf>,
42    pub resources: Option<PathBuf>,
43    pub manifest: AndroidManifest,
44    pub disable_aapt_compression: bool,
45    pub strip: StripConfig,
46    pub reverse_port_forward: HashMap<String, String>,
47}
48
49impl ApkConfig {
50    fn build_tool(&self, tool: &'static str) -> Result<Command, NdkError> {
51        let mut cmd = self.ndk.build_tool(tool)?;
52        cmd.current_dir(&self.build_dir);
53        Ok(cmd)
54    }
55
56    fn unaligned_apk(&self) -> PathBuf {
57        self.build_dir
58            .join(format!("{}-unaligned.apk", self.apk_name))
59    }
60
61    /// Retrieves the path of the APK that will be written when [`UnsignedApk::sign`]
62    /// is invoked
63    #[inline]
64    pub fn apk(&self) -> PathBuf {
65        self.build_dir.join(format!("{}.apk", self.apk_name))
66    }
67
68    pub fn create_apk(&self) -> Result<UnalignedApk, NdkError> {
69        std::fs::create_dir_all(&self.build_dir)?;
70        self.manifest.write_to(&self.build_dir)?;
71
72        let target_sdk_version = self
73            .manifest
74            .sdk
75            .target_sdk_version
76            .unwrap_or_else(|| self.ndk.default_target_platform());
77        let mut aapt = self.build_tool(bin!("aapt"))?;
78        aapt.arg("package")
79            .arg("-f")
80            .arg("-F")
81            .arg(self.unaligned_apk())
82            .arg("-M")
83            .arg("AndroidManifest.xml")
84            .arg("-I")
85            .arg(self.ndk.android_jar(target_sdk_version)?);
86
87        if self.disable_aapt_compression {
88            aapt.arg("-0").arg("");
89        }
90
91        if let Some(res) = &self.resources {
92            aapt.arg("-S").arg(res);
93        }
94
95        if let Some(assets) = &self.assets {
96            aapt.arg("-A").arg(assets);
97        }
98
99        if !aapt.status()?.success() {
100            return Err(NdkError::CmdFailed(aapt));
101        }
102
103        Ok(UnalignedApk {
104            config: self,
105            pending_libs: HashSet::default(),
106        })
107    }
108}
109
110pub struct UnalignedApk<'a> {
111    config: &'a ApkConfig,
112    pending_libs: HashSet<String>,
113}
114
115impl<'a> UnalignedApk<'a> {
116    pub fn config(&self) -> &ApkConfig {
117        self.config
118    }
119
120    pub fn add_lib(&mut self, path: &Path, target: Target) -> Result<(), NdkError> {
121        if !path.exists() {
122            return Err(NdkError::PathNotFound(path.into()));
123        }
124        let abi = target.android_abi();
125        let lib_path = Path::new("lib").join(abi).join(path.file_name().unwrap());
126        let out = self.config.build_dir.join(&lib_path);
127        std::fs::create_dir_all(out.parent().unwrap())?;
128
129        match self.config.strip {
130            StripConfig::Default => {
131                std::fs::copy(path, out)?;
132            }
133            StripConfig::Strip | StripConfig::Split => {
134                let obj_copy = self.config.ndk.toolchain_bin("objcopy", target)?;
135
136                {
137                    let mut cmd = Command::new(&obj_copy);
138                    cmd.arg("--strip-debug");
139                    cmd.arg(path);
140                    cmd.arg(&out);
141
142                    if !cmd.status()?.success() {
143                        return Err(NdkError::CmdFailed(cmd));
144                    }
145                }
146
147                if self.config.strip == StripConfig::Split {
148                    let dwarf_path = out.with_extension("dwarf");
149
150                    {
151                        let mut cmd = Command::new(&obj_copy);
152                        cmd.arg("--only-keep-debug");
153                        cmd.arg(path);
154                        cmd.arg(&dwarf_path);
155
156                        if !cmd.status()?.success() {
157                            return Err(NdkError::CmdFailed(cmd));
158                        }
159                    }
160
161                    let mut cmd = Command::new(obj_copy);
162                    cmd.arg(format!("--add-gnu-debuglink={}", dwarf_path.display()));
163                    cmd.arg(out);
164
165                    if !cmd.status()?.success() {
166                        return Err(NdkError::CmdFailed(cmd));
167                    }
168                }
169            }
170        }
171
172        // Pass UNIX path separators to `aapt` on non-UNIX systems, ensuring the resulting separator
173        // is compatible with the target device instead of the host platform.
174        // Otherwise, it results in a runtime error when loading the NativeActivity `.so` library.
175        let lib_path_unix = lib_path.to_str().unwrap().replace('\\', "/");
176
177        self.pending_libs.insert(lib_path_unix);
178
179        Ok(())
180    }
181
182    pub fn add_runtime_libs(
183        &mut self,
184        path: &Path,
185        target: Target,
186        search_paths: &[&Path],
187    ) -> Result<(), NdkError> {
188        let abi_dir = path.join(target.android_abi());
189        for entry in fs::read_dir(&abi_dir).map_err(|e| NdkError::IoPathError(abi_dir, e))? {
190            let entry = entry?;
191            let path = entry.path();
192            if path.extension() == Some(OsStr::new("so")) {
193                self.add_lib_recursively(&path, target, search_paths)?;
194            }
195        }
196        Ok(())
197    }
198
199    pub fn add_pending_libs_and_align(self) -> Result<UnsignedApk<'a>, NdkError> {
200        let mut aapt = self.config.build_tool(bin!("aapt"))?;
201        aapt.arg("add");
202
203        if self.config.disable_aapt_compression {
204            aapt.arg("-0").arg("");
205        }
206
207        aapt.arg(self.config.unaligned_apk());
208
209        for lib_path_unix in self.pending_libs {
210            aapt.arg(lib_path_unix);
211        }
212
213        if !aapt.status()?.success() {
214            return Err(NdkError::CmdFailed(aapt));
215        }
216
217        let mut zipalign = self.config.build_tool(bin!("zipalign"))?;
218        zipalign
219            .arg("-f")
220            .arg("-v")
221            .arg("4")
222            .arg(self.config.unaligned_apk())
223            .arg(self.config.apk());
224
225        if !zipalign.status()?.success() {
226            return Err(NdkError::CmdFailed(zipalign));
227        }
228
229        Ok(UnsignedApk(self.config))
230    }
231}
232
233pub struct UnsignedApk<'a>(&'a ApkConfig);
234
235impl<'a> UnsignedApk<'a> {
236    pub fn sign(self, key: Key) -> Result<Apk, NdkError> {
237        let mut apksigner = self.0.build_tool(bat!("apksigner"))?;
238        apksigner
239            .arg("sign")
240            .arg("--ks")
241            .arg(&key.path)
242            .arg("--ks-pass")
243            .arg(format!("pass:{}", &key.password))
244            .arg(self.0.apk());
245        if !apksigner.status()?.success() {
246            return Err(NdkError::CmdFailed(apksigner));
247        }
248        Ok(Apk::from_config(self.0))
249    }
250}
251
252pub struct Apk {
253    path: PathBuf,
254    package_name: String,
255    ndk: Ndk,
256    reverse_port_forward: HashMap<String, String>,
257}
258
259impl Apk {
260    pub fn from_config(config: &ApkConfig) -> Self {
261        let ndk = config.ndk.clone();
262        Self {
263            path: config.apk(),
264            package_name: config.manifest.package.clone(),
265            ndk,
266            reverse_port_forward: config.reverse_port_forward.clone(),
267        }
268    }
269
270    pub fn reverse_port_forwarding(&self, device_serial: Option<&str>) -> Result<(), NdkError> {
271        for (from, to) in &self.reverse_port_forward {
272            println!("Reverse port forwarding from {} to {}", from, to);
273            let mut adb = self.ndk.adb(device_serial)?;
274
275            adb.arg("reverse").arg(from).arg(to);
276
277            if !adb.status()?.success() {
278                return Err(NdkError::CmdFailed(adb));
279            }
280        }
281
282        Ok(())
283    }
284
285    pub fn install(&self, device_serial: Option<&str>) -> Result<(), NdkError> {
286        let mut adb = self.ndk.adb(device_serial)?;
287
288        adb.arg("install").arg("-r").arg(&self.path);
289        if !adb.status()?.success() {
290            return Err(NdkError::CmdFailed(adb));
291        }
292        Ok(())
293    }
294
295    pub fn start(&self, device_serial: Option<&str>) -> Result<(), NdkError> {
296        let mut adb = self.ndk.adb(device_serial)?;
297        adb.arg("shell")
298            .arg("am")
299            .arg("start")
300            .arg("-a")
301            .arg("android.intent.action.MAIN")
302            .arg("-n")
303            .arg(format!("{}/android.app.NativeActivity", self.package_name));
304
305        if !adb.status()?.success() {
306            return Err(NdkError::CmdFailed(adb));
307        }
308
309        Ok(())
310    }
311
312    pub fn uidof(&self, device_serial: Option<&str>) -> Result<u32, NdkError> {
313        let mut adb = self.ndk.adb(device_serial)?;
314        adb.arg("shell")
315            .arg("pm")
316            .arg("list")
317            .arg("package")
318            .arg("-U")
319            .arg(&self.package_name);
320        let output = adb.output()?;
321
322        if !output.status.success() {
323            return Err(NdkError::CmdFailed(adb));
324        }
325
326        let output = std::str::from_utf8(&output.stdout).unwrap();
327        let (_package, uid) = output
328            .lines()
329            .filter_map(|line| line.split_once(' '))
330            // `pm list package` uses the id as a substring filter; make sure
331            // we select the right package in case it returns multiple matches:
332            .find(|(package, _uid)| package.strip_prefix("package:") == Some(&self.package_name))
333            .ok_or(NdkError::PackageNotInOutput {
334                package: self.package_name.clone(),
335                output: output.to_owned(),
336            })?;
337        let uid = uid
338            .strip_prefix("uid:")
339            .ok_or(NdkError::UidNotInOutput(output.to_owned()))?;
340        uid.parse()
341            .map_err(|e| NdkError::NotAUid(e, uid.to_owned()))
342    }
343}