Skip to main content

tauri_cli/
lib.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! This Rust executable provides the full interface to all of the required activities for which the CLI is required. It will run on macOS, Windows, and Linux.
6
7#![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,
43  fmt::Display,
44  fs::read_to_string,
45  io::BufRead,
46  path::PathBuf,
47  str::FromStr,
48  sync::{Arc, Mutex},
49};
50
51use crate::error::Context;
52
53/// Tauri configuration argument option.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ConfigValue(pub(crate) serde_json::Value);
56
57impl FromStr for ConfigValue {
58  type Err = Error;
59
60  fn from_str(config: &str) -> std::result::Result<Self, Self::Err> {
61    if config.starts_with('{') {
62      Ok(Self(serde_json::from_str(config).with_context(|| {
63        format!("failed to parse config `{config}` as JSON")
64      })?))
65    } else {
66      let path = PathBuf::from(config);
67      let raw =
68        read_to_string(&path).fs_context("failed to read configuration file", path.clone())?;
69
70      match path.extension().and_then(|ext| ext.to_str()) {
71        Some("toml") => Ok(Self(::toml::from_str(&raw).with_context(|| {
72          format!("failed to parse config at {} as TOML", path.display())
73        })?)),
74        Some("json5") => Ok(Self(::json5::from_str(&raw).with_context(|| {
75          format!("failed to parse config at {} as JSON5", path.display())
76        })?)),
77        // treat all other extensions as json
78        _ => Ok(Self(
79          // from tauri-utils/src/config/parse.rs:
80          // we also want to support **valid** json5 in the .json extension
81          // if the json5 is not valid the serde_json error for regular json will be returned.
82          match ::json5::from_str(&raw) {
83            Ok(json5) => json5,
84            Err(_) => serde_json::from_str(&raw)
85              .with_context(|| format!("failed to parse config at {} as JSON", path.display()))?,
86          },
87        )),
88      }
89    }
90  }
91}
92
93#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
94pub enum RunMode {
95  Desktop,
96  #[cfg(target_os = "macos")]
97  Ios,
98  Android,
99}
100
101impl Display for RunMode {
102  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103    write!(
104      f,
105      "{}",
106      match self {
107        Self::Desktop => "desktop",
108        #[cfg(target_os = "macos")]
109        Self::Ios => "iOS",
110        Self::Android => "android",
111      }
112    )
113  }
114}
115
116#[derive(Deserialize)]
117pub struct VersionMetadata {
118  tauri: String,
119  #[serde(rename = "tauri-build")]
120  tauri_build: String,
121  #[serde(rename = "tauri-plugin")]
122  tauri_plugin: String,
123  #[serde(rename = "tauri-runtime-wry")]
124  tauri_runtime_wry: String,
125}
126
127#[derive(Deserialize)]
128pub struct PackageJson {
129  name: Option<String>,
130  version: Option<String>,
131  product_name: Option<String>,
132}
133
134#[derive(Parser)]
135#[clap(
136  author,
137  version,
138  about,
139  bin_name("cargo-tauri"),
140  subcommand_required(true),
141  arg_required_else_help(true),
142  propagate_version(true),
143  no_binary_name(true)
144)]
145pub(crate) struct Cli {
146  /// Enables verbose logging
147  #[clap(short, long, global = true, action = ArgAction::Count)]
148  verbose: u8,
149  #[clap(subcommand)]
150  command: Commands,
151}
152
153#[derive(Subcommand)]
154enum Commands {
155  Init(init::Options),
156  Dev(dev::Options),
157  Build(build::Options),
158  Bundle(bundle::Options),
159  Android(mobile::android::Cli),
160  #[cfg(target_os = "macos")]
161  Ios(mobile::ios::Cli),
162  /// Migrate from v1 to v2
163  Migrate,
164  Info(info::Options),
165  Add(add::Options),
166  Remove(remove::Options),
167  Plugin(plugin::Cli),
168  Icon(icon::Options),
169  Signer(signer::Cli),
170  Completions(completions::Options),
171  Permission(acl::permission::Cli),
172  Capability(acl::capability::Cli),
173  Inspect(inspect::Cli),
174}
175
176fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
177  let mut app = I::command();
178  err.format(&mut app)
179}
180
181fn get_verbosity(cli_verbose: u8) -> u8 {
182  std::env::var("TAURI_CLI_VERBOSITY")
183    .ok()
184    .and_then(|v| v.parse().ok())
185    .unwrap_or(cli_verbose)
186}
187
188/// Run the Tauri CLI with the passed arguments, exiting if an error occurs.
189///
190/// The passed arguments should have the binary argument(s) stripped out before being passed.
191///
192/// e.g.
193/// 1. `tauri-cli 1 2 3` -> `1 2 3`
194/// 2. `cargo tauri 1 2 3` -> `1 2 3`
195/// 3. `node tauri.js 1 2 3` -> `1 2 3`
196///
197/// The passed `bin_name` parameter should be how you want the help messages to display the command.
198/// This defaults to `cargo-tauri`, but should be set to how the program was called, such as
199/// `cargo tauri`.
200pub fn run<I, A>(args: I, bin_name: Option<String>)
201where
202  I: IntoIterator<Item = A>,
203  A: Into<OsString> + Clone,
204{
205  if let Err(e) = try_run(args, bin_name) {
206    log::error!("{e}");
207    exit(1);
208  }
209}
210
211/// Run the Tauri CLI with the passed arguments.
212///
213/// It is similar to [`run`], but instead of exiting on an error, it returns a result.
214pub fn try_run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
215where
216  I: IntoIterator<Item = A>,
217  A: Into<OsString> + Clone,
218{
219  let cli = match bin_name {
220    Some(bin_name) => Cli::command().bin_name(bin_name),
221    None => Cli::command(),
222  };
223  let cli_ = cli.clone();
224  let matches = cli.get_matches_from(args);
225
226  let res = Cli::from_arg_matches(&matches).map_err(format_error::<Cli>);
227  let cli = match res {
228    Ok(s) => s,
229    Err(e) => e.exit(),
230  };
231  // set the verbosity level so subsequent CLI calls (xcode-script, android-studio-script) refer to it
232  let verbosity_number = get_verbosity(cli.verbose);
233  unsafe { std::env::set_var("TAURI_CLI_VERBOSITY", verbosity_number.to_string()) };
234
235  let mut builder = Builder::from_default_env();
236  if let Err(err) = builder
237    .format_indent(Some(12))
238    .filter(None, verbosity_level(verbosity_number).to_level_filter())
239    // goblin spams an insane amount of really technical logs on the debug level so we're reducing one level.
240    // goblin was removed in 2.12 but is still used in apple-codesign so we keep this just in case.
241    .filter(
242      Some("goblin"),
243      verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
244    )
245    // handlebars is not that spammy but its debug logs are typically far from being helpful
246    .filter(
247      Some("handlebars"),
248      verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
249    )
250    // `ureq_proto` logs out every network packets at trace level
251    .filter(
252      Some("ureq_proto"),
253      verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
254    )
255    .format(|f, record| {
256      let mut is_command_output = false;
257      if let Some(action) = record.key_values().get("action".into()) {
258        let action = action.to_cow_str().unwrap();
259        is_command_output = action == "stdout" || action == "stderr";
260        if !is_command_output {
261          let style = Style::new().fg_color(Some(AnsiColor::Green.into())).bold();
262          write!(f, "{style}{action:>12}{style:#} ")?;
263        }
264      } else {
265        let style = f.default_level_style(record.level()).bold();
266        write!(
267          f,
268          "{style}{:>12}{style:#} ",
269          prettyprint_level(record.level())
270        )?;
271      }
272
273      if !is_command_output && log::log_enabled!(Level::Debug) {
274        let style = Style::new().fg_color(Some(AnsiColor::Black.into()));
275        write!(f, "[{style}{}{style:#}] ", record.target())?;
276      }
277
278      writeln!(f, "{}", record.args())
279    })
280    .try_init()
281  {
282    eprintln!("Failed to attach logger: {err}");
283  }
284
285  match cli.command {
286    Commands::Build(options) => build::command(options, cli.verbose)?,
287    Commands::Bundle(options) => bundle::command(options, cli.verbose)?,
288    Commands::Dev(options) => dev::command(options)?,
289    Commands::Add(options) => add::command(options)?,
290    Commands::Remove(options) => remove::command(options)?,
291    Commands::Icon(options) => icon::command(options)?,
292    Commands::Info(options) => info::command(options)?,
293    Commands::Init(options) => init::command(options)?,
294    Commands::Plugin(cli) => plugin::command(cli)?,
295    Commands::Signer(cli) => signer::command(cli)?,
296    Commands::Completions(options) => completions::command(options, cli_)?,
297    Commands::Permission(options) => acl::permission::command(options)?,
298    Commands::Capability(options) => acl::capability::command(options)?,
299    Commands::Android(c) => mobile::android::command(c, cli.verbose)?,
300    #[cfg(target_os = "macos")]
301    Commands::Ios(c) => mobile::ios::command(c, cli.verbose)?,
302    Commands::Migrate => migrate::command()?,
303    Commands::Inspect(cli) => inspect::command(cli)?,
304  }
305
306  Ok(())
307}
308
309/// This maps the occurrence of `--verbose` flags to the correct log level
310fn verbosity_level(num: u8) -> Level {
311  match num {
312    0 => Level::Info,
313    1 => Level::Debug,
314    _ => Level::Trace,
315  }
316}
317
318/// The default string representation for `Level` is all uppercaps which doesn't mix well with the other printed actions.
319fn prettyprint_level(lvl: Level) -> &'static str {
320  match lvl {
321    Level::Error => "Error",
322    Level::Warn => "Warn",
323    Level::Info => "Info",
324    Level::Debug => "Debug",
325    Level::Trace => "Trace",
326  }
327}
328
329pub trait CommandExt {
330  // The `pipe` function sets the stdout and stderr to properly
331  // show the command output in the Node.js wrapper.
332  fn piped(&mut self) -> std::io::Result<ExitStatus>;
333  fn output_ok(&mut self) -> crate::Result<Output>;
334}
335
336impl CommandExt for Command {
337  fn piped(&mut self) -> std::io::Result<ExitStatus> {
338    self.stdin(os_pipe::dup_stdin()?);
339    self.stdout(os_pipe::dup_stdout()?);
340    self.stderr(os_pipe::dup_stderr()?);
341
342    let program = self.get_program().to_string_lossy().into_owned();
343    let args = self
344      .get_args()
345      .map(|a| a.to_string_lossy())
346      .collect::<Vec<_>>()
347      .join(" ");
348
349    log::debug!(action = "Running"; "Command `{program} {args}`");
350    self.status()
351  }
352
353  fn output_ok(&mut self) -> crate::Result<Output> {
354    let program = self.get_program().to_string_lossy().into_owned();
355    let args = self
356      .get_args()
357      .map(|a| a.to_string_lossy())
358      .collect::<Vec<_>>()
359      .join(" ");
360    let cmdline = format!("{program} {args}");
361    log::debug!(action = "Running"; "Command `{cmdline}`");
362
363    self.stdout(Stdio::piped());
364    self.stderr(Stdio::piped());
365
366    let mut child = self
367      .spawn()
368      .with_context(|| format!("failed to run command `{cmdline}`"))?;
369
370    let mut stdout = child.stdout.take().map(BufReader::new).unwrap();
371    let stdout_lines = Arc::new(Mutex::new(Vec::new()));
372    let stdout_lines_ = stdout_lines.clone();
373    std::thread::spawn(move || {
374      let mut line = String::new();
375      if let Ok(mut lines) = stdout_lines_.lock() {
376        loop {
377          line.clear();
378          match stdout.read_line(&mut line) {
379            Ok(0) => break,
380            Ok(_) => {
381              log::debug!(action = "stdout"; "{}", line.trim_end());
382              lines.extend(line.as_bytes());
383            }
384            Err(_) => (),
385          }
386        }
387      }
388    });
389
390    let mut stderr = child.stderr.take().map(BufReader::new).unwrap();
391    let stderr_lines = Arc::new(Mutex::new(Vec::new()));
392    let stderr_lines_ = stderr_lines.clone();
393    std::thread::spawn(move || {
394      let mut line = String::new();
395      if let Ok(mut lines) = stderr_lines_.lock() {
396        loop {
397          line.clear();
398          match stderr.read_line(&mut line) {
399            Ok(0) => break,
400            Ok(_) => {
401              log::debug!(action = "stderr"; "{}", line.trim_end());
402              lines.extend(line.as_bytes());
403            }
404            Err(_) => (),
405          }
406        }
407      }
408    });
409
410    let status = child
411      .wait()
412      .with_context(|| format!("failed to run command `{cmdline}`"))?;
413
414    let output = Output {
415      status,
416      stdout: std::mem::take(&mut *stdout_lines.lock().unwrap()),
417      stderr: std::mem::take(&mut *stderr_lines.lock().unwrap()),
418    };
419
420    if output.status.success() {
421      Ok(output)
422    } else {
423      Err(crate::error::Error::CommandFailed {
424        command: cmdline,
425        error: std::io::Error::other(String::from_utf8_lossy(&output.stderr).to_string()),
426      })
427    }
428  }
429}
430
431#[cfg(test)]
432mod tests {
433  use clap::CommandFactory;
434
435  use crate::Cli;
436
437  #[test]
438  fn verify_cli() {
439    Cli::command().debug_assert();
440  }
441
442  #[test]
443  fn help_output_includes_build() {
444    let help = Cli::command().render_help().to_string();
445    assert!(help.contains("Build"));
446  }
447}