mod.rs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use cargo_mobile2::{
  5. android::{
  6. adb,
  7. config::{Config as AndroidConfig, Metadata as AndroidMetadata, Raw as RawAndroidConfig},
  8. device::Device,
  9. emulator,
  10. env::Env,
  11. target::Target,
  12. },
  13. config::app::{App, DEFAULT_ASSET_DIR},
  14. opts::{FilterLevel, NoiseLevel},
  15. os,
  16. util::prompt,
  17. };
  18. use clap::{Parser, Subcommand};
  19. use std::{
  20. env::set_var,
  21. fs::{create_dir, create_dir_all, write},
  22. process::exit,
  23. thread::sleep,
  24. time::Duration,
  25. };
  26. use sublime_fuzzy::best_match;
  27. use super::{
  28. ensure_init, get_app,
  29. init::{command as init_command, configure_cargo},
  30. log_finished, read_options, setup_dev_config, CliOptions, OptionsHandle, Target as MobileTarget,
  31. MIN_DEVICE_MATCH_SCORE,
  32. };
  33. use crate::{helpers::config::Config as TauriConfig, Result};
  34. mod android_studio_script;
  35. mod build;
  36. mod dev;
  37. mod open;
  38. pub(crate) mod project;
  39. #[derive(Parser)]
  40. #[clap(
  41. author,
  42. version,
  43. about = "Android commands",
  44. subcommand_required(true),
  45. arg_required_else_help(true)
  46. )]
  47. pub struct Cli {
  48. #[clap(subcommand)]
  49. command: Commands,
  50. }
  51. #[derive(Debug, Parser)]
  52. #[clap(about = "Initialize Android target in the project")]
  53. pub struct InitOptions {
  54. /// Skip prompting for values
  55. #[clap(long, env = "CI")]
  56. ci: bool,
  57. /// Skips installing rust toolchains via rustup
  58. #[clap(long)]
  59. skip_targets_install: bool,
  60. }
  61. #[derive(Subcommand)]
  62. enum Commands {
  63. Init(InitOptions),
  64. /// Open project in Android Studio
  65. Open,
  66. Dev(dev::Options),
  67. Build(build::Options),
  68. #[clap(hide(true))]
  69. AndroidStudioScript(android_studio_script::Options),
  70. }
  71. pub fn command(cli: Cli, verbosity: u8) -> Result<()> {
  72. let noise_level = NoiseLevel::from_occurrences(verbosity as u64);
  73. match cli.command {
  74. Commands::Init(options) => init_command(
  75. MobileTarget::Android,
  76. options.ci,
  77. false,
  78. options.skip_targets_install,
  79. )?,
  80. Commands::Open => open::command()?,
  81. Commands::Dev(options) => dev::command(options, noise_level)?,
  82. Commands::Build(options) => build::command(options, noise_level)?,
  83. Commands::AndroidStudioScript(options) => android_studio_script::command(options)?,
  84. }
  85. Ok(())
  86. }
  87. pub fn get_config(
  88. app: &App,
  89. config: &TauriConfig,
  90. features: Option<&Vec<String>>,
  91. cli_options: &CliOptions,
  92. ) -> (AndroidConfig, AndroidMetadata) {
  93. let mut android_options = cli_options.clone();
  94. if let Some(features) = features {
  95. android_options
  96. .features
  97. .get_or_insert(Vec::new())
  98. .extend_from_slice(features);
  99. }
  100. let raw = RawAndroidConfig {
  101. features: android_options.features.clone(),
  102. logcat_filter_specs: vec![
  103. "RustStdoutStderr".into(),
  104. format!(
  105. "*:{}",
  106. match cli_options.noise_level {
  107. NoiseLevel::Polite => FilterLevel::Info,
  108. NoiseLevel::LoudAndProud => FilterLevel::Debug,
  109. NoiseLevel::FranklyQuitePedantic => FilterLevel::Verbose,
  110. }
  111. .logcat()
  112. ),
  113. ],
  114. min_sdk_version: Some(config.bundle.android.min_sdk_version),
  115. ..Default::default()
  116. };
  117. let config = AndroidConfig::from_raw(app.clone(), Some(raw)).unwrap();
  118. let metadata = AndroidMetadata {
  119. supported: true,
  120. cargo_args: Some(android_options.args),
  121. features: android_options.features,
  122. ..Default::default()
  123. };
  124. set_var(
  125. "WRY_ANDROID_PACKAGE",
  126. format!("{}.{}", app.reverse_domain(), app.name_snake()),
  127. );
  128. set_var("WRY_ANDROID_LIBRARY", app.lib_name());
  129. set_var("TAURI_ANDROID_PROJECT_PATH", config.project_dir());
  130. let src_main_dir = config.project_dir().join("app/src/main").join(format!(
  131. "java/{}/{}",
  132. app.reverse_domain().replace('.', "/"),
  133. app.name_snake()
  134. ));
  135. if config.project_dir().exists() {
  136. if src_main_dir.exists() {
  137. let _ = create_dir(src_main_dir.join("generated"));
  138. } else {
  139. log::error!(
  140. "Project directory {} does not exist. Did you update the package name in `Cargo.toml` or the bundle identifier in `tauri.conf.json > identifier`? Save your changes, delete the `gen/android` folder and run `tauri android init` to recreate the Android project.",
  141. src_main_dir.display()
  142. );
  143. exit(1);
  144. }
  145. }
  146. set_var(
  147. "WRY_ANDROID_KOTLIN_FILES_OUT_DIR",
  148. src_main_dir.join("generated"),
  149. );
  150. (config, metadata)
  151. }
  152. fn env() -> Result<Env> {
  153. let env = super::env()?;
  154. cargo_mobile2::android::env::Env::from_env(env).map_err(Into::into)
  155. }
  156. fn delete_codegen_vars() {
  157. for (k, _) in std::env::vars() {
  158. if k.starts_with("WRY_") && (k.ends_with("CLASS_EXTENSION") || k.ends_with("CLASS_INIT")) {
  159. std::env::remove_var(k);
  160. }
  161. }
  162. }
  163. fn adb_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
  164. let device_list = adb::device_list(env)
  165. .map_err(|cause| anyhow::anyhow!("Failed to detect connected Android devices: {cause}"))?;
  166. if !device_list.is_empty() {
  167. let device = if let Some(t) = target {
  168. let (device, score) = device_list
  169. .into_iter()
  170. .rev()
  171. .map(|d| {
  172. let score = best_match(t, d.name()).map_or(0, |m| m.score());
  173. (d, score)
  174. })
  175. .max_by_key(|(_, score)| *score)
  176. // we already checked the list is not empty
  177. .unwrap();
  178. if score > MIN_DEVICE_MATCH_SCORE {
  179. device
  180. } else {
  181. anyhow::bail!("Could not find an Android device matching {t}")
  182. }
  183. } else if device_list.len() > 1 {
  184. let index = prompt::list(
  185. concat!("Detected ", "Android", " devices"),
  186. device_list.iter(),
  187. "device",
  188. None,
  189. "Device",
  190. )
  191. .map_err(|cause| anyhow::anyhow!("Failed to prompt for Android device: {cause}"))?;
  192. device_list.into_iter().nth(index).unwrap()
  193. } else {
  194. device_list.into_iter().next().unwrap()
  195. };
  196. log::info!(
  197. "Detected connected device: {} with target {:?}",
  198. device,
  199. device.target().triple,
  200. );
  201. Ok(device)
  202. } else {
  203. Err(anyhow::anyhow!("No connected Android devices detected"))
  204. }
  205. }
  206. fn emulator_prompt(env: &'_ Env, target: Option<&str>) -> Result<emulator::Emulator> {
  207. let emulator_list = emulator::avd_list(env).unwrap_or_default();
  208. if !emulator_list.is_empty() {
  209. let emulator = if let Some(t) = target {
  210. let (device, score) = emulator_list
  211. .into_iter()
  212. .rev()
  213. .map(|d| {
  214. let score = best_match(t, d.name()).map_or(0, |m| m.score());
  215. (d, score)
  216. })
  217. .max_by_key(|(_, score)| *score)
  218. // we already checked the list is not empty
  219. .unwrap();
  220. if score > MIN_DEVICE_MATCH_SCORE {
  221. device
  222. } else {
  223. anyhow::bail!("Could not find an Android Emulator matching {t}")
  224. }
  225. } else if emulator_list.len() > 1 {
  226. let index = prompt::list(
  227. concat!("Detected ", "Android", " emulators"),
  228. emulator_list.iter(),
  229. "emulator",
  230. None,
  231. "Emulator",
  232. )
  233. .map_err(|cause| anyhow::anyhow!("Failed to prompt for Android Emulator device: {cause}"))?;
  234. emulator_list.into_iter().nth(index).unwrap()
  235. } else {
  236. emulator_list.into_iter().next().unwrap()
  237. };
  238. Ok(emulator)
  239. } else {
  240. Err(anyhow::anyhow!("No available Android Emulator detected"))
  241. }
  242. }
  243. fn device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
  244. if let Ok(device) = adb_device_prompt(env, target) {
  245. Ok(device)
  246. } else {
  247. let emulator = emulator_prompt(env, target)?;
  248. log::info!("Starting emulator {}", emulator.name());
  249. emulator.start_detached(env)?;
  250. let mut tries = 0;
  251. loop {
  252. sleep(Duration::from_secs(2));
  253. if let Ok(device) = adb_device_prompt(env, Some(emulator.name())) {
  254. return Ok(device);
  255. }
  256. if tries >= 3 {
  257. log::info!("Waiting for emulator to start... (maybe the emulator is unathorized or offline, run `adb devices` to check)");
  258. } else {
  259. log::info!("Waiting for emulator to start...");
  260. }
  261. tries += 1;
  262. }
  263. }
  264. }
  265. fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> {
  266. device_prompt(env, None).map(|device| device.target()).ok()
  267. }
  268. fn open_and_wait(config: &AndroidConfig, env: &Env) -> ! {
  269. log::info!("Opening Android Studio");
  270. if let Err(e) = os::open_file_with("Android Studio", config.project_dir(), &env.base) {
  271. log::error!("{}", e);
  272. }
  273. loop {
  274. sleep(Duration::from_secs(24 * 60 * 60));
  275. }
  276. }
  277. fn inject_assets(config: &AndroidConfig, tauri_config: &TauriConfig) -> Result<()> {
  278. let asset_dir = config
  279. .project_dir()
  280. .join("app/src/main")
  281. .join(DEFAULT_ASSET_DIR);
  282. create_dir_all(&asset_dir)?;
  283. write(
  284. asset_dir.join("tauri.conf.json"),
  285. serde_json::to_string(&tauri_config)?,
  286. )?;
  287. Ok(())
  288. }