pub mod env;
pub mod runner;
pub mod test;
use std::{
ffi::OsString,
fs, io,
panic::PanicHookInfo,
path::{Path, PathBuf},
process::Command,
time::Instant,
};
use anyhow::Context;
use cargo_metadata::{Artifact, CrateType, MetadataCommand, semver::Version};
use clap::{CommandFactory, Parser};
use filetime::FileTime;
use crate::{
ARCH,
meta::{Target, default_targets},
shell::{Shell, Verbosity},
sysroot_suffix, sysroot_target,
};
trait CommandExt {
fn with_serial(self, serial: Option<&str>) -> Self;
}
impl CommandExt for Command {
fn with_serial(mut self, serial: Option<&str>) -> Self {
if let Some(serial) = serial {
self.arg("-s").arg(serial);
}
self
}
}
#[derive(Debug, Parser, Clone)]
struct BuildArgs {
#[arg(short, long, env = "CARGO_NDK_TARGET", value_delimiter = ',')]
target: Vec<Target>,
#[arg(short = 'P', long, default_value_t = 21, env = "CARGO_NDK_PLATFORM")]
platform: u8,
#[arg(long, default_value_t = false, env = "CARGO_NDK_LINK_BUILTINS")]
link_builtins: bool,
#[arg(long, default_value_t = false, env = "CARGO_NDK_LINK_LIBCXX_SHARED")]
link_libcxx_shared: bool,
#[arg(short, long, value_name = "DIR", env = "CARGO_NDK_OUTPUT_PATH")]
output_dir: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
manifest_path: Option<PathBuf>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
cargo_args: Vec<String>,
}
fn highest_version_ndk_in_path(ndk_dir: &Path) -> Option<PathBuf> {
if ndk_dir.exists() {
fs::read_dir(ndk_dir)
.ok()?
.filter_map(Result::ok)
.filter_map(|x| {
let path = x.path();
path.components()
.next_back()
.and_then(|comp| comp.as_os_str().to_str())
.and_then(|name| Version::parse(name).ok())
.map(|version| (version, path))
})
.max_by(|(a, _), (b, _)| a.cmp(b))
.map(|(_, path)| path)
} else {
None
}
}
fn find_first_consistent_var_set<'a>(
vars: &'a [&str],
shell: &mut Shell,
) -> Option<(&'a str, OsString)> {
let mut first_var_set = None;
for var in vars {
if let Some(path) = std::env::var_os(var) {
if let Some((first_var, first_path)) = first_var_set.as_ref() {
if *first_path != path {
shell
.warn(format!(
"Environment variable `{first_var} = {first_path:#?}` doesn't match `{var} = {path:#?}`"
))
.unwrap();
}
continue;
}
first_var_set = Some((*var, path));
}
}
first_var_set
}
fn derive_ndk_path(shell: &mut Shell) -> Option<(PathBuf, String)> {
let ndk_vars = [
"ANDROID_NDK_HOME",
"ANDROID_NDK_ROOT",
"ANDROID_NDK_PATH",
"NDK_HOME",
];
if let Some((var_name, path)) = find_first_consistent_var_set(&ndk_vars, shell) {
let path = PathBuf::from(path);
return highest_version_ndk_in_path(&path)
.or(Some(path))
.map(|path| (path, var_name.to_string()));
}
let sdk_vars = ["ANDROID_HOME", "ANDROID_SDK_ROOT", "ANDROID_SDK_HOME"];
if let Some((var_name, sdk_path)) = find_first_consistent_var_set(&sdk_vars, shell) {
let ndk_path = PathBuf::from(&sdk_path).join("ndk");
if let Some(v) = highest_version_ndk_in_path(&ndk_path) {
return Some((v, var_name.to_string()));
}
}
let ndk_dir = default_ndk_dir();
highest_version_ndk_in_path(&ndk_dir).map(|path| (path, "standard location".to_string()))
}
fn default_ndk_dir() -> PathBuf {
#[cfg(windows)]
let dir = pathos::user::local_dir()
.unwrap()
.to_path_buf()
.join("Android")
.join("sdk")
.join("ndk");
#[cfg(target_os = "linux")]
let dir = pathos::xdg::home_dir()
.unwrap()
.join("Android")
.join("Sdk")
.join("ndk");
#[cfg(target_os = "macos")]
let dir = pathos::user::home_dir()
.unwrap()
.join("Library")
.join("Android")
.join("sdk")
.join("ndk");
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
let dir = PathBuf::new();
dir
}
fn derive_adb_path(shell: &mut Shell) -> anyhow::Result<PathBuf> {
let sdk_vars = ["ANDROID_HOME", "ANDROID_SDK_ROOT", "ANDROID_SDK_HOME"];
if let Some((_, sdk_path)) = find_first_consistent_var_set(&sdk_vars, shell) {
let adb_path = PathBuf::from(&sdk_path).join("platform-tools").join("adb");
#[cfg(windows)]
let adb_path = adb_path.with_extension("exe");
if adb_path.exists() {
return Ok(adb_path);
}
}
#[cfg(windows)]
let adb_name = "adb.exe";
#[cfg(not(windows))]
let adb_name = "adb";
if let Ok(output) = Command::new("which").arg(adb_name).output() {
if output.status.success() {
let path_str = String::from_utf8_lossy(&output.stdout);
let path_str = path_str.trim();
return Ok(PathBuf::from(path_str));
}
}
Err(anyhow::anyhow!(
"Could not find adb. Please set ANDROID_HOME or ensure adb is in your PATH."
))
}
fn derive_ndk_version(path: &Path) -> anyhow::Result<Version> {
let data = fs::read_to_string(path.join("source.properties"))?;
for line in data.split('\n') {
if line.starts_with("Pkg.Revision") {
let mut chunks = line.split(" = ");
let _ = chunks.next().ok_or_else(|| io::Error::other("No chunk"))?;
let version = chunks.next().ok_or_else(|| io::Error::other("No chunk"))?;
let version = match Version::parse(version) {
Ok(v) => v,
Err(_e) => {
return Err(anyhow::anyhow!(format!(
"Could not parse NDK version. Got: '{}'",
version
)));
}
};
return Ok(version);
}
}
Err(anyhow::anyhow!("Could not find Pkg.Revision in given path"))
}
fn is_supported_rustc_version() -> bool {
version_check::is_min_version("1.68.0").unwrap_or_default()
}
fn panic_hook(info: &PanicHookInfo<'_>) {
fn _attempt_shell(lines: &[String]) -> Result<(), anyhow::Error> {
let mut shell = Shell::new();
shell.error("cargo-ndk panicked! Generating report...")?;
for line in lines {
println!("{line}");
}
shell.error("end of panic report. Please report the above to: <https://github.com/bbqsrc/cargo-ndk/issues>")?;
Ok(())
}
let location = info.location().unwrap();
let msg = match info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => match info.payload().downcast_ref::<String>() {
Some(s) => &s[..],
None => "Box<dyn Any>",
},
};
let env = std::env::vars()
.map(|(x, y)| format!("{x}={y:?}"))
.collect::<Vec<_>>();
let args = std::env::args().collect::<Vec<_>>();
let lines = vec![
format!("location: {location}"),
format!("message: {msg}"),
format!("args: {args:?}"),
format!(
"pwd: {}",
std::env::current_dir()
.map(|x| x.display().to_string())
.unwrap_or_else(|_| "<unknown>".to_string())
),
format!("env:\n {}", env.join("\n ")),
];
if _attempt_shell(&lines).is_err() {
for line in lines {
eprintln!("{line}");
}
}
}
fn parse_mixed_args<T>(args: Vec<String>) -> anyhow::Result<T>
where
T: clap::Parser + Clone + clap::CommandFactory + HasCargoArgs,
{
let mut global_args = vec!["cargo-ndk".to_string()];
let mut cargo_args = Vec::new();
let mut i = 1;
let cmd = T::command();
let mut global_flags = Vec::new();
let mut value_flags = Vec::new();
for arg in cmd.get_arguments() {
if arg.get_id() == "cargo_args" {
continue;
}
if let Some(long) = arg.get_long() {
let long_flag = format!("--{long}");
global_flags.push(long_flag.clone());
if arg.get_action().takes_values() {
value_flags.push(long_flag);
}
}
if let Some(short) = arg.get_short() {
let short_flag = format!("-{short}");
global_flags.push(short_flag.clone());
if arg.get_action().takes_values() {
value_flags.push(short_flag);
}
}
}
while i < args.len() {
let arg = &args[i];
if global_flags.contains(&arg.to_string()) {
global_args.push(arg.clone());
if value_flags.contains(&arg.to_string()) && i + 1 < args.len() {
i += 1;
global_args.push(args[i].clone());
}
} else if arg.starts_with("--") && arg.contains('=') {
let flag_name = arg.split('=').next().unwrap();
if global_flags.contains(&flag_name.to_string()) {
global_args.push(arg.clone());
} else {
cargo_args.push(arg.clone());
}
} else {
cargo_args.push(arg.clone());
}
i += 1;
}
let mut parsed_args = T::try_parse_from(&global_args)?;
parsed_args.set_cargo_args(cargo_args);
Ok(parsed_args)
}
trait HasCargoArgs {
fn set_cargo_args(&mut self, args: Vec<String>);
}
impl HasCargoArgs for BuildArgs {
fn set_cargo_args(&mut self, args: Vec<String>) {
self.cargo_args = args;
}
}
trait StringVecExt {
fn contains_str(&self, value: &str) -> bool;
}
impl StringVecExt for Vec<String> {
fn contains_str(&self, value: &str) -> bool {
self.iter().any(|s| s == value)
}
}
fn init(args: Vec<String>) -> anyhow::Result<(Shell, Vec<String>)> {
if std::env::var_os("CARGO_NDK_NO_PANIC_HOOK").is_none() {
std::panic::set_hook(Box::new(panic_hook));
}
if args.contains_str("--help") {
BuildArgs::command().print_long_help().unwrap();
std::process::exit(0);
}
if args.contains_str("-h") {
BuildArgs::command().print_help().unwrap();
std::process::exit(0);
}
if args.contains_str("--version") || args.contains_str("-V") {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
let verbosity = if args.contains_str("-q") {
Verbosity::Quiet
} else if args.contains_str("-vv") {
Verbosity::VeryVerbose
} else if args.contains_str("-v") || args.contains_str("--verbose") {
Verbosity::Verbose
} else {
Verbosity::Normal
};
let color = args
.iter()
.position(|x| x == "--color")
.and_then(|p| args.get(p + 1))
.map(|x| &**x);
let mut shell = Shell::new();
shell.set_verbosity(verbosity);
shell.set_color_choice(color)?;
shell.verbose(|shell| {
shell.status_with_color(
"Using",
format!("cargo-ndk v{}", env!("CARGO_PKG_VERSION")),
termcolor::Color::Cyan,
)
})?;
if !is_supported_rustc_version() {
shell.error("Rust compiler is too old and not supported by cargo-ndk.")?;
shell.note("Upgrade Rust to at least v1.68.0.")?;
std::process::exit(1);
}
Ok((shell, args))
}
pub fn run(args: Vec<String>) -> anyhow::Result<()> {
let (mut shell, args) = init(args)?;
let args = match parse_mixed_args::<BuildArgs>(args) {
Ok(args) => args,
Err(e) => {
shell.error(e)?;
std::process::exit(2);
}
};
if args.cargo_args.is_empty() {
shell.error("No args found to pass to cargo!")?;
shell.note("You still need to specify build arguments to cargo to achieve anything. :)")?;
std::process::exit(1);
}
let metadata = match MetadataCommand::new().no_deps().exec() {
Ok(v) => v,
Err(e) => {
shell.error("Failed to load Cargo.toml in current directory.")?;
shell.error(e)?;
std::process::exit(1);
}
};
let out_dir = metadata.target_directory;
let (ndk_home, ndk_detection_method) = match derive_ndk_path(&mut shell) {
Some((path, method)) => (path, method),
None => {
shell.error("Could not find any NDK.")?;
shell.note(
"Set the environment ANDROID_NDK_HOME to your NDK installation's root directory,\nor install the NDK using Android Studio."
)?;
std::process::exit(1);
}
};
let ndk_version = match derive_ndk_version(&ndk_home) {
Ok(v) => v,
Err(e) => {
shell.error(format!(
"Error detecting NDK version for path {}",
ndk_home.display()
))?;
shell.error(e)?;
std::process::exit(1);
}
};
shell.verbose(|shell| {
shell.status_with_color(
"Detected",
format!(
"NDK v{} ({}) [{}]",
ndk_version,
ndk_home.display(),
ndk_detection_method
),
termcolor::Color::Cyan,
)
})?;
let working_dir = std::env::current_dir().expect("current directory could not be resolved");
let cargo_args = &args.cargo_args;
let cargo_manifest = args
.manifest_path
.or_else(|| {
if let Some(selected_package) = cargo_args
.iter()
.position(|arg| arg == "-p" || arg == "--package")
.and_then(|idx| cargo_args.get(idx + 1))
{
let selected_package = metadata
.packages
.iter()
.find(|p| p.name.as_str() == selected_package)
.unwrap_or_else(|| panic!("unknown package: {selected_package}"));
Some(selected_package.manifest_path.as_std_path().to_path_buf())
} else {
None
}
})
.unwrap_or_else(|| working_dir.join("Cargo.toml"));
let cmake_toolchain_path = ndk_home
.join("build")
.join("cmake")
.join("android.toolchain.cmake");
shell.very_verbose(|shell| {
shell.status_with_color(
"Exporting",
format!("CARGO_NDK_CMAKE_TOOLCHAIN_PATH={:?}", &cmake_toolchain_path),
termcolor::Color::Cyan,
)
})?;
unsafe {
std::env::set_var("CARGO_NDK_CMAKE_TOOLCHAIN_PATH", cmake_toolchain_path);
}
let platform = args.platform;
let targets = if !args.target.is_empty() {
args.target
} else {
default_targets().to_vec()
};
if let Some(output_dir) = args.output_dir.as_ref() {
if let Err(e) = fs::create_dir_all(output_dir) {
shell.error(format!("failed to create output dir, {e}"))?;
std::process::exit(1);
}
let output_dir = match dunce::canonicalize(output_dir) {
Ok(p) => p,
Err(e) => {
shell.error(format!("failed to canonicalize output dir, {e}"))?;
if out_dir.is_absolute() {
output_dir.clone()
} else {
std::process::exit(1)
}
}
};
shell.verbose(|shell| {
shell.status_with_color(
"Exporting",
format!("CARGO_NDK_OUTPUT_PATH={output_dir:?}"),
termcolor::Color::Cyan,
)
})?;
unsafe {
std::env::set_var("CARGO_NDK_OUTPUT_PATH", output_dir);
}
}
shell.verbose(|shell| {
shell.status_with_color(
"Setting",
format!("Android SDK platform level to {platform}"),
termcolor::Color::Cyan,
)
})?;
unsafe {
std::env::set_var("CARGO_NDK_ANDROID_PLATFORM", platform.to_string());
}
shell.verbose(|shell| {
shell.status_with_color(
"Building",
format!(
"targets ({})",
targets
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
),
termcolor::Color::Cyan,
)
})?;
let start_time = Instant::now();
let targets = targets
.into_iter()
.map(|target| {
let triple = target.triple();
shell.status("Building", format!("{} ({})", &target, &triple))?;
shell.very_verbose(|shell| {
shell.status_with_color(
"Exporting",
format!("CARGO_NDK_ANDROID_PLATFORM={:?}", &target.to_string()),
termcolor::Color::Cyan,
)
})?;
unsafe {
std::env::set_var("CARGO_NDK_ANDROID_PLATFORM", target.to_string());
}
shell.very_verbose(|shell| {
shell.status_with_color(
"Exporting",
format!("ANDROID_PLATFORM={platform}"),
termcolor::Color::Cyan,
)
})?;
unsafe {
std::env::set_var("ANDROID_PLATFORM", platform.to_string());
}
let android_abi = target.to_string();
shell.very_verbose(|shell| {
shell.status_with_color(
"Exporting",
format!("ANDROID_ABI={:?}", &android_abi),
termcolor::Color::Cyan,
)
})?;
unsafe {
std::env::set_var("ANDROID_ABI", android_abi);
}
let (status, artifacts) = crate::cargo::run(
&mut shell,
&working_dir,
&ndk_home,
&ndk_version,
triple,
platform,
args.link_builtins,
args.link_libcxx_shared,
&args.cargo_args,
&cargo_manifest,
)?;
let code = status.code().unwrap_or(-1);
if code != 0 {
shell.note(
"If the build failed due to a missing target, you can run this command:",
)?;
shell.note("")?;
shell.note(format!(" rustup target install {triple}"))?;
std::process::exit(code);
}
Ok((target, artifacts))
})
.collect::<anyhow::Result<Vec<_>>>()?;
if let Some(output_dir) = args.output_dir.as_ref() {
shell.concise(|shell| {
shell.status(
"Copying",
format!(
"libraries to {}",
dunce::canonicalize(output_dir).unwrap().display()
),
)
})?;
for (target, artifacts) in targets.iter() {
shell.very_verbose(|shell| {
shell.note(format!("artifacts for {target}: {artifacts:?}"))
})?;
let arch_output_dir = output_dir.join(target.to_string());
fs::create_dir_all(&arch_output_dir).unwrap();
if artifacts.is_empty() || !artifacts.iter().any(artifact_is_cdylib) {
shell.error("No usable artifacts produced by cargo")?;
shell.error("Did you set the crate-type in Cargo.toml to include 'cdylib'?")?;
shell.error("For more info, see <https://doc.rust-lang.org/cargo/reference/cargo-targets.html#library>.")?;
std::process::exit(1);
}
if args.link_libcxx_shared {
let cargo_ndk_sysroot_path = ndk_home.join(sysroot_suffix(ARCH));
let cargo_ndk_sysroot_target = sysroot_target(target.triple());
let cargo_ndk_sysroot_libs_path = cargo_ndk_sysroot_path
.join("usr")
.join("lib")
.join(cargo_ndk_sysroot_target);
let dest = arch_output_dir.join("libc++_shared.so");
if is_fresh(&cargo_ndk_sysroot_libs_path, &dest)? {
shell.verbose(|shell| shell.status("Fresh", "libc++_shared.so"))?;
} else {
shell.verbose(|shell| {
shell.status(
"Copying",
format!("libc++_shared.so -> {}", &dest.display()),
)
})?;
fs::copy(cargo_ndk_sysroot_libs_path.join("libc++_shared.so"), &dest)
.with_context(|| {
format!(
"failed to copy libc++_shared.so from {} to {}",
cargo_ndk_sysroot_libs_path.display(),
output_dir.display()
)
})?;
}
}
for artifact in artifacts.iter().filter(|a| artifact_is_cdylib(a)) {
let Some(file) = artifact
.filenames
.iter()
.find(|name| name.extension() == Some("so"))
else {
shell.error("No cdylib file found to copy")?;
std::process::exit(1);
};
let dest = arch_output_dir.join(file.file_name().unwrap());
if is_fresh(file.as_std_path(), &dest)? {
shell.verbose(|shell| shell.status("Fresh", file))?;
continue;
}
shell.verbose(|shell| {
shell.status("Copying", format!("{file} -> {}", &dest.display()))
})?;
fs::copy(file, &dest)
.with_context(|| format!("failed to copy {file:?} over to {dest:?}"))?;
filetime::set_file_mtime(
&dest,
FileTime::from_last_modification_time(
&dest
.metadata()
.with_context(|| format!("failed getting metadata for {dest:?}"))?,
),
)
.with_context(|| format!("unable to update the modification time of {dest:?}"))?;
}
}
}
shell.verbose(|shell| {
let duration = start_time.elapsed();
let secs = duration.as_secs();
let d = if secs >= 60 {
format!("{}m {:02}s", secs / 60, secs % 60)
} else {
format!("{}.{:02}s", secs, duration.subsec_nanos() / 10_000_000)
};
let t = targets
.iter()
.map(|(target, _)| target.to_string())
.collect::<Vec<_>>()
.join(", ");
shell.status("Finished", format!("targets ({t}) in {d}",))
})?;
Ok(())
}
#[inline]
fn artifact_is_cdylib(artifact: &Artifact) -> bool {
artifact.target.crate_types.contains(&CrateType::CDyLib)
}
#[inline]
fn is_fresh(src: &Path, dest: &Path) -> anyhow::Result<bool> {
if !dest.exists() {
return Ok(false);
}
let src = src
.metadata()
.with_context(|| format!("failed getting metadata for {src:?}"))?;
let dest = dest
.metadata()
.with_context(|| format!("failed getting metadata for {dest:?}"))?;
let Some((src, dest)) = src.modified().ok().zip(dest.modified().ok()) else {
return Ok(false);
};
Ok(src <= dest)
}