mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-11-28 03:47:37 +03:00
feat(cli): add android build
command (#4999)
This commit is contained in:
parent
b3a3afc7de
commit
4c9ea450c3
6
.changes/cli-android-build.md
Normal file
6
.changes/cli-android-build.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"cli.rs": minor
|
||||
"cli.js": minor
|
||||
---
|
||||
|
||||
Added `android build` command.
|
2
tooling/cli/Cargo.lock
generated
2
tooling/cli/Cargo.lock
generated
@ -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",
|
||||
|
@ -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 (A–Z, a–z, and 0–9), 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 (A–Z, a–z, and 0–9), 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),
|
||||
|
@ -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()))?
|
||||
|
@ -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}}
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user