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