feat(cli): add android build command (#4999)

This commit is contained in:
Lucas Fernandes Nogueira 2022-08-22 12:49:58 -03:00 committed by GitHub
parent b3a3afc7de
commit 4c9ea450c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 299 additions and 103 deletions

View File

@ -0,0 +1,6 @@
---
"cli.rs": minor
"cli.js": minor
---
Added `android build` command.

View File

@ -250,7 +250,7 @@ dependencies = [
[[package]]
name = "cargo-mobile"
version = "0.1.0"
source = "git+https://github.com/tauri-apps/cargo-mobile?branch=dev#c30e0f1338632a9d904e0aca602408fca9d70650"
source = "git+https://github.com/tauri-apps/cargo-mobile?branch=dev#76732050ab72f308a202aeb580bcdfa6936c4e28"
dependencies = [
"cocoa",
"colored 1.9.3",

View File

@ -58,104 +58,12 @@ pub struct Options {
}
pub fn command(mut options: Options) -> Result<()> {
let (merge_config, merge_config_path) = if let Some(config) = &options.config {
if config.starts_with('{') {
(Some(config.to_string()), None)
} else {
(
Some(
std::fs::read_to_string(&config)
.with_context(|| "failed to read custom configuration")?,
),
Some(config.clone()),
)
}
} else {
(None, None)
};
options.config = merge_config;
let tauri_path = tauri_dir();
set_current_dir(&tauri_path).with_context(|| "failed to change current working directory")?;
let mut interface = setup(&mut options)?;
let config = get_config(options.config.as_deref())?;
let config_guard = config.lock().unwrap();
let config_ = config_guard.as_ref().unwrap();
let mut interface = AppInterface::new(config_)?;
let bundle_identifier_source = match config_.find_bundle_identifier_overwriter() {
Some(source) if source == MERGE_CONFIG_EXTENSION_NAME => merge_config_path.unwrap_or(source),
Some(source) => source,
None => "tauri.conf.json".into(),
};
if config_.tauri.bundle.identifier == "com.tauri.dev" {
error!(
"You must change the bundle identifier in `{} > tauri > bundle > identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
bundle_identifier_source
);
std::process::exit(1);
}
if config_
.tauri
.bundle
.identifier
.chars()
.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
{
error!(
"The bundle identifier \"{}\" set in `{} > tauri > bundle > identifier`. The bundle identifier string must contain only alphanumeric characters (AZ, az, and 09), hyphens (-), and periods (.).",
config_.tauri.bundle.identifier,
bundle_identifier_source
);
std::process::exit(1);
}
if let Some(before_build) = config_.build.before_build_command.clone() {
run_hook("beforeBuildCommand", before_build, options.debug)?;
}
if let AppUrl::Url(WindowUrl::App(web_asset_path)) = &config_.build.dist_dir {
if !web_asset_path.exists() {
return Err(anyhow::anyhow!(
"Unable to find your web assets, did you forget to build your web app? Your distDir is set to \"{:?}\".",
web_asset_path
));
}
if web_asset_path.canonicalize()?.file_name() == Some(std::ffi::OsStr::new("src-tauri")) {
return Err(anyhow::anyhow!(
"The configured distDir is the `src-tauri` folder.
Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
));
}
let mut out_folders = Vec::new();
for folder in &["node_modules", "src-tauri", "target"] {
if web_asset_path.join(folder).is_dir() {
out_folders.push(folder.to_string());
}
}
if !out_folders.is_empty() {
return Err(anyhow::anyhow!(
"The configured distDir includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
out_folders,
if out_folders.len() == 1 { "folder" }else { "folders" }
)
);
}
}
if options.runner.is_none() {
options.runner = config_.build.runner.clone();
}
if let Some(list) = options.features.as_mut() {
list.extend(config_.build.features.clone().unwrap_or_default());
}
let app_settings = interface.app_settings();
let interface_options = options.clone().into();
@ -310,6 +218,108 @@ pub fn command(mut options: Options) -> Result<()> {
Ok(())
}
pub fn setup(options: &mut Options) -> Result<AppInterface> {
let (merge_config, merge_config_path) = if let Some(config) = &options.config {
if config.starts_with('{') {
(Some(config.to_string()), None)
} else {
(
Some(
std::fs::read_to_string(&config)
.with_context(|| "failed to read custom configuration")?,
),
Some(config.clone()),
)
}
} else {
(None, None)
};
options.config = merge_config;
let tauri_path = tauri_dir();
set_current_dir(&tauri_path).with_context(|| "failed to change current working directory")?;
let config = get_config(options.config.as_deref())?;
let config_guard = config.lock().unwrap();
let config_ = config_guard.as_ref().unwrap();
let interface = AppInterface::new(config_)?;
let bundle_identifier_source = match config_.find_bundle_identifier_overwriter() {
Some(source) if source == MERGE_CONFIG_EXTENSION_NAME => merge_config_path.unwrap_or(source),
Some(source) => source,
None => "tauri.conf.json".into(),
};
if config_.tauri.bundle.identifier == "com.tauri.dev" {
error!(
"You must change the bundle identifier in `{} > tauri > bundle > identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
bundle_identifier_source
);
std::process::exit(1);
}
if config_
.tauri
.bundle
.identifier
.chars()
.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
{
error!(
"The bundle identifier \"{}\" set in `{} > tauri > bundle > identifier`. The bundle identifier string must contain only alphanumeric characters (AZ, az, and 09), hyphens (-), and periods (.).",
config_.tauri.bundle.identifier,
bundle_identifier_source
);
std::process::exit(1);
}
if let Some(before_build) = config_.build.before_build_command.clone() {
run_hook("beforeBuildCommand", before_build, options.debug)?;
}
if let AppUrl::Url(WindowUrl::App(web_asset_path)) = &config_.build.dist_dir {
if !web_asset_path.exists() {
return Err(anyhow::anyhow!(
"Unable to find your web assets, did you forget to build your web app? Your distDir is set to \"{:?}\".",
web_asset_path
));
}
if web_asset_path.canonicalize()?.file_name() == Some(std::ffi::OsStr::new("src-tauri")) {
return Err(anyhow::anyhow!(
"The configured distDir is the `src-tauri` folder.
Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
));
}
let mut out_folders = Vec::new();
for folder in &["node_modules", "src-tauri", "target"] {
if web_asset_path.join(folder).is_dir() {
out_folders.push(folder.to_string());
}
}
if !out_folders.is_empty() {
return Err(anyhow::anyhow!(
"The configured distDir includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > distDir`.",
out_folders,
if out_folders.len() == 1 { "folder" }else { "folders" }
)
);
}
}
if options.runner.is_none() {
options.runner = config_.build.runner.clone();
}
if let Some(list) = options.features.as_mut() {
list.extend(config_.build.features.clone().unwrap_or_default());
}
Ok(interface)
}
fn run_hook(name: &str, hook: HookCommand, debug: bool) -> Result<()> {
let (script, script_cwd) = match hook {
HookCommand::Script(s) if s.is_empty() => (None, None),

View File

@ -4,7 +4,7 @@
use cargo_mobile::{
android::{
adb,
aab, adb, apk,
config::{Config as AndroidConfig, Metadata as AndroidMetadata},
device::{Device, RunError},
env::{Env, Error as EnvError},
@ -14,7 +14,7 @@ use cargo_mobile::{
device::PromptError,
opts::{NoiseLevel, Profile},
os,
target::call_for_targets_with_fallback,
target::{call_for_targets_with_fallback, TargetTrait},
util::prompt,
};
use clap::{Parser, Subcommand};
@ -30,6 +30,8 @@ use crate::{
Result,
};
use std::{fmt::Write, path::PathBuf};
pub(crate) mod project;
#[derive(Debug, thiserror::Error)]
@ -46,8 +48,10 @@ enum Error {
OpenFailed(os::OpenFileError),
#[error("{0}")]
DevFailed(String),
#[error("{0}")]
BuildFailed(String),
#[error(transparent)]
BuildFailed(BuildError),
AndroidStudioScriptFailed(BuildError),
#[error(transparent)]
RunFailed(RunError),
#[error("{0}")]
@ -77,7 +81,8 @@ pub struct AndroidStudioScriptOptions {
long = "target",
multiple_occurrences(true),
multiple_values(true),
value_parser(clap::builder::PossibleValuesParser::new(["aarch64", "armv7", "i686", "x86_64"]))
default_value = Target::DEFAULT_KEY,
value_parser(clap::builder::PossibleValuesParser::new(Target::name_list()))
)]
targets: Option<Vec<String>>,
/// Builds with the release flag
@ -120,12 +125,59 @@ impl From<DevOptions> for crate::dev::Options {
}
}
#[derive(Debug, Clone, Parser)]
#[clap(about = "Android build")]
pub struct BuildOptions {
/// Builds with the debug flag
#[clap(short, long)]
pub debug: bool,
/// Which targets to build (all by default).
#[clap(
short,
long = "target",
multiple_occurrences(true),
multiple_values(true),
value_parser(clap::builder::PossibleValuesParser::new(Target::name_list()))
)]
targets: Option<Vec<String>>,
/// List of cargo features to activate
#[clap(short, long, multiple_occurrences(true), multiple_values(true))]
pub features: Option<Vec<String>>,
/// JSON string or path to JSON file to merge with tauri.conf.json
#[clap(short, long)]
pub config: Option<String>,
/// Whether to split the APKs and AABs per ABIs.
#[clap(long)]
pub split_per_abi: bool,
/// Build APKs.
#[clap(long)]
pub apk: bool,
/// Build AABs.
#[clap(long)]
pub aab: bool,
}
impl From<BuildOptions> for crate::build::Options {
fn from(options: BuildOptions) -> Self {
Self {
runner: None,
debug: options.debug,
target: None,
features: options.features,
bundles: None,
config: options.config,
args: Vec::new(),
}
}
}
#[derive(Subcommand)]
enum Commands {
Init(InitOptions),
/// Open project in Android Studio
Open,
Dev(DevOptions),
Build(BuildOptions),
#[clap(hide(true))]
AndroidStudioScript(AndroidStudioScriptOptions),
}
@ -134,8 +186,9 @@ pub fn command(cli: Cli) -> Result<()> {
match cli.command {
Commands::Init(options) => init_command(options, MobileTarget::Android)?,
Commands::Open => open()?,
Commands::AndroidStudioScript(options) => android_studio_script(options)?,
Commands::Dev(options) => dev(options)?,
Commands::Build(options) => build(options)?,
Commands::AndroidStudioScript(options) => android_studio_script(options)?,
}
Ok(())
@ -182,8 +235,134 @@ fn device_prompt<'a>(env: &'_ Env) -> Result<Device<'a>, PromptError<adb::device
}
}
fn get_targets_or_all<'a>(targets: Vec<String>) -> Result<Vec<&'a Target<'a>>, Error> {
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).ok_or_else(|| {
Error::TargetInvalid(format!(
"Target {} is invalid; the possible targets are {}",
t, possible_targets
))
})?;
outs.push(target);
}
Ok(outs)
}
}
fn build(options: BuildOptions) -> Result<()> {
with_config(|root_conf, config, _metadata| {
ensure_init(config.project_dir(), MobileTarget::Android)
.map_err(|e| Error::ProjectNotInitialized(e.to_string()))?;
let env = Env::new().map_err(Error::EnvInitFailed)?;
super::init::init_dot_cargo(root_conf, Some(&env)).map_err(Error::InitDotCargo)?;
run_build(options, config, env).map_err(|e| Error::BuildFailed(e.to_string()))
})
.map_err(Into::into)
}
fn run_build(mut options: BuildOptions, config: &AndroidConfig, env: Env) -> Result<()> {
let profile = if options.debug {
Profile::Debug
} else {
Profile::Release
};
if !(options.apk || options.aab) {
// if the user didn't specify the format to build, we'll do both
options.apk = true;
options.aab = true;
}
let bundle_identifier = {
let tauri_config = get_tauri_config(None)?;
let tauri_config_guard = tauri_config.lock().unwrap();
let tauri_config_ = tauri_config_guard.as_ref().unwrap();
tauri_config_.tauri.bundle.identifier.clone()
};
let mut build_options = options.clone().into();
let interface = crate::build::setup(&mut build_options)?;
let app_settings = interface.app_settings();
let bin_path = app_settings.app_binary_path(&InterfaceOptions {
debug: build_options.debug,
..Default::default()
})?;
let out_dir = bin_path.parent().unwrap();
let _lock = flock::open_rw(&out_dir.join("lock").with_extension("android"), "Android")?;
let cli_options = CliOptions {
features: build_options.features.clone(),
args: build_options.args.clone(),
vars: Default::default(),
};
write_options(cli_options, &bundle_identifier, MobileTarget::Android)?;
options
.features
.get_or_insert(Vec::new())
.push("custom-protocol".into());
let apk_outputs = if options.apk {
apk::build(
config,
&env,
NoiseLevel::Polite,
profile,
get_targets_or_all(Vec::new())?,
options.split_per_abi,
)?
} else {
Vec::new()
};
let aab_outputs = if options.aab {
aab::build(
config,
&env,
NoiseLevel::Polite,
profile,
get_targets_or_all(Vec::new())?,
options.split_per_abi,
)?
} else {
Vec::new()
};
log_finished(apk_outputs, "APK");
log_finished(aab_outputs, "AAB");
Ok(())
}
fn log_finished(outputs: Vec<PathBuf>, kind: &str) {
if !outputs.is_empty() {
let mut printable_paths = String::new();
for path in &outputs {
writeln!(printable_paths, " {}", path.display()).unwrap();
}
log::info!(action = "Finished"; "{} {}{} at:\n{}", outputs.len(), kind, if outputs.len() == 1 { "" } else { "s" }, printable_paths);
}
}
fn dev(options: DevOptions) -> Result<()> {
with_config(|_, config, _metadata| {
ensure_init(config.project_dir(), MobileTarget::Android)
.map_err(|e| Error::ProjectNotInitialized(e.to_string()))?;
run_dev(options, config).map_err(|e| Error::DevFailed(e.to_string()))
})
.map_err(Into::into)
@ -194,8 +373,7 @@ fn run_dev(options: DevOptions, config: &AndroidConfig) -> Result<()> {
let mut interface = crate::dev::setup(&mut dev_options)?;
let bundle_identifier = {
let tauri_config =
get_tauri_config(None).map_err(|e| Error::InvalidTauriConfig(e.to_string()))?;
let tauri_config = get_tauri_config(None)?;
let tauri_config_guard = tauri_config.lock().unwrap();
let tauri_config_ = tauri_config_guard.as_ref().unwrap();
tauri_config_.tauri.bundle.identifier.clone()
@ -319,7 +497,7 @@ fn android_studio_script(options: AndroidStudioScriptOptions) -> Result<()> {
|target: &Target| {
target
.build(config, metadata, &env, NoiseLevel::Polite, true, profile)
.map_err(Error::BuildFailed)
.map_err(Error::AndroidStudioScriptFailed)
},
)
.map_err(|e| Error::TargetInvalid(e.to_string()))?

View File

@ -41,9 +41,11 @@ android {
flavorDimensions.add("abi")
productFlavors {
create("universal") {
val abiList = findProperty("abiList") as? String
dimension = "abi"
ndk {
abiFilters += listOf(
abiFilters += abiList?.split(",")?.map { it.trim() } ?: listOf(
{{~#each targets}}
"{{this.abi}}",{{/each}}
)