cargo_rapk/
apk.rs

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