소스 검색

feat(cli): validate target argument (#4458)

Lucas Fernandes Nogueira 3 년 전
부모
커밋
13b8a2403d

+ 6 - 0
.changes/cli-check-target.md

@@ -0,0 +1,6 @@
+---
+"cli.rs": patch
+"cli.js": patch
+---
+
+Check if target exists and is installed on dev and build commands.

+ 1 - 1
tooling/cli/node/index.d.ts

@@ -3,4 +3,4 @@
 
 /* auto-generated by NAPI-RS */
 
-export function run(args: Array<string>, binName: string | undefined | null, callback: (...args: any[]) => any): void
+export function run(args: Array<string>, binName?: string | undefined | null): void

+ 2 - 18
tooling/cli/node/src/lib.rs

@@ -2,23 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use napi::{
-  threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode},
-  Error, JsFunction, Result, Status,
-};
-
 #[napi_derive::napi]
-pub fn run(args: Vec<String>, bin_name: Option<String>, callback: JsFunction) -> Result<()> {
-  let function: ThreadsafeFunction<bool, ErrorStrategy::CalleeHandled> = callback
-    .create_threadsafe_function(0, |ctx| ctx.env.get_boolean(ctx.value).map(|v| vec![v]))?;
-
-  std::thread::spawn(move || match tauri_cli::run(args, bin_name) {
-    Ok(_) => function.call(Ok(true), ThreadsafeFunctionCallMode::Blocking),
-    Err(e) => function.call(
-      Err(Error::new(Status::GenericFailure, format!("{:#}", e))),
-      ThreadsafeFunctionCallMode::Blocking,
-    ),
-  });
-
-  Ok(())
+pub fn run(args: Vec<String>, bin_name: Option<String>) {
+  tauri_cli::run(args, bin_name);
 }

+ 1 - 4
tooling/cli/node/tauri.js

@@ -43,7 +43,4 @@ if (binStem === 'node' || binStem === 'nodejs') {
   arguments.unshift(bin)
 }
 
-cli.run(arguments, binName).catch((err) => {
-  console.log(`Error running CLI: ${err.message}`)
-  process.exit(1)
-})
+cli.run(arguments, binName)

+ 2 - 9
tooling/cli/node/test/jest/__tests__/template.spec.js

@@ -23,11 +23,7 @@ describe('[CLI] cli.js template', () => {
       await move(outPath, cacheOutPath)
     }
 
-    await cli.run(['init', '--directory', process.cwd(), '--force', '--tauri-path', resolve(currentDirName, '../../../../../..'), '--ci'])
-      .catch(err => {
-        console.error(err)
-        throw err
-      })
+    cli.run(['init', '--directory', process.cwd(), '--force', '--tauri-path', resolve(currentDirName, '../../../../../..'), '--ci'])
 
     if (outExists) {
       await move(cacheOutPath, outPath)
@@ -43,10 +39,7 @@ describe('[CLI] cli.js template', () => {
     const config = readFileSync(configPath).toString()
     writeFileSync(configPath, config.replace('com.tauri.dev', 'com.tauri.test'))
 
-    await cli.run(['build', '--verbose']).catch(err => {
-      console.error(err)
-      throw err
-    })
+    cli.run(['build', '--verbose'])
     process.chdir(cwd)
   })
 })

+ 4 - 4
tooling/cli/src/build.rs

@@ -160,16 +160,16 @@ pub fn command(mut options: Options) -> Result<()> {
     list.extend(config_.build.features.clone().unwrap_or_default());
   }
 
-  let interface = AppInterface::new(config_)?;
+  let mut interface = AppInterface::new(config_)?;
   let app_settings = interface.app_settings();
   let interface_options = options.clone().into();
 
   let bin_path = app_settings.app_binary_path(&interface_options)?;
   let out_dir = bin_path.parent().unwrap();
 
-  interface
-    .build(interface_options)
-    .with_context(|| "failed to build app")?;
+  interface.build(interface_options)?;
+
+  let app_settings = interface.app_settings();
 
   if config_.tauri.bundle.active {
     let package_types = if let Some(names) = &options.bundles {

+ 2 - 2
tooling/cli/src/dev.rs

@@ -252,7 +252,7 @@ fn command_internal(mut options: Options) -> Result<()> {
     }
   }
 
-  let interface = AppInterface::new(config.lock().unwrap().as_ref().unwrap())?;
+  let mut interface = AppInterface::new(config.lock().unwrap().as_ref().unwrap())?;
 
   let exit_on_panic = options.exit_on_panic;
   let process = interface.dev(options.clone().into(), &manifest, move |status, reason| {
@@ -334,7 +334,7 @@ fn lookup<F: FnMut(FileType, PathBuf)>(dir: &Path, mut f: F) {
 
 #[allow(clippy::too_many_arguments)]
 fn watch<P: DevProcess, I: Interface<Dev = P>>(
-  interface: I,
+  mut interface: I,
   process: Arc<Mutex<P>>,
   tauri_path: PathBuf,
   merge_config: Option<String>,

+ 2 - 2
tooling/cli/src/interface/mod.rs

@@ -85,9 +85,9 @@ pub trait Interface: Sized {
 
   fn new(config: &Config) -> crate::Result<Self>;
   fn app_settings(&self) -> &Self::AppSettings;
-  fn build(&self, options: Options) -> crate::Result<()>;
+  fn build(&mut self, options: Options) -> crate::Result<()>;
   fn dev<F: FnOnce(ExitStatus, ExitReason) + Send + 'static>(
-    &self,
+    &mut self,
     options: Options,
     manifest: &Manifest,
     on_exit: F,

+ 60 - 3
tooling/cli/src/interface/rust.rs

@@ -94,10 +94,17 @@ impl DevProcess for DevChild {
   }
 }
 
+#[derive(Debug)]
+struct Target {
+  name: String,
+  installed: bool,
+}
+
 pub struct Rust {
   app_settings: RustAppSettings,
   config_features: Vec<String>,
   product_name: Option<String>,
+  available_targets: Option<Vec<Target>>,
 }
 
 impl Interface for Rust {
@@ -109,6 +116,7 @@ impl Interface for Rust {
       app_settings: RustAppSettings::new(config)?,
       config_features: config.build.features.clone().unwrap_or_default(),
       product_name: config.package.product_name.clone(),
+      available_targets: None,
     })
   }
 
@@ -116,7 +124,7 @@ impl Interface for Rust {
     &self.app_settings
   }
 
-  fn build(&self, options: Options) -> crate::Result<()> {
+  fn build(&mut self, options: Options) -> crate::Result<()> {
     let bin_path = self.app_settings.app_binary_path(&options)?;
     let out_dir = bin_path.parent().unwrap();
 
@@ -165,7 +173,7 @@ impl Interface for Rust {
   }
 
   fn dev<F: FnOnce(ExitStatus, ExitReason) + Send + 'static>(
-    &self,
+    &mut self,
     options: Options,
     manifest: &Manifest,
     on_exit: F,
@@ -174,6 +182,12 @@ impl Interface for Rust {
     let product_name = self.product_name.clone();
 
     let runner = options.runner.unwrap_or_else(|| "cargo".into());
+
+    if let Some(target) = &options.target {
+      self.fetch_available_targets();
+      self.validate_target(target)?;
+    }
+
     let mut build_cmd = Command::new(&runner);
     build_cmd
       .env(
@@ -345,9 +359,52 @@ impl Interface for Rust {
 }
 
 impl Rust {
-  fn build_app(&self, options: Options) -> crate::Result<()> {
+  fn fetch_available_targets(&mut self) {
+    if let Ok(output) = Command::new("rustup").args(["target", "list"]).output() {
+      let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
+      self.available_targets.replace(
+        stdout
+          .split('\n')
+          .map(|t| {
+            let mut s = t.split(' ');
+            let name = s.next().unwrap().to_string();
+            let installed = s.next().map(|v| v == "(installed)").unwrap_or_default();
+            Target { name, installed }
+          })
+          .filter(|t| !t.name.is_empty())
+          .collect(),
+      );
+    }
+  }
+
+  fn validate_target(&self, target: &str) -> crate::Result<()> {
+    if let Some(available_targets) = &self.available_targets {
+      if let Some(target) = available_targets.iter().find(|t| t.name == target) {
+        if !target.installed {
+          anyhow::bail!(
+            "Target {target} is not installed (installed targets: {installed}). Please run `rustup target add {target}`.",
+            target = target.name,
+            installed = available_targets.iter().filter(|t| t.installed).map(|t| t.name.as_str()).collect::<Vec<&str>>().join(", ")
+          );
+        }
+      }
+      if !available_targets.iter().any(|t| t.name == target) {
+        anyhow::bail!("Target {target} does not exist. Please run `rustup target list` to see the available targets.", target = target);
+      }
+    }
+    Ok(())
+  }
+
+  fn build_app(&mut self, options: Options) -> crate::Result<()> {
     let runner = options.runner.unwrap_or_else(|| "cargo".into());
 
+    if let Some(target) = &options.target {
+      if self.available_targets.is_none() {
+        self.fetch_available_targets();
+      }
+      self.validate_target(target)?;
+    }
+
     let mut args = Vec::new();
     if !options.args.is_empty() {
       args.extend(options.args);

+ 13 - 2
tooling/cli/src/lib.rs

@@ -19,7 +19,7 @@ use env_logger::Builder;
 use log::{debug, log_enabled, Level};
 use serde::Deserialize;
 use std::io::{BufReader, Write};
-use std::process::{Command, ExitStatus, Output, Stdio};
+use std::process::{exit, Command, ExitStatus, Output, Stdio};
 use std::{
   ffi::OsString,
   sync::{Arc, Mutex},
@@ -85,7 +85,18 @@ fn format_error<I: IntoApp>(err: clap::Error) -> clap::Error {
 /// The passed `bin_name` parameter should be how you want the help messages to display the command.
 /// This defaults to `cargo-tauri`, but should be set to how the program was called, such as
 /// `cargo tauri`.
-pub fn run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
+pub fn run<I, A>(args: I, bin_name: Option<String>)
+where
+  I: IntoIterator<Item = A>,
+  A: Into<OsString> + Clone,
+{
+  if let Err(e) = try_run(args, bin_name) {
+    log::error!("{:#}", e);
+    exit(1);
+  }
+}
+
+fn try_run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
 where
   I: IntoIterator<Item = A>,
   A: Into<OsString> + Clone,

+ 2 - 3
tooling/cli/src/main.rs

@@ -2,13 +2,12 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use anyhow::Context;
 use std::env::args_os;
 use std::ffi::OsStr;
 use std::path::Path;
 use std::process::exit;
 
-fn main() -> tauri_cli::Result<()> {
+fn main() {
   let mut args = args_os().peekable();
   let bin_name = match args
     .next()
@@ -33,5 +32,5 @@ fn main() -> tauri_cli::Result<()> {
     }
   };
 
-  tauri_cli::run(args, bin_name).context("Try running with --verbose to see command output")
+  tauri_cli::run(args, bin_name)
 }