Skip to content

Commit 13b8a24

Browse files
authored
feat(cli): validate target argument (#4458)
1 parent 08a73ac commit 13b8a24

File tree

11 files changed

+95
-48
lines changed

11 files changed

+95
-48
lines changed

.changes/cli-check-target.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"cli.rs": patch
3+
"cli.js": patch
4+
---
5+
6+
Check if target exists and is installed on dev and build commands.

tooling/cli/node/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
/* auto-generated by NAPI-RS */
55

6-
export function run(args: Array<string>, binName: string | undefined | null, callback: (...args: any[]) => any): void
6+
export function run(args: Array<string>, binName?: string | undefined | null): void

tooling/cli/node/src/lib.rs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// SPDX-License-Identifier: MIT
44

5-
use napi::{
6-
threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode},
7-
Error, JsFunction, Result, Status,
8-
};
9-
105
#[napi_derive::napi]
11-
pub fn run(args: Vec<String>, bin_name: Option<String>, callback: JsFunction) -> Result<()> {
12-
let function: ThreadsafeFunction<bool, ErrorStrategy::CalleeHandled> = callback
13-
.create_threadsafe_function(0, |ctx| ctx.env.get_boolean(ctx.value).map(|v| vec![v]))?;
14-
15-
std::thread::spawn(move || match tauri_cli::run(args, bin_name) {
16-
Ok(_) => function.call(Ok(true), ThreadsafeFunctionCallMode::Blocking),
17-
Err(e) => function.call(
18-
Err(Error::new(Status::GenericFailure, format!("{:#}", e))),
19-
ThreadsafeFunctionCallMode::Blocking,
20-
),
21-
});
22-
23-
Ok(())
6+
pub fn run(args: Vec<String>, bin_name: Option<String>) {
7+
tauri_cli::run(args, bin_name);
248
}

tooling/cli/node/tauri.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,4 @@ if (binStem === 'node' || binStem === 'nodejs') {
4343
arguments.unshift(bin)
4444
}
4545

46-
cli.run(arguments, binName).catch((err) => {
47-
console.log(`Error running CLI: ${err.message}`)
48-
process.exit(1)
49-
})
46+
cli.run(arguments, binName)

tooling/cli/node/test/jest/__tests__/template.spec.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,7 @@ describe('[CLI] cli.js template', () => {
2323
await move(outPath, cacheOutPath)
2424
}
2525

26-
await cli.run(['init', '--directory', process.cwd(), '--force', '--tauri-path', resolve(currentDirName, '../../../../../..'), '--ci'])
27-
.catch(err => {
28-
console.error(err)
29-
throw err
30-
})
26+
cli.run(['init', '--directory', process.cwd(), '--force', '--tauri-path', resolve(currentDirName, '../../../../../..'), '--ci'])
3127

3228
if (outExists) {
3329
await move(cacheOutPath, outPath)
@@ -43,10 +39,7 @@ describe('[CLI] cli.js template', () => {
4339
const config = readFileSync(configPath).toString()
4440
writeFileSync(configPath, config.replace('com.tauri.dev', 'com.tauri.test'))
4541

46-
await cli.run(['build', '--verbose']).catch(err => {
47-
console.error(err)
48-
throw err
49-
})
42+
cli.run(['build', '--verbose'])
5043
process.chdir(cwd)
5144
})
5245
})

tooling/cli/src/build.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,16 @@ pub fn command(mut options: Options) -> Result<()> {
160160
list.extend(config_.build.features.clone().unwrap_or_default());
161161
}
162162

163-
let interface = AppInterface::new(config_)?;
163+
let mut interface = AppInterface::new(config_)?;
164164
let app_settings = interface.app_settings();
165165
let interface_options = options.clone().into();
166166

167167
let bin_path = app_settings.app_binary_path(&interface_options)?;
168168
let out_dir = bin_path.parent().unwrap();
169169

170-
interface
171-
.build(interface_options)
172-
.with_context(|| "failed to build app")?;
170+
interface.build(interface_options)?;
171+
172+
let app_settings = interface.app_settings();
173173

174174
if config_.tauri.bundle.active {
175175
let package_types = if let Some(names) = &options.bundles {

tooling/cli/src/dev.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ fn command_internal(mut options: Options) -> Result<()> {
252252
}
253253
}
254254

255-
let interface = AppInterface::new(config.lock().unwrap().as_ref().unwrap())?;
255+
let mut interface = AppInterface::new(config.lock().unwrap().as_ref().unwrap())?;
256256

257257
let exit_on_panic = options.exit_on_panic;
258258
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) {
334334

335335
#[allow(clippy::too_many_arguments)]
336336
fn watch<P: DevProcess, I: Interface<Dev = P>>(
337-
interface: I,
337+
mut interface: I,
338338
process: Arc<Mutex<P>>,
339339
tauri_path: PathBuf,
340340
merge_config: Option<String>,

tooling/cli/src/interface/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ pub trait Interface: Sized {
8585

8686
fn new(config: &Config) -> crate::Result<Self>;
8787
fn app_settings(&self) -> &Self::AppSettings;
88-
fn build(&self, options: Options) -> crate::Result<()>;
88+
fn build(&mut self, options: Options) -> crate::Result<()>;
8989
fn dev<F: FnOnce(ExitStatus, ExitReason) + Send + 'static>(
90-
&self,
90+
&mut self,
9191
options: Options,
9292
manifest: &Manifest,
9393
on_exit: F,

tooling/cli/src/interface/rust.rs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,17 @@ impl DevProcess for DevChild {
9494
}
9595
}
9696

97+
#[derive(Debug)]
98+
struct Target {
99+
name: String,
100+
installed: bool,
101+
}
102+
97103
pub struct Rust {
98104
app_settings: RustAppSettings,
99105
config_features: Vec<String>,
100106
product_name: Option<String>,
107+
available_targets: Option<Vec<Target>>,
101108
}
102109

103110
impl Interface for Rust {
@@ -109,14 +116,15 @@ impl Interface for Rust {
109116
app_settings: RustAppSettings::new(config)?,
110117
config_features: config.build.features.clone().unwrap_or_default(),
111118
product_name: config.package.product_name.clone(),
119+
available_targets: None,
112120
})
113121
}
114122

115123
fn app_settings(&self) -> &Self::AppSettings {
116124
&self.app_settings
117125
}
118126

119-
fn build(&self, options: Options) -> crate::Result<()> {
127+
fn build(&mut self, options: Options) -> crate::Result<()> {
120128
let bin_path = self.app_settings.app_binary_path(&options)?;
121129
let out_dir = bin_path.parent().unwrap();
122130

@@ -165,7 +173,7 @@ impl Interface for Rust {
165173
}
166174

167175
fn dev<F: FnOnce(ExitStatus, ExitReason) + Send + 'static>(
168-
&self,
176+
&mut self,
169177
options: Options,
170178
manifest: &Manifest,
171179
on_exit: F,
@@ -174,6 +182,12 @@ impl Interface for Rust {
174182
let product_name = self.product_name.clone();
175183

176184
let runner = options.runner.unwrap_or_else(|| "cargo".into());
185+
186+
if let Some(target) = &options.target {
187+
self.fetch_available_targets();
188+
self.validate_target(target)?;
189+
}
190+
177191
let mut build_cmd = Command::new(&runner);
178192
build_cmd
179193
.env(
@@ -345,9 +359,52 @@ impl Interface for Rust {
345359
}
346360

347361
impl Rust {
348-
fn build_app(&self, options: Options) -> crate::Result<()> {
362+
fn fetch_available_targets(&mut self) {
363+
if let Ok(output) = Command::new("rustup").args(["target", "list"]).output() {
364+
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
365+
self.available_targets.replace(
366+
stdout
367+
.split('\n')
368+
.map(|t| {
369+
let mut s = t.split(' ');
370+
let name = s.next().unwrap().to_string();
371+
let installed = s.next().map(|v| v == "(installed)").unwrap_or_default();
372+
Target { name, installed }
373+
})
374+
.filter(|t| !t.name.is_empty())
375+
.collect(),
376+
);
377+
}
378+
}
379+
380+
fn validate_target(&self, target: &str) -> crate::Result<()> {
381+
if let Some(available_targets) = &self.available_targets {
382+
if let Some(target) = available_targets.iter().find(|t| t.name == target) {
383+
if !target.installed {
384+
anyhow::bail!(
385+
"Target {target} is not installed (installed targets: {installed}). Please run `rustup target add {target}`.",
386+
target = target.name,
387+
installed = available_targets.iter().filter(|t| t.installed).map(|t| t.name.as_str()).collect::<Vec<&str>>().join(", ")
388+
);
389+
}
390+
}
391+
if !available_targets.iter().any(|t| t.name == target) {
392+
anyhow::bail!("Target {target} does not exist. Please run `rustup target list` to see the available targets.", target = target);
393+
}
394+
}
395+
Ok(())
396+
}
397+
398+
fn build_app(&mut self, options: Options) -> crate::Result<()> {
349399
let runner = options.runner.unwrap_or_else(|| "cargo".into());
350400

401+
if let Some(target) = &options.target {
402+
if self.available_targets.is_none() {
403+
self.fetch_available_targets();
404+
}
405+
self.validate_target(target)?;
406+
}
407+
351408
let mut args = Vec::new();
352409
if !options.args.is_empty() {
353410
args.extend(options.args);

tooling/cli/src/lib.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use env_logger::Builder;
1919
use log::{debug, log_enabled, Level};
2020
use serde::Deserialize;
2121
use std::io::{BufReader, Write};
22-
use std::process::{Command, ExitStatus, Output, Stdio};
22+
use std::process::{exit, Command, ExitStatus, Output, Stdio};
2323
use std::{
2424
ffi::OsString,
2525
sync::{Arc, Mutex},
@@ -85,7 +85,18 @@ fn format_error<I: IntoApp>(err: clap::Error) -> clap::Error {
8585
/// The passed `bin_name` parameter should be how you want the help messages to display the command.
8686
/// This defaults to `cargo-tauri`, but should be set to how the program was called, such as
8787
/// `cargo tauri`.
88-
pub fn run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
88+
pub fn run<I, A>(args: I, bin_name: Option<String>)
89+
where
90+
I: IntoIterator<Item = A>,
91+
A: Into<OsString> + Clone,
92+
{
93+
if let Err(e) = try_run(args, bin_name) {
94+
log::error!("{:#}", e);
95+
exit(1);
96+
}
97+
}
98+
99+
fn try_run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
89100
where
90101
I: IntoIterator<Item = A>,
91102
A: Into<OsString> + Clone,

0 commit comments

Comments
 (0)