1#![doc(
8 html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
9 html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
10)]
11#![cfg(any(target_os = "macos", target_os = "linux", windows))]
12
13mod acl;
14mod add;
15mod build;
16mod bundle;
17mod completions;
18mod dev;
19mod error;
20mod helpers;
21mod icon;
22mod info;
23mod init;
24mod inspect;
25mod interface;
26mod migrate;
27mod mobile;
28mod plugin;
29mod remove;
30mod runtime;
31mod signer;
32
33use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
34use env_logger::Builder;
35use env_logger::fmt::style::{AnsiColor, Style};
36pub use error::{Error, ErrorExt, Result};
37use log::Level;
38use serde::{Deserialize, Serialize};
39use std::io::{BufReader, Write};
40use std::process::{Command, ExitStatus, Output, Stdio, exit};
41use std::{
42 ffi::OsString, fmt::Display, fs::read_to_string, io::BufRead, path::PathBuf, str::FromStr,
43};
44
45use crate::error::Context;
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ConfigValue(pub(crate) serde_json::Value);
50
51impl FromStr for ConfigValue {
52 type Err = Error;
53
54 fn from_str(config: &str) -> std::result::Result<Self, Self::Err> {
55 if config.starts_with('{') {
56 Ok(Self(serde_json::from_str(config).with_context(|| {
57 format!("failed to parse config `{config}` as JSON")
58 })?))
59 } else {
60 let path = PathBuf::from(config);
61 let raw =
62 read_to_string(&path).fs_context("failed to read configuration file", path.clone())?;
63
64 match path.extension().and_then(|ext| ext.to_str()) {
65 Some("toml") => Ok(Self(::toml::from_str(&raw).with_context(|| {
66 format!("failed to parse config at {} as TOML", path.display())
67 })?)),
68 Some("json5") => Ok(Self(::json5::from_str(&raw).with_context(|| {
69 format!("failed to parse config at {} as JSON5", path.display())
70 })?)),
71 _ => Ok(Self(
73 match ::json5::from_str(&raw) {
77 Ok(json5) => json5,
78 Err(_) => serde_json::from_str(&raw)
79 .with_context(|| format!("failed to parse config at {} as JSON", path.display()))?,
80 },
81 )),
82 }
83 }
84 }
85}
86
87#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
88pub enum RunMode {
89 Desktop,
90 #[cfg(target_os = "macos")]
91 Ios,
92 Android,
93}
94
95impl Display for RunMode {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 write!(
98 f,
99 "{}",
100 match self {
101 Self::Desktop => "desktop",
102 #[cfg(target_os = "macos")]
103 Self::Ios => "iOS",
104 Self::Android => "android",
105 }
106 )
107 }
108}
109
110#[derive(Deserialize)]
111pub struct VersionMetadata {
112 tauri: String,
113 #[serde(rename = "tauri-build")]
114 tauri_build: String,
115 #[serde(rename = "tauri-plugin")]
116 tauri_plugin: String,
117 #[serde(rename = "tauri-runtime-wry")]
118 tauri_runtime_wry: String,
119}
120
121#[derive(Deserialize)]
122pub struct PackageJson {
123 name: Option<String>,
124 version: Option<String>,
125 product_name: Option<String>,
126}
127
128#[derive(Parser)]
129#[clap(
130 author,
131 version,
132 about,
133 bin_name("cargo-tauri"),
134 subcommand_required(true),
135 arg_required_else_help(true),
136 propagate_version(true),
137 no_binary_name(true)
138)]
139pub(crate) struct Cli {
140 #[clap(short, long, global = true, action = ArgAction::Count)]
142 verbose: u8,
143 #[clap(subcommand)]
144 command: Commands,
145}
146
147#[derive(Subcommand)]
148enum Commands {
149 Init(init::Options),
150 Dev(dev::Options),
151 Build(build::Options),
152 Bundle(bundle::Options),
153 Android(mobile::android::Cli),
154 #[cfg(target_os = "macos")]
155 Ios(mobile::ios::Cli),
156 Migrate,
158 Info(info::Options),
159 Add(add::Options),
160 Remove(remove::Options),
161 Plugin(plugin::Cli),
162 Icon(icon::Options),
163 Signer(signer::Cli),
164 Completions(completions::Options),
165 Permission(acl::permission::Cli),
166 Capability(acl::capability::Cli),
167 Inspect(inspect::Cli),
168}
169
170fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
171 let mut app = I::command();
172 err.format(&mut app)
173}
174
175fn get_verbosity(cli_verbose: u8) -> u8 {
176 std::env::var("TAURI_CLI_VERBOSITY")
177 .ok()
178 .and_then(|v| v.parse().ok())
179 .unwrap_or(cli_verbose)
180}
181
182pub fn run<I, A>(args: I, bin_name: Option<String>)
195where
196 I: IntoIterator<Item = A>,
197 A: Into<OsString> + Clone,
198{
199 if let Err(e) = try_run(args, bin_name) {
200 log::error!("{e}");
201 exit(1);
202 }
203}
204
205pub fn try_run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
209where
210 I: IntoIterator<Item = A>,
211 A: Into<OsString> + Clone,
212{
213 let cli = match bin_name {
214 Some(bin_name) => Cli::command().bin_name(bin_name),
215 None => Cli::command(),
216 };
217 let cli_ = cli.clone();
218 let matches = cli.get_matches_from(args);
219
220 let res = Cli::from_arg_matches(&matches).map_err(format_error::<Cli>);
221 let cli = match res {
222 Ok(s) => s,
223 Err(e) => e.exit(),
224 };
225 let verbosity_number = get_verbosity(cli.verbose);
227 unsafe { std::env::set_var("TAURI_CLI_VERBOSITY", verbosity_number.to_string()) };
228
229 let mut builder = Builder::from_default_env();
230 if let Err(err) = builder
231 .format_indent(Some(12))
232 .filter(None, verbosity_level(verbosity_number).to_level_filter())
233 .filter(
236 Some("goblin"),
237 verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
238 )
239 .filter(
241 Some("handlebars"),
242 verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
243 )
244 .filter(
246 Some("ureq_proto"),
247 verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
248 )
249 .format(|f, record| {
250 let mut is_command_output = false;
251 if let Some(action) = record.key_values().get("action".into()) {
252 let action = action.to_cow_str().unwrap();
253 is_command_output = action == "stdout" || action == "stderr";
254 if !is_command_output {
255 let style = Style::new().fg_color(Some(AnsiColor::Green.into())).bold();
256 write!(f, "{style}{action:>12}{style:#} ")?;
257 }
258 } else {
259 let style = f.default_level_style(record.level()).bold();
260 write!(
261 f,
262 "{style}{:>12}{style:#} ",
263 prettyprint_level(record.level())
264 )?;
265 }
266
267 if !is_command_output && log::log_enabled!(Level::Debug) {
268 let style = Style::new().fg_color(Some(AnsiColor::Black.into()));
269 write!(f, "[{style}{}{style:#}] ", record.target())?;
270 }
271
272 writeln!(f, "{}", record.args())
273 })
274 .try_init()
275 {
276 eprintln!("Failed to attach logger: {err}");
277 }
278
279 match cli.command {
280 Commands::Build(options) => build::command(options, cli.verbose)?,
281 Commands::Bundle(options) => bundle::command(options, cli.verbose)?,
282 Commands::Dev(options) => dev::command(options)?,
283 Commands::Add(options) => add::command(options)?,
284 Commands::Remove(options) => remove::command(options)?,
285 Commands::Icon(options) => icon::command(options)?,
286 Commands::Info(options) => info::command(options)?,
287 Commands::Init(options) => init::command(options)?,
288 Commands::Plugin(cli) => plugin::command(cli)?,
289 Commands::Signer(cli) => signer::command(cli)?,
290 Commands::Completions(options) => completions::command(options, cli_)?,
291 Commands::Permission(options) => acl::permission::command(options)?,
292 Commands::Capability(options) => acl::capability::command(options)?,
293 Commands::Android(c) => mobile::android::command(c, cli.verbose)?,
294 #[cfg(target_os = "macos")]
295 Commands::Ios(c) => mobile::ios::command(c, cli.verbose)?,
296 Commands::Migrate => migrate::command()?,
297 Commands::Inspect(cli) => inspect::command(cli)?,
298 }
299
300 Ok(())
301}
302
303fn verbosity_level(num: u8) -> Level {
305 match num {
306 0 => Level::Info,
307 1 => Level::Debug,
308 _ => Level::Trace,
309 }
310}
311
312fn prettyprint_level(lvl: Level) -> &'static str {
314 match lvl {
315 Level::Error => "Error",
316 Level::Warn => "Warn",
317 Level::Info => "Info",
318 Level::Debug => "Debug",
319 Level::Trace => "Trace",
320 }
321}
322
323pub trait CommandExt {
324 fn piped(&mut self) -> std::io::Result<ExitStatus>;
327 fn output_ok(&mut self) -> crate::Result<Output>;
328}
329
330impl CommandExt for Command {
331 fn piped(&mut self) -> std::io::Result<ExitStatus> {
332 self.stdin(os_pipe::dup_stdin()?);
333 self.stdout(os_pipe::dup_stdout()?);
334 self.stderr(os_pipe::dup_stderr()?);
335
336 let program = self.get_program().to_string_lossy().into_owned();
337 let args = self
338 .get_args()
339 .map(|a| a.to_string_lossy())
340 .collect::<Vec<_>>()
341 .join(" ");
342
343 log::debug!(action = "Running"; "Command `{program} {args}`");
344 self.status()
345 }
346
347 fn output_ok(&mut self) -> crate::Result<Output> {
348 let program = self.get_program().to_string_lossy().into_owned();
349 let args = self
350 .get_args()
351 .map(|a| a.to_string_lossy())
352 .collect::<Vec<_>>()
353 .join(" ");
354 let cmdline = format!("{program} {args}");
355 log::debug!(action = "Running"; "Command `{cmdline}`");
356
357 self.stdout(Stdio::piped());
358 self.stderr(Stdio::piped());
359
360 let mut child = self
361 .spawn()
362 .with_context(|| format!("failed to run command `{cmdline}`"))?;
363
364 let mut stdout = child.stdout.take().map(BufReader::new).unwrap();
365 let stdout_thread = std::thread::spawn(move || {
366 let mut line = String::new();
367 let mut lines = Vec::new();
368 loop {
369 line.clear();
370 match stdout.read_line(&mut line) {
371 Ok(0) => break,
372 Ok(_) => {
373 log::debug!(action = "stdout"; "{}", line.trim_end());
374 lines.extend(line.as_bytes());
375 }
376 Err(_) => (),
377 }
378 }
379 lines
380 });
381
382 let mut stderr = child.stderr.take().map(BufReader::new).unwrap();
383 let stderr_thread = std::thread::spawn(move || {
384 let mut line = String::new();
385 let mut lines = Vec::new();
386 loop {
387 line.clear();
388 match stderr.read_line(&mut line) {
389 Ok(0) => break,
390 Ok(_) => {
391 log::debug!(action = "stderr"; "{}", line.trim_end());
392 lines.extend(line.as_bytes());
393 }
394 Err(_) => (),
395 }
396 }
397 lines
398 });
399
400 let status = child
401 .wait()
402 .with_context(|| format!("failed to run command `{cmdline}`"))?;
403
404 let output = Output {
406 status,
407 stdout: stdout_thread.join().unwrap_or_default(),
408 stderr: stderr_thread.join().unwrap_or_default(),
409 };
410
411 if output.status.success() {
412 Ok(output)
413 } else {
414 Err(crate::error::Error::CommandFailed {
415 command: cmdline,
416 error: std::io::Error::other(String::from_utf8_lossy(&output.stderr).to_string()),
417 })
418 }
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use clap::CommandFactory;
425
426 use crate::Cli;
427
428 #[test]
429 fn verify_cli() {
430 Cli::command().debug_assert();
431 }
432
433 #[test]
434 fn help_output_includes_build() {
435 let help = Cli::command().render_help().to_string();
436 assert!(help.contains("Build"));
437 }
438
439 #[cfg(unix)]
440 #[test]
441 fn output_ok_captures_all_output() {
442 use crate::CommandExt;
443
444 for _ in 0..20 {
445 let output = std::process::Command::new("sh")
446 .args(["-c", "echo out1; echo err1 >&2; echo out2; echo err2 >&2"])
447 .output_ok()
448 .unwrap();
449 assert_eq!(output.stdout, b"out1\nout2\n");
450 assert_eq!(output.stderr, b"err1\nerr2\n");
451 }
452 }
453}