use crate::contrib::collect_android_contributions;
use crate::error::Error;
use crate::java::compile_java_sources;
use crate::manifest::{Inheritable, Manifest, Root};
use cargo_subcommand::{Artifact, ArtifactType, CrateType, Profile, Subcommand};
use rndk::apk::{Apk, ApkConfig};
use rndk::cargo::{VersionCode, cargo_ndk};
use rndk::error::NdkError;
use rndk::manifest::{IntentFilter, MetaData};
use rndk::ndk::{Key, Ndk};
use rndk::target::Target;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
const NATIVE_ACTIVITY_NAME: &str = "android.app.NativeActivity";
#[derive(Clone, Copy)]
struct ReproCfg {
deterministic: bool,
unsigned: bool,
align: u32,
ts_unix: Option<u64>,
no_normalize_zip: bool,
}
impl Default for ReproCfg {
fn default() -> Self {
Self {
deterministic: false,
unsigned: false,
align: 16,
ts_unix: None,
no_normalize_zip: false,
}
}
}
pub struct ApkBuilder<'a> {
cmd: &'a Subcommand,
ndk: Ndk,
manifest: Manifest,
build_dir: PathBuf,
build_targets: Vec<Target>,
device_serial: Option<String>,
repro: ReproCfg,
}
impl<'a> ApkBuilder<'a> {
fn apply_deterministic_env(&self) {
if self.repro.deterministic {
unsafe { std::env::set_var("CARGO_RAPK_DETERMINISTIC", "1") };
if let Some(ts) = self.repro.ts_unix {
unsafe { std::env::set_var("SOURCE_DATE_EPOCH", ts.to_string()) };
}
} else {
unsafe { std::env::remove_var("CARGO_RAPK_DETERMINISTIC") };
}
}
pub fn from_subcommand(
cmd: &'a Subcommand,
device_serial: Option<String>,
) -> Result<Self, Error> {
println!(
"Using package `{}` in `{}`",
cmd.package(),
cmd.manifest().display()
);
let ndk = Ndk::from_env()?;
let mut manifest = Manifest::parse_from_toml(cmd.manifest())?;
let workspace_manifest: Option<Root> = cmd
.workspace_manifest()
.map(Root::parse_from_toml)
.transpose()?;
let build_targets = if let Some(target) = cmd.target() {
vec![Target::from_rust_triple(target)?]
} else if !manifest.build_targets.is_empty() {
manifest.build_targets.clone()
} else {
vec![
ndk.detect_abi(device_serial.as_deref())
.unwrap_or(Target::Arm64V8a),
]
};
let build_dir = dunce::simplified(cmd.target_dir())
.join(cmd.profile())
.join("apk");
let package_version = match &manifest.version {
Inheritable::Value(v) => v.clone(),
Inheritable::Inherited { workspace: true } => {
let workspace = workspace_manifest
.ok_or(Error::InheritanceMissingWorkspace)?
.workspace
.ok_or(Error::MissingWorkspaceTable)?;
workspace
.package
.ok_or(Error::WorkspaceMissingInheritedField("package"))?
.version
.ok_or(Error::WorkspaceMissingInheritedField("package.version"))?
}
Inheritable::Inherited { workspace: false } => return Err(Error::InheritedFalse),
};
let version_code = VersionCode::from_semver(&package_version)?.to_code(1);
if manifest
.android_manifest
.version_name
.replace(package_version)
.is_some()
{
return Err(Error::VersionNameSet);
}
if manifest
.android_manifest
.version_code
.replace(version_code)
.is_some()
{
return Err(Error::VersionCodeSet);
}
manifest
.android_manifest
.sdk
.target_sdk_version
.get_or_insert_with(|| ndk.default_target_platform());
manifest
.android_manifest
.application
.debuggable
.get_or_insert_with(|| *cmd.profile() == Profile::Dev);
Ok(Self {
cmd,
ndk,
manifest,
build_dir,
build_targets,
device_serial,
repro: ReproCfg::default(),
})
}
pub fn set_repro_flags(
&mut self,
deterministic: bool,
unsigned: bool,
align: u32,
ts: Option<u64>,
no_norm: bool,
) {
let env_ts = std::env::var("SOURCE_DATE_EPOCH")
.ok()
.and_then(|s| s.parse::<u64>().ok());
self.repro = ReproCfg {
deterministic,
unsigned,
align: if align == 0 { 4 } else { align },
ts_unix: ts.or(env_ts),
no_normalize_zip: no_norm,
};
if deterministic {
unsafe { std::env::set_var("CARGO_RAPK_DETERMINISTIC", "1") };
} else {
unsafe { std::env::remove_var("CARGO_RAPK_DETERMINISTIC") };
}
}
pub fn check(&self) -> Result<(), Error> {
self.apply_deterministic_env();
for target in &self.build_targets {
let mut cargo = cargo_ndk(
&self.ndk,
*target,
self.min_sdk_version(),
self.cmd.target_dir(),
)?;
cargo.arg("check");
if self.cmd.target().is_none() {
cargo.arg("--target").arg(target.rust_triple());
}
self.cmd.args().apply(&mut cargo);
if !cargo.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(cargo)).into());
}
}
Ok(())
}
pub fn build(&self, artifact: &Artifact) -> Result<Apk, Error> {
self.apply_deterministic_env();
let mut manifest = self.manifest.android_manifest.clone();
if manifest.package.is_empty() {
let name = artifact.name.replace('-', "_");
manifest.package = match artifact.r#type {
ArtifactType::Lib | ArtifactType::Bin => format!("rust.{name}"),
ArtifactType::Example => format!("rust.example.{name}"),
};
}
if manifest.application.label.is_empty() {
manifest.application.label = artifact.name.to_string();
}
let crate_path = self.cmd.manifest().parent().expect("invalid manifest path");
let mut java_sources = self
.manifest
.java_sources
.iter()
.map(|p| dunce::simplified(&crate_path.join(p)).to_owned())
.collect::<Vec<_>>();
let contrib = collect_android_contributions(self.cmd.manifest())?;
java_sources.extend(contrib.java_sources);
let mut existing_activity_names = manifest
.application
.activity
.iter()
.map(|activity| activity.name.clone())
.collect::<HashSet<_>>();
for activity in contrib.activities {
if existing_activity_names.insert(activity.name.clone()) {
manifest.application.activity.push(activity);
}
}
let mut existing_service_names = manifest
.application
.service
.iter()
.map(|service| service.name.clone())
.collect::<HashSet<_>>();
for service in contrib.services {
if existing_service_names.insert(service.name.clone()) {
manifest.application.service.push(service);
}
}
if manifest.application.activity.is_empty() {
manifest
.application
.activity
.push(rndk::manifest::Activity::default());
}
let has_main_action = manifest.application.activity.iter().any(|activity| {
activity
.intent_filter
.iter()
.any(|i| i.actions.iter().any(|f| f == "android.intent.action.MAIN"))
});
if !has_main_action {
let activity_index = manifest
.application
.activity
.iter()
.position(|activity| activity.name == NATIVE_ACTIVITY_NAME)
.unwrap_or(0);
let activity = &mut manifest.application.activity[activity_index];
activity.intent_filter.push(IntentFilter {
actions: vec!["android.intent.action.MAIN".to_string()],
categories: vec!["android.intent.category.LAUNCHER".to_string()],
data: vec![],
});
}
let target_sdk_version = manifest
.sdk
.target_sdk_version
.unwrap_or_else(|| self.ndk.default_target_platform());
if target_sdk_version >= 31 {
for activity in &mut manifest.application.activity {
if !activity.intent_filter.is_empty() {
activity.exported.get_or_insert(true);
}
}
}
let lib_name = artifact.name.replace('-', "_");
let lib_name_meta = MetaData {
name: "android.app.lib_name".to_string(),
value: lib_name,
};
let mut attached_to_native_activity = false;
for activity in &mut manifest.application.activity {
if activity.name == NATIVE_ACTIVITY_NAME {
activity.meta_data.push(lib_name_meta.clone());
attached_to_native_activity = true;
}
}
if !attached_to_native_activity {
manifest.application.activity[0]
.meta_data
.push(lib_name_meta);
}
let java_sources = dedup_paths(java_sources);
let is_debug_profile = *self.cmd.profile() == Profile::Dev;
let assets = self
.manifest
.assets
.as_ref()
.map(|p| dunce::simplified(&crate_path.join(p)).to_owned());
let resources = self
.manifest
.resources
.as_ref()
.map(|p| dunce::simplified(&crate_path.join(p)).to_owned());
let runtime_libs = self
.manifest
.runtime_libs
.as_ref()
.map(|p| dunce::simplified(&crate_path.join(p)).to_owned());
if !java_sources.is_empty() {
manifest.application.has_code = true;
}
let apk_name = self
.manifest
.apk_name
.clone()
.unwrap_or_else(|| artifact.name.to_string());
let config = ApkConfig {
ndk: self.ndk.clone(),
build_dir: self.build_dir.join(artifact.build_dir()),
apk_name,
assets,
resources,
manifest,
disable_aapt_compression: is_debug_profile,
strip: self.manifest.strip,
reverse_port_forward: self.manifest.reverse_port_forward.clone(),
align: self.repro.align,
normalize_zip: self.repro.deterministic && !self.repro.no_normalize_zip,
zip_timestamp: self.repro.ts_unix,
};
let mut apk = config.create_apk()?;
if !java_sources.is_empty() {
let dex_files = compile_java_sources(
&self.ndk,
java_sources.as_slice(),
&config.build_dir,
self.min_sdk_version(),
target_sdk_version,
)?;
for dex_file in dex_files {
let file_name = dex_file
.file_name()
.ok_or_else(|| NdkError::PathNotFound(dex_file.clone()))?;
apk.add_file(&dex_file, Path::new(file_name))?;
}
}
for target in &self.build_targets {
let triple = target.rust_triple();
let build_dir = self.cmd.build_dir(Some(triple));
let artifact = self.cmd.artifact(artifact, Some(triple), CrateType::Cdylib);
let mut cargo = cargo_ndk(
&self.ndk,
*target,
self.min_sdk_version(),
self.cmd.target_dir(),
)?;
cargo.arg("build");
if self.cmd.target().is_none() {
cargo.arg("--target").arg(triple);
}
self.cmd.args().apply(&mut cargo);
if !cargo.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(cargo)).into());
}
let mut libs_search_paths = rndk::dylibs::get_libs_search_paths(
self.cmd.target_dir(),
triple,
self.cmd.profile().as_ref(),
)?;
libs_search_paths.push(build_dir.join("deps"));
let libs_search_paths = libs_search_paths
.iter()
.map(|p| p.as_path())
.collect::<Vec<_>>();
apk.add_lib_recursively(&artifact, *target, libs_search_paths.as_slice())?;
if let Some(runtime_libs) = &runtime_libs {
apk.add_runtime_libs(runtime_libs, *target, libs_search_paths.as_slice())?;
}
}
let unsigned = apk.add_pending_libs_and_align()?;
if self.repro.unsigned {
eprintln!(
"--unsigned set; producing unsigned APK at {}",
config.apk().display()
);
return Ok(rndk::apk::Apk::from_config(unsigned.config()));
}
let profile_name = match self.cmd.profile() {
Profile::Dev => "dev",
Profile::Release => "release",
Profile::Custom(c) => c.as_str(),
};
let keystore_env = format!(
"CARGO_RAPK_{}_KEYSTORE",
profile_name.to_uppercase().replace('-', "_")
);
let password_env = format!("{keystore_env}_PASSWORD");
let path = std::env::var_os(&keystore_env).map(PathBuf::from);
let password = std::env::var(&password_env).ok();
let signing_key = match (path, password) {
(Some(path), Some(password)) => Key { path, password },
(Some(path), None) if *self.cmd.profile() == Profile::Dev => Key {
path,
password: rndk::ndk::DEFAULT_DEV_KEYSTORE_PASSWORD.to_owned(),
},
(Some(_path), None) => {
return Err(Error::MissingKeystorePassword(profile_name.to_owned()));
}
(None, _) => {
if let Some(msk) = self.manifest.signing.get(profile_name) {
Key {
path: crate_path.join(&msk.path),
password: msk.keystore_password.clone(),
}
} else if *self.cmd.profile() == Profile::Dev {
self.ndk.debug_key()?
} else {
return Err(Error::MissingReleaseKey(profile_name.to_owned()));
}
}
};
println!(
"Signing `{}` with keystore `{}`",
config.apk().display(),
signing_key.path.display()
);
Ok(unsigned.sign(signing_key)?)
}
pub fn run(&self, artifact: &Artifact, no_logcat: bool) -> Result<(), Error> {
self.apply_deterministic_env();
let apk = self.build(artifact)?;
apk.reverse_port_forwarding(self.device_serial.as_deref())?;
apk.install(self.device_serial.as_deref())?;
apk.start(self.device_serial.as_deref())?;
let uid = apk.uidof(self.device_serial.as_deref())?;
if !no_logcat {
self.ndk
.adb(self.device_serial.as_deref())?
.arg("logcat")
.arg("-v")
.arg("color")
.arg("--uid")
.arg(uid.to_string())
.status()?;
}
Ok(())
}
pub fn gdb(&self, artifact: &Artifact) -> Result<(), Error> {
self.apply_deterministic_env();
let apk = self.build(artifact)?;
apk.install(self.device_serial.as_deref())?;
let target_dir = self.build_dir.join(artifact.build_dir());
self.ndk.ndk_gdb(
target_dir,
NATIVE_ACTIVITY_NAME,
self.device_serial.as_deref(),
)?;
Ok(())
}
pub fn default(&self, cargo_cmd: &str, cargo_args: &[String]) -> Result<(), Error> {
self.apply_deterministic_env();
for target in &self.build_targets {
let mut cargo = cargo_ndk(
&self.ndk,
*target,
self.min_sdk_version(),
self.cmd.target_dir(),
)?;
cargo.arg(cargo_cmd);
self.cmd.args().apply(&mut cargo);
if self.cmd.target().is_none() {
let triple = target.rust_triple();
cargo.arg("--target").arg(triple);
}
for additional_arg in cargo_args {
cargo.arg(additional_arg);
}
if !cargo.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(cargo)).into());
}
}
Ok(())
}
fn min_sdk_version(&self) -> u32 {
self.manifest
.android_manifest
.sdk
.min_sdk_version
.unwrap_or(23)
.max(23)
}
}
fn dedup_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
let mut seen = HashSet::new();
paths
.into_iter()
.filter(|path| seen.insert(path.clone()))
.collect()
}
#[cfg(test)]
mod tests {
use super::dedup_paths;
use std::path::PathBuf;
#[test]
fn dedup_paths_preserves_first_occurrence_order() {
let input = vec![
PathBuf::from("a"),
PathBuf::from("b"),
PathBuf::from("a"),
PathBuf::from("c"),
PathBuf::from("b"),
];
let output = dedup_paths(input);
assert_eq!(
output,
vec![PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")]
);
}
}