use serde::Deserialize;
use crate::error::NdkError;
use crate::manifest::AndroidManifest;
use crate::ndk::{Key, Ndk};
use crate::target::Target;
use std::collections::{HashMap, HashSet};
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum StripConfig {
#[default]
Default,
Strip,
Split,
}
pub struct ApkConfig {
pub ndk: Ndk,
pub build_dir: PathBuf,
pub apk_name: String,
pub assets: Option<PathBuf>,
pub resources: Option<PathBuf>,
pub manifest: AndroidManifest,
pub disable_aapt_compression: bool,
pub strip: StripConfig,
pub reverse_port_forward: HashMap<String, String>,
pub align: u32,
pub normalize_zip: bool,
pub zip_timestamp: Option<u64>,
}
impl ApkConfig {
fn build_tool(&self, tool: &'static str) -> Result<Command, NdkError> {
let mut cmd = self.ndk.build_tool(tool)?;
cmd.current_dir(&self.build_dir);
Ok(cmd)
}
fn unaligned_apk(&self) -> PathBuf {
self.build_dir
.join(format!("{}-unaligned.apk", self.apk_name))
}
#[inline]
pub fn apk(&self) -> PathBuf {
self.build_dir.join(format!("{}.apk", self.apk_name))
}
pub fn create_apk(&self) -> Result<UnalignedApk<'_>, NdkError> {
std::fs::create_dir_all(&self.build_dir)?;
self.manifest.write_to(&self.build_dir)?;
let target_sdk_version = self
.manifest
.sdk
.target_sdk_version
.unwrap_or_else(|| self.ndk.default_target_platform());
let mut aapt = self.build_tool(bin!("aapt"))?;
aapt.arg("package")
.arg("-f")
.arg("-F")
.arg(self.unaligned_apk())
.arg("-M")
.arg("AndroidManifest.xml")
.arg("-I")
.arg(self.ndk.android_jar(target_sdk_version)?);
if self.disable_aapt_compression {
aapt.arg("-0").arg("");
}
if let Some(res) = &self.resources {
aapt.arg("-S").arg(res);
}
if let Some(assets) = &self.assets {
aapt.arg("-A").arg(assets);
}
if !aapt.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(aapt)));
}
Ok(UnalignedApk {
config: self,
pending_entries: HashSet::default(),
})
}
}
pub struct UnalignedApk<'a> {
config: &'a ApkConfig,
pending_entries: HashSet<String>,
}
impl<'a> UnalignedApk<'a> {
pub fn config(&self) -> &ApkConfig {
self.config
}
pub fn add_lib(&mut self, path: &Path, target: Target) -> Result<(), NdkError> {
if !path.exists() {
return Err(NdkError::PathNotFound(path.into()));
}
let abi = target.android_abi();
let lib_path = Path::new("lib").join(abi).join(path.file_name().unwrap());
let out = self.config.build_dir.join(&lib_path);
std::fs::create_dir_all(out.parent().unwrap())?;
match self.config.strip {
StripConfig::Default => {
std::fs::copy(path, out)?;
}
StripConfig::Strip | StripConfig::Split => {
let obj_copy = self.config.ndk.toolchain_bin("objcopy", target)?;
{
let mut cmd = Command::new(&obj_copy);
cmd.arg("--strip-debug");
cmd.arg(path);
cmd.arg(&out);
if !cmd.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(cmd)));
}
}
if self.config.strip == StripConfig::Split {
let dwarf_path = out.with_extension("dwarf");
{
let mut cmd = Command::new(&obj_copy);
cmd.arg("--only-keep-debug");
cmd.arg(path);
cmd.arg(&dwarf_path);
if !cmd.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(cmd)));
}
}
let mut cmd = Command::new(obj_copy);
cmd.arg(format!("--add-gnu-debuglink={}", dwarf_path.display()));
cmd.arg(out);
if !cmd.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(cmd)));
}
}
}
}
let lib_path_unix = lib_path.to_str().unwrap().replace('\\', "/");
self.pending_entries.insert(lib_path_unix);
Ok(())
}
pub fn add_runtime_libs(
&mut self,
path: &Path,
target: Target,
search_paths: &[&Path],
) -> Result<(), NdkError> {
let abi_dir = path.join(target.android_abi());
for entry in fs::read_dir(&abi_dir).map_err(|e| NdkError::IoPathError(abi_dir, e))? {
let entry = entry?;
let path = entry.path();
if path.extension() == Some(OsStr::new("so")) {
self.add_lib_recursively(&path, target, search_paths)?;
}
}
Ok(())
}
pub fn add_file(&mut self, src: &Path, dst: &Path) -> Result<(), NdkError> {
if !src.exists() {
return Err(NdkError::PathNotFound(src.into()));
}
let out = self.config.build_dir.join(dst);
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(src, out)?;
let dst_unix = dst.to_string_lossy().replace('\\', "/");
self.pending_entries.insert(dst_unix);
Ok(())
}
pub fn add_pending_libs_and_align(self) -> Result<UnsignedApk<'a>, NdkError> {
let mut aapt = self.config.build_tool(bin!("aapt"))?;
aapt.arg("add");
if self.config.disable_aapt_compression {
aapt.arg("-0").arg("");
}
aapt.arg(self.config.unaligned_apk());
let mut entries: Vec<_> = self.pending_entries.into_iter().collect();
entries.sort();
for path_unix in entries {
aapt.arg(path_unix);
}
if !aapt.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(aapt)));
}
if self.config.normalize_zip {
super::zipnorm::normalize_zip_in_place(
self.config.unaligned_apk(),
self.config.zip_timestamp,
)
.map_err(|e| NdkError::IoPathError(self.config.unaligned_apk(), e))?;
}
let mut zipalign = self.config.build_tool(bin!("zipalign"))?;
zipalign.arg("-f").arg("-v");
let page_size_kb = std::env::var("CARGO_RAPK_PAGE_SIZE_KB")
.ok()
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(16);
let bt_ver = self.config.ndk.build_tools_version();
if bt_ver >= "35.0.0" {
zipalign.arg("-P").arg(page_size_kb.to_string());
} else {
eprintln!(
"zipalign -P requires Build-Tools >= 35.0.0 (found {}); continuing without -P",
bt_ver
);
}
zipalign
.arg(self.config.align.to_string())
.arg(self.config.unaligned_apk())
.arg(self.config.apk());
if !zipalign.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(zipalign)));
}
Ok(UnsignedApk(self.config))
}
}
pub struct UnsignedApk<'a>(&'a ApkConfig);
impl<'a> UnsignedApk<'a> {
pub fn config(&self) -> &'a ApkConfig {
self.0
}
pub fn sign(self, key: Key) -> Result<Apk, NdkError> {
let mut apksigner = self.0.build_tool(bat!("apksigner"))?;
apksigner
.arg("sign")
.arg("--ks")
.arg(&key.path)
.arg("--ks-pass")
.arg(format!("pass:{}", &key.password));
if self.0.normalize_zip {
apksigner
.arg("--v1-signing-enabled")
.arg("false")
.arg("--v2-signing-enabled")
.arg("true")
.arg("--v3-signing-enabled")
.arg("true")
.arg("--v4-signing-enabled")
.arg("false");
}
apksigner.arg(self.0.apk());
if !apksigner.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(apksigner)));
}
Ok(Apk::from_config(self.0))
}
}
pub struct Apk {
path: PathBuf,
package_name: String,
activity_name: String,
ndk: Ndk,
reverse_port_forward: HashMap<String, String>,
}
impl Apk {
pub fn from_config(config: &ApkConfig) -> Self {
let ndk = config.ndk.clone();
let activity_name = config
.manifest
.application
.activity
.first()
.map(|a| a.name.clone())
.unwrap_or_else(|| "android.app.NativeActivity".to_string());
Self {
path: config.apk(),
package_name: config.manifest.package.clone(),
activity_name,
ndk,
reverse_port_forward: config.reverse_port_forward.clone(),
}
}
pub fn reverse_port_forwarding(&self, device_serial: Option<&str>) -> Result<(), NdkError> {
for (from, to) in &self.reverse_port_forward {
println!("Reverse port forwarding from {from} to {to}");
let mut adb = self.ndk.adb(device_serial)?;
adb.arg("reverse").arg(from).arg(to);
if !adb.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(adb)));
}
}
Ok(())
}
pub fn install(&self, device_serial: Option<&str>) -> Result<(), NdkError> {
let mut adb = self.ndk.adb(device_serial)?;
adb.arg("install").arg("-r").arg(&self.path);
if !adb.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(adb)));
}
Ok(())
}
pub fn start(&self, device_serial: Option<&str>) -> Result<(), NdkError> {
let mut adb = self.ndk.adb(device_serial)?;
adb.arg("shell")
.arg("am")
.arg("start")
.arg("-a")
.arg("android.intent.action.MAIN")
.arg("-c")
.arg("android.intent.category.LAUNCHER")
.arg("-n")
.arg(format!("{}/{}", self.package_name, self.activity_name));
if !adb.status()?.success() {
return Err(NdkError::CmdFailed(Box::new(adb)));
}
Ok(())
}
pub fn uidof(&self, device_serial: Option<&str>) -> Result<u32, NdkError> {
let mut adb = self.ndk.adb(device_serial)?;
adb.arg("shell")
.arg("pm")
.arg("list")
.arg("package")
.arg("-U")
.arg(&self.package_name);
let output = adb.output()?;
if !output.status.success() {
return Err(NdkError::CmdFailed(Box::new(adb)));
}
let output = std::str::from_utf8(&output.stdout).unwrap();
let (_package, uid) = output
.lines()
.filter_map(|line| line.split_once(' '))
.find(|(package, _uid)| package.strip_prefix("package:") == Some(&self.package_name))
.ok_or(NdkError::PackageNotInOutput {
package: self.package_name.clone(),
output: output.to_owned(),
})?;
let uid = uid
.strip_prefix("uid:")
.ok_or(NdkError::UidNotInOutput(output.to_owned()))?;
let uid = uid
.split(',')
.find(|part| !part.trim().is_empty())
.unwrap_or(uid)
.trim();
uid.parse()
.map_err(|e| NdkError::NotAUid(e, uid.to_owned()))
}
}