use super::{
configure_cargo, delete_codegen_vars, ensure_init, env, get_app, get_config, inject_resources,
log_finished, open_and_wait, MobileTarget, OptionsHandle,
};
use crate::{
build::Options as BuildOptions,
error::Context,
helpers::{
app_paths::Dirs,
config::{get_config as get_tauri_config, ConfigMetadata},
flock,
},
interface::{AppInterface, Options as InterfaceOptions},
mobile::{android::generate_tauri_properties, write_options, CliOptions, TargetDevice},
ConfigValue, Error, Result,
};
use clap::{ArgAction, Parser};
use cargo_mobile2::{
android::{aab, apk, config::Config as AndroidConfig, env::Env, target::Target},
opts::{NoiseLevel, Profile},
target::TargetTrait,
};
use std::env::set_current_dir;
use std::path::Path;
#[derive(Debug, Clone, Parser)]
#[clap(
about = "Build your app in release mode for Android and generate APKs and AABs",
long_about = "Build your app in release mode for Android and generate APKs and AABs. It makes use of the `build.frontendDist` property from your `tauri.conf.json` file. It also runs your `build.beforeBuildCommand` which usually builds your frontend into `build.frontendDist`."
)]
pub struct Options {
#[clap(short, long)]
pub debug: bool,
#[clap(
short,
long = "target",
action = ArgAction::Append,
num_args(0..),
value_parser(clap::builder::PossibleValuesParser::new(Target::name_list()))
)]
pub targets: Option<Vec<String>>,
#[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
pub features: Vec<String>,
#[clap(short, long)]
pub config: Vec<ConfigValue>,
#[clap(long)]
pub split_per_abi: bool,
#[clap(long)]
pub apk: bool,
#[clap(long)]
pub aab: bool,
#[clap(skip)]
pub skip_bundle: bool,
#[clap(short, long)]
pub open: bool,
#[clap(long, env = "CI")]
pub ci: bool,
#[clap(last(true))]
pub args: Vec<String>,
#[clap(long)]
pub ignore_version_mismatches: bool,
#[clap(skip)]
pub target_device: Option<TargetDevice>,
}
impl From<Options> for BuildOptions {
fn from(options: Options) -> Self {
Self {
runner: None,
debug: options.debug,
target: None,
features: options.features,
bundles: None,
no_bundle: false,
config: options.config,
args: options.args,
ci: options.ci,
skip_stapling: false,
ignore_version_mismatches: options.ignore_version_mismatches,
no_sign: false,
}
}
}
pub struct BuiltApplication {
pub config: AndroidConfig,
pub interface: AppInterface,
#[allow(dead_code)]
options_handle: OptionsHandle,
}
pub fn command(options: Options, noise_level: NoiseLevel) -> Result<BuiltApplication> {
let dirs = crate::helpers::app_paths::resolve_dirs();
let tauri_config = get_tauri_config(
tauri_utils::platform::Target::Android,
&options
.config
.iter()
.map(|conf| &conf.0)
.collect::<Vec<_>>(),
dirs.tauri,
)?;
run(options, noise_level, &dirs, &tauri_config)
}
pub fn run(
options: Options,
noise_level: NoiseLevel,
dirs: &Dirs,
tauri_config: &ConfigMetadata,
) -> Result<BuiltApplication> {
delete_codegen_vars();
let mut build_options: BuildOptions = options.clone().into();
let first_target = Target::all()
.get(
options
.targets
.as_ref()
.and_then(|l| l.first().map(|t| t.as_str()))
.unwrap_or(Target::DEFAULT_KEY),
)
.unwrap();
build_options.target = Some(first_target.triple.into());
let interface = AppInterface::new(tauri_config, build_options.target.clone(), dirs.tauri)?;
interface.build_options(&mut build_options.args, &mut build_options.features, true);
let app = get_app(MobileTarget::Android, tauri_config, &interface, dirs.tauri);
let (config, metadata) = get_config(
&app,
tauri_config,
&build_options.features,
&CliOptions {
dev: false,
features: build_options.features.clone(),
args: build_options.args.clone(),
noise_level,
vars: Default::default(),
config: build_options.config.clone(),
target_device: None,
},
);
let profile = if options.debug {
Profile::Debug
} else {
Profile::Release
};
set_current_dir(dirs.tauri).context("failed to set current directory to Tauri directory")?;
ensure_init(
tauri_config,
config.app(),
config.project_dir(),
MobileTarget::Android,
options.ci,
)?;
let mut env = env(options.ci)?;
configure_cargo(&mut env, &config)?;
generate_tauri_properties(&config, tauri_config, false)?;
crate::build::setup(&interface, &mut build_options, tauri_config, dirs, true)?;
let installed_targets =
crate::interface::rust::installation::installed_targets().unwrap_or_default();
if !installed_targets.contains(&first_target.triple().into()) {
log::info!("Installing target {}", first_target.triple());
first_target
.install()
.map_err(|error| Error::CommandFailed {
command: "rustup target add".to_string(),
error,
})
.context("failed to install target")?;
}
first_target
.build(&config, &metadata, &env, noise_level, true, profile)
.context("failed to build Android app")?;
let open = options.open;
let options_handle = run_build(
&interface,
options,
build_options,
tauri_config,
profile,
&config,
&mut env,
noise_level,
dirs.tauri,
)?;
if open {
open_and_wait(&config, &env);
}
Ok(BuiltApplication {
config,
interface,
options_handle,
})
}
#[allow(clippy::too_many_arguments)]
fn run_build(
interface: &AppInterface,
mut options: Options,
build_options: BuildOptions,
tauri_config: &ConfigMetadata,
profile: Profile,
config: &AndroidConfig,
env: &mut Env,
noise_level: NoiseLevel,
tauri_dir: &Path,
) -> Result<OptionsHandle> {
if !(options.skip_bundle || options.apk || options.aab) {
options.apk = true;
options.aab = true;
}
let interface_options = InterfaceOptions {
debug: build_options.debug,
target: build_options.target.clone(),
args: build_options.args.clone(),
..Default::default()
};
let app_settings = interface.app_settings();
let out_dir = app_settings.out_dir(&interface_options, tauri_dir)?;
let _lock = flock::open_rw(out_dir.join("lock").with_extension("android"), "Android")?;
let cli_options = CliOptions {
dev: false,
features: build_options.features.clone(),
args: build_options.args.clone(),
noise_level,
vars: Default::default(),
config: build_options.config,
target_device: options.target_device.clone(),
};
let handle = write_options(tauri_config, cli_options)?;
inject_resources(config, tauri_config)?;
let apk_outputs = if options.apk {
apk::build(
config,
env,
noise_level,
profile,
get_targets_or_all(options.targets.clone().unwrap_or_default())?,
options.split_per_abi,
)
.context("failed to build APK")?
} else {
Vec::new()
};
let aab_outputs = if options.aab {
aab::build(
config,
env,
noise_level,
profile,
get_targets_or_all(options.targets.unwrap_or_default())?,
options.split_per_abi,
)
.context("failed to build AAB")?
} else {
Vec::new()
};
if !apk_outputs.is_empty() {
log_finished(apk_outputs, "APK");
}
if !aab_outputs.is_empty() {
log_finished(aab_outputs, "AAB");
}
Ok(handle)
}
fn get_targets_or_all<'a>(targets: Vec<String>) -> Result<Vec<&'a Target<'a>>> {
if targets.is_empty() {
Ok(Target::all().iter().map(|t| t.1).collect())
} else {
let mut outs = Vec::new();
let possible_targets = Target::all()
.keys()
.map(|key| key.to_string())
.collect::<Vec<String>>()
.join(",");
for t in targets {
let target = Target::for_name(&t).with_context(|| {
format!("Target {t} is invalid; the possible targets are {possible_targets}",)
})?;
outs.push(target);
}
Ok(outs)
}
}