mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-01 10:01:07 +02:00
299 lines
8.7 KiB
Rust
299 lines
8.7 KiB
Rust
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
use clap::{Parser, Subcommand};
|
|
use std::{env::set_var, fs::create_dir, process::exit, thread::sleep, time::Duration};
|
|
use sublime_fuzzy::best_match;
|
|
use tauri_mobile::{
|
|
android::{
|
|
adb,
|
|
config::{Config as AndroidConfig, Metadata as AndroidMetadata, Raw as RawAndroidConfig},
|
|
device::Device,
|
|
emulator,
|
|
env::Env,
|
|
target::Target,
|
|
},
|
|
config::app::App,
|
|
opts::{FilterLevel, NoiseLevel},
|
|
os,
|
|
util::prompt,
|
|
};
|
|
|
|
use super::{
|
|
ensure_init, get_app,
|
|
init::{command as init_command, configure_cargo},
|
|
log_finished, read_options, setup_dev_config, CliOptions, Target as MobileTarget,
|
|
MIN_DEVICE_MATCH_SCORE,
|
|
};
|
|
use crate::{
|
|
helpers::config::{get as get_tauri_config, Config as TauriConfig},
|
|
Result,
|
|
};
|
|
|
|
mod android_studio_script;
|
|
mod build;
|
|
mod dev;
|
|
mod open;
|
|
pub(crate) mod project;
|
|
|
|
#[derive(Parser)]
|
|
#[clap(
|
|
author,
|
|
version,
|
|
about = "Android commands",
|
|
subcommand_required(true),
|
|
arg_required_else_help(true)
|
|
)]
|
|
pub struct Cli {
|
|
#[clap(subcommand)]
|
|
command: Commands,
|
|
}
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[clap(about = "Initializes a Tauri Android project")]
|
|
pub struct InitOptions {
|
|
/// Skip prompting for values
|
|
#[clap(long)]
|
|
ci: bool,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Commands {
|
|
Init(InitOptions),
|
|
/// Open project in Android Studio
|
|
Open,
|
|
Dev(dev::Options),
|
|
Build(build::Options),
|
|
#[clap(hide(true))]
|
|
AndroidStudioScript(android_studio_script::Options),
|
|
}
|
|
|
|
pub fn command(cli: Cli, verbosity: u8) -> Result<()> {
|
|
let noise_level = NoiseLevel::from_occurrences(verbosity as u64);
|
|
match cli.command {
|
|
Commands::Init(options) => init_command(MobileTarget::Android, options.ci, false)?,
|
|
Commands::Open => open::command()?,
|
|
Commands::Dev(options) => dev::command(options, noise_level)?,
|
|
Commands::Build(options) => build::command(options, noise_level)?,
|
|
Commands::AndroidStudioScript(options) => android_studio_script::command(options)?,
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_config(
|
|
app: Option<App>,
|
|
config: &TauriConfig,
|
|
cli_options: &CliOptions,
|
|
) -> (App, AndroidConfig, AndroidMetadata) {
|
|
let app = app.unwrap_or_else(|| get_app(config));
|
|
let android_options = cli_options.clone();
|
|
|
|
let raw = RawAndroidConfig {
|
|
features: android_options.features.clone(),
|
|
logcat_filter_specs: vec![
|
|
"RustStdoutStderr".into(),
|
|
format!(
|
|
"*:{}",
|
|
match cli_options.noise_level {
|
|
NoiseLevel::Polite => FilterLevel::Info,
|
|
NoiseLevel::LoudAndProud => FilterLevel::Debug,
|
|
NoiseLevel::FranklyQuitePedantic => FilterLevel::Verbose,
|
|
}
|
|
.logcat()
|
|
),
|
|
],
|
|
min_sdk_version: Some(config.tauri.bundle.android.min_sdk_version),
|
|
..Default::default()
|
|
};
|
|
let config = AndroidConfig::from_raw(app.clone(), Some(raw)).unwrap();
|
|
|
|
let metadata = AndroidMetadata {
|
|
supported: true,
|
|
cargo_args: Some(android_options.args),
|
|
features: android_options.features,
|
|
..Default::default()
|
|
};
|
|
|
|
set_var(
|
|
"WRY_ANDROID_PACKAGE",
|
|
format!("{}.{}", app.reverse_domain(), app.name_snake()),
|
|
);
|
|
set_var("WRY_ANDROID_LIBRARY", app.lib_name());
|
|
set_var("TAURI_ANDROID_PROJECT_PATH", config.project_dir());
|
|
|
|
let src_main_dir = config.project_dir().join("app/src/main").join(format!(
|
|
"java/{}/{}",
|
|
app.reverse_domain().replace('.', "/"),
|
|
app.name_snake()
|
|
));
|
|
if config.project_dir().exists() {
|
|
if src_main_dir.exists() {
|
|
let _ = create_dir(src_main_dir.join("generated"));
|
|
} else {
|
|
log::error!(
|
|
"Project directory {} does not exist. Did you update the package name in `Cargo.toml` or the bundle identifier in `tauri.conf.json > tauri > bundle > identifier`? Save your changes, delete the `gen/android` folder and run `tauri android init` to recreate the Android project.",
|
|
src_main_dir.display()
|
|
);
|
|
exit(1);
|
|
}
|
|
}
|
|
set_var(
|
|
"WRY_ANDROID_KOTLIN_FILES_OUT_DIR",
|
|
src_main_dir.join("generated"),
|
|
);
|
|
|
|
(app, config, metadata)
|
|
}
|
|
|
|
pub fn with_config<T>(
|
|
cli_options: Option<CliOptions>,
|
|
f: impl FnOnce(&App, &AndroidConfig, &AndroidMetadata, CliOptions) -> Result<T>,
|
|
) -> Result<T> {
|
|
let (app, config, metadata, cli_options) = {
|
|
let tauri_config = get_tauri_config(None)?;
|
|
let tauri_config_guard = tauri_config.lock().unwrap();
|
|
let tauri_config_ = tauri_config_guard.as_ref().unwrap();
|
|
let cli_options =
|
|
cli_options.unwrap_or_else(|| read_options(&tauri_config_.tauri.bundle.identifier));
|
|
let (app, config, metadata) = get_config(None, tauri_config_, &cli_options);
|
|
(app, config, metadata, cli_options)
|
|
};
|
|
f(&app, &config, &metadata, cli_options)
|
|
}
|
|
|
|
fn env() -> Result<Env> {
|
|
let env = super::env()?;
|
|
tauri_mobile::android::env::Env::from_env(env).map_err(Into::into)
|
|
}
|
|
|
|
fn delete_codegen_vars() {
|
|
for (k, _) in std::env::vars() {
|
|
if k.starts_with("WRY_") && (k.ends_with("CLASS_EXTENSION") || k.ends_with("CLASS_INIT")) {
|
|
std::env::remove_var(k);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn adb_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
|
|
let device_list = adb::device_list(env)
|
|
.map_err(|cause| anyhow::anyhow!("Failed to detect connected Android devices: {cause}"))?;
|
|
if !device_list.is_empty() {
|
|
let device = if let Some(t) = target {
|
|
let (device, score) = device_list
|
|
.into_iter()
|
|
.rev()
|
|
.map(|d| {
|
|
let score = best_match(t, d.name()).map_or(0, |m| m.score());
|
|
(d, score)
|
|
})
|
|
.max_by_key(|(_, score)| *score)
|
|
// we already checked the list is not empty
|
|
.unwrap();
|
|
if score > MIN_DEVICE_MATCH_SCORE {
|
|
device
|
|
} else {
|
|
anyhow::bail!("Could not find an Android device matching {t}")
|
|
}
|
|
} else if device_list.len() > 1 {
|
|
let index = prompt::list(
|
|
concat!("Detected ", "Android", " devices"),
|
|
device_list.iter(),
|
|
"device",
|
|
None,
|
|
"Device",
|
|
)
|
|
.map_err(|cause| anyhow::anyhow!("Failed to prompt for Android device: {cause}"))?;
|
|
device_list.into_iter().nth(index).unwrap()
|
|
} else {
|
|
device_list.into_iter().next().unwrap()
|
|
};
|
|
|
|
log::info!(
|
|
"Detected connected device: {} with target {:?}",
|
|
device,
|
|
device.target().triple,
|
|
);
|
|
Ok(device)
|
|
} else {
|
|
Err(anyhow::anyhow!("No connected Android devices detected"))
|
|
}
|
|
}
|
|
|
|
fn emulator_prompt(env: &'_ Env, target: Option<&str>) -> Result<emulator::Emulator> {
|
|
let emulator_list = emulator::avd_list(env).unwrap_or_default();
|
|
if !emulator_list.is_empty() {
|
|
let emulator = if let Some(t) = target {
|
|
let (device, score) = emulator_list
|
|
.into_iter()
|
|
.rev()
|
|
.map(|d| {
|
|
let score = best_match(t, d.name()).map_or(0, |m| m.score());
|
|
(d, score)
|
|
})
|
|
.max_by_key(|(_, score)| *score)
|
|
// we already checked the list is not empty
|
|
.unwrap();
|
|
if score > MIN_DEVICE_MATCH_SCORE {
|
|
device
|
|
} else {
|
|
anyhow::bail!("Could not find an Android Emulator matching {t}")
|
|
}
|
|
} else if emulator_list.len() > 1 {
|
|
let index = prompt::list(
|
|
concat!("Detected ", "Android", " emulators"),
|
|
emulator_list.iter(),
|
|
"emulator",
|
|
None,
|
|
"Emulator",
|
|
)
|
|
.map_err(|cause| anyhow::anyhow!("Failed to prompt for Android Emulator device: {cause}"))?;
|
|
emulator_list.into_iter().nth(index).unwrap()
|
|
} else {
|
|
emulator_list.into_iter().next().unwrap()
|
|
};
|
|
|
|
Ok(emulator)
|
|
} else {
|
|
Err(anyhow::anyhow!("No available Android Emulator detected"))
|
|
}
|
|
}
|
|
|
|
fn device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
|
|
if let Ok(device) = adb_device_prompt(env, target) {
|
|
Ok(device)
|
|
} else {
|
|
let emulator = emulator_prompt(env, target)?;
|
|
log::info!("Starting emulator {}", emulator.name());
|
|
emulator.start_detached(env)?;
|
|
let mut tries = 0;
|
|
loop {
|
|
sleep(Duration::from_secs(2));
|
|
if let Ok(device) = adb_device_prompt(env, Some(emulator.name())) {
|
|
return Ok(device);
|
|
}
|
|
if tries >= 3 {
|
|
log::info!("Waiting for emulator to start... (maybe the emulator is unathorized or offline, run `adb devices` to check)");
|
|
} else {
|
|
log::info!("Waiting for emulator to start...");
|
|
}
|
|
tries += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> {
|
|
device_prompt(env, None).map(|device| device.target()).ok()
|
|
}
|
|
|
|
fn open_and_wait(config: &AndroidConfig, env: &Env) -> ! {
|
|
log::info!("Opening Android Studio");
|
|
if let Err(e) = os::open_file_with("Android Studio", config.project_dir(), &env.base) {
|
|
log::error!("{}", e);
|
|
}
|
|
loop {
|
|
sleep(Duration::from_secs(24 * 60 * 60));
|
|
}
|
|
}
|