Просмотр исходного кода

feat(cli): improve device/simulator prompt logic (#5114)

Lucas Fernandes Nogueira 2 лет назад
Родитель
Сommit
8f3a9c5cf6

+ 8 - 1
tooling/cli/Cargo.lock

@@ -293,7 +293,7 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
 [[package]]
 name = "cargo-mobile"
 version = "0.1.0"
-source = "git+https://github.com/tauri-apps/cargo-mobile?branch=dev#4a28c9e2370c07b4cb83458e58da0d7bfe4e9b1e"
+source = "git+https://github.com/tauri-apps/cargo-mobile?branch=dev#123b8cf58e9a894375bb9d9e7a098419a7d67b5f"
 dependencies = [
  "cocoa",
  "colored 1.9.3",
@@ -3410,6 +3410,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "sublime_fuzzy"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7986063f7c0ab374407e586d7048a3d5aac94f103f751088bf398e07cd5400"
+
 [[package]]
 name = "subtle"
 version = "2.4.1"
@@ -3528,6 +3534,7 @@ dependencies = [
  "serde_json",
  "serde_with 2.0.0",
  "shared_child",
+ "sublime_fuzzy",
  "tauri-bundler",
  "tauri-utils",
  "tempfile",

+ 1 - 0
tooling/cli/Cargo.toml

@@ -33,6 +33,7 @@ cargo-mobile = { git = "https://github.com/tauri-apps/cargo-mobile", branch = "d
 textwrap = { version = "0.11.0", features = ["term_size"] }
 interprocess = "1"
 thiserror = "1"
+sublime_fuzzy = "0.7"
 clap = { version = "3.2", features = [ "derive" ] }
 anyhow = "1.0"
 tauri-bundler = { version = "1.0.5", path = "../bundler" }

+ 68 - 60
tooling/cli/src/mobile/android.rs

@@ -12,7 +12,6 @@ use cargo_mobile::{
     target::Target,
   },
   config::app::App,
-  device::PromptError,
   opts::NoiseLevel,
   os,
   util::prompt,
@@ -23,11 +22,12 @@ use std::{
   thread::{sleep, spawn},
   time::Duration,
 };
+use sublime_fuzzy::best_match;
 
 use super::{
   ensure_init, get_app,
   init::{command as init_command, init_dot_cargo, Options as InitOptions},
-  log_finished, read_options, CliOptions, Target as MobileTarget,
+  log_finished, read_options, CliOptions, Target as MobileTarget, MIN_DEVICE_MATCH_SCORE,
 };
 use crate::{
   helpers::config::{get as get_tauri_config, Config as TauriConfig},
@@ -141,34 +141,39 @@ fn delete_codegen_vars() {
   }
 }
 
-fn adb_device_prompt<'a>(
-  env: &'_ Env,
-  target: Option<&str>,
-) -> Result<Device<'a>, PromptError<adb::device_list::Error>> {
-  let device_list =
-    adb::device_list(env).map_err(|cause| PromptError::detection_failed("Android", cause))?;
+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 index = if device_list.len() > 1 {
-      if let Some(t) = target {
-        let t = t.to_lowercase();
-        device_list
-          .iter()
-          .position(|d| d.name().to_lowercase().starts_with(&t))
-          .unwrap_or_default()
+    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 {
-        prompt::list(
-          concat!("Detected ", "Android", " devices"),
-          device_list.iter(),
-          "device",
-          None,
-          "Device",
-        )
-        .map_err(|cause| PromptError::prompt_failed("Android", cause))?
+        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 {
-      0
+      device_list.into_iter().next().unwrap()
     };
-    let device = device_list.into_iter().nth(index).unwrap();
     println!(
       "Detected connected device: {} with target {:?}",
       device,
@@ -176,57 +181,60 @@ fn adb_device_prompt<'a>(
     );
     Ok(device)
   } else {
-    Err(PromptError::none_detected("Android"))
+    Err(anyhow::anyhow!("No connected Android devices detected"))
   }
 }
 
-fn emulator_prompt(
-  env: &'_ Env,
-  target: Option<&str>,
-) -> Result<emulator::Emulator, PromptError<adb::device_list::Error>> {
+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() {
-    Err(PromptError::none_detected("Android emulator"))
-  } else {
-    let index = if emulator_list.len() > 1 {
-      if let Some(t) = target {
-        let t = t.to_lowercase();
-        emulator_list
-          .iter()
-          .position(|d| d.name().to_lowercase().starts_with(&t))
-          .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 {
-        prompt::list(
-          concat!("Detected ", "Android", " emulators"),
-          emulator_list.iter(),
-          "emulator",
-          None,
-          "Emulator",
-        )
-        .map_err(|cause| PromptError::prompt_failed("Android emulator", cause))?
+        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 {
-      0
+      emulator_list.into_iter().next().unwrap()
     };
 
-    Ok(emulator_list.into_iter().nth(index).unwrap())
+    let handle = emulator.start(env)?;
+    spawn(move || {
+      let _ = handle.wait();
+    });
+
+    Ok(emulator)
+  } else {
+    Err(anyhow::anyhow!("No available Android Emulator detected"))
   }
 }
 
-fn device_prompt<'a>(
-  env: &'_ Env,
-  target: Option<&str>,
-) -> Result<Device<'a>, PromptError<adb::device_list::Error>> {
+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)?;
-    let handle = emulator.start(env).map_err(|e| {
-      PromptError::prompt_failed(
-        "Android emulator",
-        std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
-      )
-    })?;
+    let handle = emulator.start(env)?;
     spawn(move || {
       let _ = handle.wait();
     });

+ 8 - 34
tooling/cli/src/mobile/android/dev.rs

@@ -1,6 +1,6 @@
 use super::{
   delete_codegen_vars, device_prompt, ensure_init, env, init_dot_cargo, open_and_wait, with_config,
-  MobileTarget, PromptError,
+  MobileTarget,
 };
 use crate::{
   helpers::{config::get as get_tauri_config, flock},
@@ -12,21 +12,14 @@ use clap::Parser;
 
 use cargo_mobile::{
   android::{
-    adb,
     config::{Config as AndroidConfig, Metadata as AndroidMetadata},
-    device::RunError as DeviceRunError,
-    emulator,
     env::Env,
   },
   config::app::App,
   opts::{NoiseLevel, Profile},
 };
 
-use std::{
-  env::set_var,
-  thread::{sleep, spawn},
-  time::Duration,
-};
+use std::env::set_var;
 
 const WEBVIEW_CLIENT_CLASS_EXTENSION: &str = "
     @android.annotation.SuppressLint(\"WebViewClientOnReceivedSslError\")
@@ -119,25 +112,6 @@ fn run_dev(
   let env = env()?;
   init_dot_cargo(app, Some((&env, config)))?;
 
-  if let Some(device) = &options.device {
-    let emulators = emulator::avd_list(&env).unwrap_or_default();
-    for emulator in emulators {
-      if emulator
-        .name()
-        .to_lowercase()
-        .starts_with(&device.to_lowercase())
-      {
-        log::info!("Starting emulator {}", emulator.name());
-        let handle = emulator.start(&env)?;
-        spawn(move || {
-          let _ = handle.wait();
-        });
-        sleep(Duration::from_secs(3));
-        break;
-      }
-    }
-  }
-
   let open = options.open;
   let exit_on_panic = options.exit_on_panic;
   let no_watch = options.no_watch;
@@ -189,10 +163,10 @@ fn run_dev(
 
 #[derive(Debug, thiserror::Error)]
 enum RunError {
-  #[error(transparent)]
-  FailedToPromptForDevice(PromptError<adb::device_list::Error>),
-  #[error(transparent)]
-  RunFailed(DeviceRunError),
+  #[error("{0}")]
+  FailedToPromptForDevice(String),
+  #[error("{0}")]
+  RunFailed(String),
 }
 
 fn run(
@@ -212,7 +186,7 @@ fn run(
   let build_app_bundle = metadata.asset_packs().is_some();
 
   device_prompt(env, device)
-    .map_err(RunError::FailedToPromptForDevice)?
+    .map_err(|e| RunError::FailedToPromptForDevice(e.to_string()))?
     .run(
       config,
       env,
@@ -224,5 +198,5 @@ fn run(
       ".MainActivity".into(),
     )
     .map(DevChild::new)
-    .map_err(RunError::RunFailed)
+    .map_err(|e| RunError::RunFailed(e.to_string()))
 }

+ 65 - 45
tooling/cli/src/mobile/ios.rs

@@ -13,18 +13,18 @@ use cargo_mobile::{
     target::Target,
   },
   config::app::App,
-  device::PromptError,
   env::Env,
   opts::NoiseLevel,
   os,
   util::prompt,
 };
 use clap::{Parser, Subcommand};
+use sublime_fuzzy::best_match;
 
 use super::{
   ensure_init, env, get_app,
   init::{command as init_command, init_dot_cargo, Options as InitOptions},
-  log_finished, read_options, CliOptions, Target as MobileTarget,
+  log_finished, read_options, CliOptions, Target as MobileTarget, MIN_DEVICE_MATCH_SCORE,
 };
 use crate::{
   helpers::config::{get as get_tauri_config, Config as TauriConfig},
@@ -126,21 +126,28 @@ fn with_config<T>(
   f(&app, &config, &metadata, cli_options)
 }
 
-fn ios_deploy_device_prompt<'a>(
-  env: &'_ Env,
-  target: Option<&str>,
-) -> Result<Device<'a>, PromptError<ios_deploy::DeviceListError>> {
-  let device_list =
-    ios_deploy::device_list(env).map_err(|cause| PromptError::detection_failed("iOS", cause))?;
+fn ios_deploy_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
+  let device_list = ios_deploy::device_list(env)
+    .map_err(|cause| anyhow::anyhow!("Failed to detect connected iOS devices: {cause}"))?;
   if !device_list.is_empty() {
-    let index = if device_list.len() > 1 {
-      if let Some(t) = target {
-        let t = t.to_lowercase();
-        device_list
-          .iter()
-          .position(|d| d.name().to_lowercase().starts_with(&t))
-          .unwrap_or_default()
+    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 iOS device matching {t}")
+      }
+    } else {
+      let index = if device_list.len() > 1 {
         prompt::list(
           concat!("Detected ", "iOS", " devices"),
           device_list.iter(),
@@ -148,12 +155,12 @@ fn ios_deploy_device_prompt<'a>(
           None,
           "Device",
         )
-        .map_err(|cause| PromptError::prompt_failed("iOS", cause))?
-      }
-    } else {
-      0
+        .map_err(|cause| anyhow::anyhow!("Failed to prompt for iOS device: {cause}"))?
+      } else {
+        0
+      };
+      device_list.into_iter().nth(index).unwrap()
     };
-    let device = device_list.into_iter().nth(index).unwrap();
     println!(
       "Detected connected device: {} with target {:?}",
       device,
@@ -161,41 +168,54 @@ fn ios_deploy_device_prompt<'a>(
     );
     Ok(device)
   } else {
-    Err(PromptError::none_detected("iOS"))
+    Err(anyhow::anyhow!("No connected iOS devices detected"))
   }
 }
 
-fn simulator_prompt(
-  env: &'_ Env,
-  target: Option<&str>,
-) -> Result<simctl::Device, PromptError<simctl::DeviceListError>> {
-  let simulator_list = simctl::device_list(env)
-    .map_err(|cause| PromptError::detection_failed("iOS Simulator", cause))?;
+fn simulator_prompt(env: &'_ Env, target: Option<&str>) -> Result<simctl::Device> {
+  let simulator_list = simctl::device_list(env).map_err(|cause| {
+    anyhow::anyhow!("Failed to detect connected iOS Simulator devices: {cause}")
+  })?;
   if !simulator_list.is_empty() {
-    let index = if simulator_list.len() > 1 {
-      if let Some(t) = target {
-        let t = t.to_lowercase();
-        simulator_list
-          .iter()
-          .position(|d| d.name().to_lowercase().starts_with(&t))
-          .unwrap_or_default()
+    let device = if let Some(t) = target {
+      let (device, score) = simulator_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 {
-        prompt::list(
-          concat!("Detected ", "iOS", " simulators"),
-          simulator_list.iter(),
-          "simulator",
-          None,
-          "Simulator",
-        )
-        .map_err(|cause| PromptError::prompt_failed("iOS Simulator", cause))?
+        anyhow::bail!("Could not find an iOS Simulator matching {t}")
       }
+    } else if simulator_list.len() > 1 {
+      let index = prompt::list(
+        concat!("Detected ", "iOS", " simulators"),
+        simulator_list.iter(),
+        "simulator",
+        None,
+        "Simulator",
+      )
+      .map_err(|cause| anyhow::anyhow!("Failed to prompt for iOS Simulator device: {cause}"))?;
+      simulator_list.into_iter().nth(index).unwrap()
     } else {
-      0
+      simulator_list.into_iter().next().unwrap()
     };
-    let device = simulator_list.into_iter().nth(index).unwrap();
+
+    log::info!("Starting simulator {}", device.name());
+    let handle = device.start(env)?;
+    spawn(move || {
+      let _ = handle.wait();
+    });
+
     Ok(device)
   } else {
-    Err(PromptError::none_detected("iOS Simulator"))
+    Err(anyhow::anyhow!("No available iOS Simulator detected"))
   }
 }
 

+ 4 - 28
tooling/cli/src/mobile/ios/dev.rs

@@ -10,17 +10,12 @@ use crate::{
 use clap::Parser;
 
 use cargo_mobile::{
-  apple::{config::Config as AppleConfig, device::RunError as DeviceRunError, simctl},
+  apple::config::Config as AppleConfig,
   config::app::App,
   env::Env,
   opts::{NoiseLevel, Profile},
 };
 
-use std::{
-  thread::{sleep, spawn},
-  time::Duration,
-};
-
 #[derive(Debug, Clone, Parser)]
 #[clap(about = "iOS dev")]
 pub struct Options {
@@ -98,25 +93,6 @@ fn run_dev(
   let env = env()?;
   init_dot_cargo(app, None)?;
 
-  if let Some(device) = &options.device {
-    let simulators = simctl::device_list(&env).unwrap_or_default();
-    for simulator in simulators {
-      if simulator
-        .name()
-        .to_lowercase()
-        .starts_with(&device.to_lowercase())
-      {
-        log::info!("Starting simulator {}", simulator.name());
-        let handle = simulator.start(&env)?;
-        spawn(move || {
-          let _ = handle.wait();
-        });
-        sleep(Duration::from_secs(3));
-        break;
-      }
-    }
-  }
-
   let open = options.open;
   let exit_on_panic = options.exit_on_panic;
   let no_watch = options.no_watch;
@@ -163,8 +139,8 @@ fn run_dev(
 enum RunError {
   #[error("{0}")]
   FailedToPromptForDevice(String),
-  #[error(transparent)]
-  RunFailed(DeviceRunError),
+  #[error("{0}")]
+  RunFailed(String),
 }
 fn run(
   device: Option<&str>,
@@ -185,5 +161,5 @@ fn run(
     .map_err(|e| RunError::FailedToPromptForDevice(e.to_string()))?
     .run(config, env, noise_level, non_interactive, profile)
     .map(DevChild::new)
-    .map_err(RunError::RunFailed)
+    .map_err(|e| RunError::RunFailed(e.to_string()))
 }

+ 2 - 0
tooling/cli/src/mobile/mod.rs

@@ -40,6 +40,8 @@ mod init;
 #[cfg(target_os = "macos")]
 pub mod ios;
 
+const MIN_DEVICE_MATCH_SCORE: isize = 0;
+
 #[derive(Clone)]
 pub struct DevChild {
   child: Arc<SharedChild>,