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
21impl 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 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 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 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 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 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}