cargo_apk/
apk.rs

1use crate::error::Error;
2use crate::manifest::{Inheritable, Manifest, Root};
3use cargo_subcommand::{Artifact, ArtifactType, CrateType, Profile, Subcommand};
4use ndk_build::apk::{Apk, ApkConfig};
5use ndk_build::cargo::{cargo_ndk, VersionCode};
6use ndk_build::dylibs::get_libs_search_paths;
7use ndk_build::error::NdkError;
8use ndk_build::manifest::{IntentFilter, MetaData};
9use ndk_build::ndk::{Key, Ndk};
10use ndk_build::target::Target;
11use std::path::PathBuf;
12
13pub struct ApkBuilder<'a> {
14    cmd: &'a Subcommand,
15    ndk: Ndk,
16    manifest: Manifest,
17    build_dir: PathBuf,
18    build_targets: Vec<Target>,
19    device_serial: Option<String>,
20}
21
22impl<'a> ApkBuilder<'a> {
23    pub fn from_subcommand(
24        cmd: &'a Subcommand,
25        device_serial: Option<String>,
26    ) -> Result<Self, Error> {
27        println!(
28            "Using package `{}` in `{}`",
29            cmd.package(),
30            cmd.manifest().display()
31        );
32        let ndk = Ndk::from_env()?;
33        let mut manifest = Manifest::parse_from_toml(cmd.manifest())?;
34        let workspace_manifest: Option<Root> = cmd
35            .workspace_manifest()
36            .map(Root::parse_from_toml)
37            .transpose()?;
38        let build_targets = if let Some(target) = cmd.target() {
39            vec![Target::from_rust_triple(target)?]
40        } else if !manifest.build_targets.is_empty() {
41            manifest.build_targets.clone()
42        } else {
43            vec![ndk
44                .detect_abi(device_serial.as_deref())
45                .unwrap_or(Target::Arm64V8a)]
46        };
47        let build_dir = dunce::simplified(cmd.target_dir())
48            .join(cmd.profile())
49            .join("apk");
50
51        let package_version = match &manifest.version {
52            Inheritable::Value(v) => v.clone(),
53            Inheritable::Inherited { workspace: true } => {
54                let workspace = workspace_manifest
55                    .ok_or(Error::InheritanceMissingWorkspace)?
56                    .workspace
57                    .unwrap_or_else(|| {
58                        // Unlikely to fail as cargo-subcommand should give us
59                        // a `Cargo.toml` containing a `[workspace]` table
60                        panic!(
61                            "Manifest `{:?}` must contain a `[workspace]` table",
62                            cmd.workspace_manifest().unwrap()
63                        )
64                    });
65
66                workspace
67                    .package
68                    .ok_or(Error::WorkspaceMissingInheritedField("package"))?
69                    .version
70                    .ok_or(Error::WorkspaceMissingInheritedField("package.version"))?
71            }
72            Inheritable::Inherited { workspace: false } => return Err(Error::InheritedFalse),
73        };
74        let version_code = VersionCode::from_semver(&package_version)?.to_code(1);
75
76        // Set default Android manifest values
77        if manifest
78            .android_manifest
79            .version_name
80            .replace(package_version)
81            .is_some()
82        {
83            panic!("version_name should not be set in TOML");
84        }
85
86        if manifest
87            .android_manifest
88            .version_code
89            .replace(version_code)
90            .is_some()
91        {
92            panic!("version_code should not be set in TOML");
93        }
94
95        let target_sdk_version = *manifest
96            .android_manifest
97            .sdk
98            .target_sdk_version
99            .get_or_insert_with(|| ndk.default_target_platform());
100
101        manifest
102            .android_manifest
103            .application
104            .debuggable
105            .get_or_insert_with(|| *cmd.profile() == Profile::Dev);
106
107        let activity = &mut manifest.android_manifest.application.activity;
108
109        // Add a default `MAIN` action to launch the activity, if the user didn't supply it by hand.
110        if activity
111            .intent_filter
112            .iter()
113            .all(|i| i.actions.iter().all(|f| f != "android.intent.action.MAIN"))
114        {
115            activity.intent_filter.push(IntentFilter {
116                actions: vec!["android.intent.action.MAIN".to_string()],
117                categories: vec!["android.intent.category.LAUNCHER".to_string()],
118                data: vec![],
119            });
120        }
121
122        // Export the sole Rust activity on Android S and up, if the user didn't explicitly do so.
123        // Without this, apps won't start on S+.
124        // https://developer.android.com/about/versions/12/behavior-changes-12#exported
125        if target_sdk_version >= 31 {
126            activity.exported.get_or_insert(true);
127        }
128
129        Ok(Self {
130            cmd,
131            ndk,
132            manifest,
133            build_dir,
134            build_targets,
135            device_serial,
136        })
137    }
138
139    pub fn check(&self) -> Result<(), Error> {
140        for target in &self.build_targets {
141            let mut cargo = cargo_ndk(
142                &self.ndk,
143                *target,
144                self.min_sdk_version(),
145                self.cmd.target_dir(),
146            )?;
147            cargo.arg("check");
148            if self.cmd.target().is_none() {
149                let triple = target.rust_triple();
150                cargo.arg("--target").arg(triple);
151            }
152            self.cmd.args().apply(&mut cargo);
153            if !cargo.status()?.success() {
154                return Err(NdkError::CmdFailed(cargo).into());
155            }
156        }
157        Ok(())
158    }
159
160    pub fn build(&self, artifact: &Artifact) -> Result<Apk, Error> {
161        // Set artifact specific manifest default values.
162        let mut manifest = self.manifest.android_manifest.clone();
163
164        if manifest.package.is_empty() {
165            let name = artifact.name.replace('-', "_");
166            manifest.package = match artifact.r#type {
167                ArtifactType::Lib => format!("rust.{}", name),
168                ArtifactType::Bin => format!("rust.{}", name),
169                ArtifactType::Example => format!("rust.example.{}", name),
170            };
171        }
172
173        if manifest.application.label.is_empty() {
174            manifest.application.label = artifact.name.to_string();
175        }
176
177        manifest.application.activity.meta_data.push(MetaData {
178            name: "android.app.lib_name".to_string(),
179            value: artifact.name.replace('-', "_"),
180        });
181
182        let crate_path = self.cmd.manifest().parent().expect("invalid manifest path");
183
184        let is_debug_profile = *self.cmd.profile() == Profile::Dev;
185
186        let assets = self
187            .manifest
188            .assets
189            .as_ref()
190            .map(|assets| dunce::simplified(&crate_path.join(assets)).to_owned());
191        let resources = self
192            .manifest
193            .resources
194            .as_ref()
195            .map(|res| dunce::simplified(&crate_path.join(res)).to_owned());
196        let runtime_libs = self
197            .manifest
198            .runtime_libs
199            .as_ref()
200            .map(|libs| dunce::simplified(&crate_path.join(libs)).to_owned());
201        let apk_name = self
202            .manifest
203            .apk_name
204            .clone()
205            .unwrap_or_else(|| artifact.name.to_string());
206
207        let config = ApkConfig {
208            ndk: self.ndk.clone(),
209            build_dir: self.build_dir.join(artifact.build_dir()),
210            apk_name,
211            assets,
212            resources,
213            manifest,
214            disable_aapt_compression: is_debug_profile,
215            strip: self.manifest.strip,
216            reverse_port_forward: self.manifest.reverse_port_forward.clone(),
217        };
218        let mut apk = config.create_apk()?;
219
220        for target in &self.build_targets {
221            let triple = target.rust_triple();
222            let build_dir = self.cmd.build_dir(Some(triple));
223            let artifact = self.cmd.artifact(artifact, Some(triple), CrateType::Cdylib);
224
225            let mut cargo = cargo_ndk(
226                &self.ndk,
227                *target,
228                self.min_sdk_version(),
229                self.cmd.target_dir(),
230            )?;
231            cargo.arg("build");
232            if self.cmd.target().is_none() {
233                cargo.arg("--target").arg(triple);
234            }
235            self.cmd.args().apply(&mut cargo);
236
237            if !cargo.status()?.success() {
238                return Err(NdkError::CmdFailed(cargo).into());
239            }
240
241            let mut libs_search_paths =
242                get_libs_search_paths(self.cmd.target_dir(), triple, self.cmd.profile().as_ref())?;
243            libs_search_paths.push(build_dir.join("deps"));
244
245            let libs_search_paths = libs_search_paths
246                .iter()
247                .map(|path| path.as_path())
248                .collect::<Vec<_>>();
249
250            apk.add_lib_recursively(&artifact, *target, libs_search_paths.as_slice())?;
251
252            if let Some(runtime_libs) = &runtime_libs {
253                apk.add_runtime_libs(runtime_libs, *target, libs_search_paths.as_slice())?;
254            }
255        }
256
257        let profile_name = match self.cmd.profile() {
258            Profile::Dev => "dev",
259            Profile::Release => "release",
260            Profile::Custom(c) => c.as_str(),
261        };
262
263        let keystore_env = format!(
264            "CARGO_APK_{}_KEYSTORE",
265            profile_name.to_uppercase().replace('-', "_")
266        );
267        let password_env = format!("{}_PASSWORD", keystore_env);
268
269        let path = std::env::var_os(&keystore_env).map(PathBuf::from);
270        let password = std::env::var(&password_env).ok();
271
272        let signing_key = match (path, password) {
273            (Some(path), Some(password)) => Key { path, password },
274            (Some(path), None) if is_debug_profile => {
275                eprintln!(
276                    "{} not specified, falling back to default password",
277                    password_env
278                );
279                Key {
280                    path,
281                    password: ndk_build::ndk::DEFAULT_DEV_KEYSTORE_PASSWORD.to_owned(),
282                }
283            }
284            (Some(path), None) => {
285                eprintln!("`{}` was specified via `{}`, but `{}` was not specified, both or neither must be present for profiles other than `dev`", path.display(), keystore_env, password_env);
286                return Err(Error::MissingReleaseKey(profile_name.to_owned()));
287            }
288            (None, _) => {
289                if let Some(msk) = self.manifest.signing.get(profile_name) {
290                    Key {
291                        path: crate_path.join(&msk.path),
292                        password: msk.keystore_password.clone(),
293                    }
294                } else if is_debug_profile {
295                    self.ndk.debug_key()?
296                } else {
297                    return Err(Error::MissingReleaseKey(profile_name.to_owned()));
298                }
299            }
300        };
301
302        let unsigned = apk.add_pending_libs_and_align()?;
303
304        println!(
305            "Signing `{}` with keystore `{}`",
306            config.apk().display(),
307            signing_key.path.display()
308        );
309        Ok(unsigned.sign(signing_key)?)
310    }
311
312    pub fn run(&self, artifact: &Artifact, no_logcat: bool) -> Result<(), Error> {
313        let apk = self.build(artifact)?;
314        apk.reverse_port_forwarding(self.device_serial.as_deref())?;
315        apk.install(self.device_serial.as_deref())?;
316        apk.start(self.device_serial.as_deref())?;
317        let uid = apk.uidof(self.device_serial.as_deref())?;
318
319        if !no_logcat {
320            self.ndk
321                .adb(self.device_serial.as_deref())?
322                .arg("logcat")
323                .arg("-v")
324                .arg("color")
325                .arg("--uid")
326                .arg(uid.to_string())
327                .status()?;
328        }
329
330        Ok(())
331    }
332
333    pub fn gdb(&self, artifact: &Artifact) -> Result<(), Error> {
334        let apk = self.build(artifact)?;
335        apk.install(self.device_serial.as_deref())?;
336
337        let target_dir = self.build_dir.join(artifact.build_dir());
338        self.ndk.ndk_gdb(
339            target_dir,
340            "android.app.NativeActivity",
341            self.device_serial.as_deref(),
342        )?;
343        Ok(())
344    }
345
346    pub fn default(&self, cargo_cmd: &str, cargo_args: &[String]) -> Result<(), Error> {
347        for target in &self.build_targets {
348            let mut cargo = cargo_ndk(
349                &self.ndk,
350                *target,
351                self.min_sdk_version(),
352                self.cmd.target_dir(),
353            )?;
354            cargo.arg(cargo_cmd);
355            self.cmd.args().apply(&mut cargo);
356
357            if self.cmd.target().is_none() {
358                let triple = target.rust_triple();
359                cargo.arg("--target").arg(triple);
360            }
361
362            for additional_arg in cargo_args {
363                cargo.arg(additional_arg);
364            }
365
366            if !cargo.status()?.success() {
367                return Err(NdkError::CmdFailed(cargo).into());
368            }
369        }
370        Ok(())
371    }
372
373    /// Returns `minSdkVersion` for use in compiler target selection:
374    /// <https://developer.android.com/ndk/guides/sdk-versions#minsdkversion>
375    ///
376    /// Has a lower bound of `23` to retain backwards compatibility with
377    /// the previous default.
378    fn min_sdk_version(&self) -> u32 {
379        self.manifest
380            .android_manifest
381            .sdk
382            .min_sdk_version
383            .unwrap_or(23)
384            .max(23)
385    }
386}