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, fmt::Display, fs::read_to_string, io::BufRead, path::PathBuf, str::FromStr,
43};
44
45use crate::error::Context;
46
47/// Tauri configuration argument option.
48#[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        // treat all other extensions as json
72        _ => Ok(Self(
73          // from tauri-utils/src/config/parse.rs:
74          // we also want to support **valid** json5 in the .json extension
75          // if the json5 is not valid the serde_json error for regular json will be returned.
76          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  /// Enables verbose logging
141  #[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 from v1 to v2
157  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
182/// Run the Tauri CLI with the passed arguments, exiting if an error occurs.
183///
184/// The passed arguments should have the binary argument(s) stripped out before being passed.
185///
186/// e.g.
187/// 1. `tauri-cli 1 2 3` -> `1 2 3`
188/// 2. `cargo tauri 1 2 3` -> `1 2 3`
189/// 3. `node tauri.js 1 2 3` -> `1 2 3`
190///
191/// The passed `bin_name` parameter should be how you want the help messages to display the command.
192/// This defaults to `cargo-tauri`, but should be set to how the program was called, such as
193/// `cargo tauri`.
194pub 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
205/// Run the Tauri CLI with the passed arguments.
206///
207/// It is similar to [`run`], but instead of exiting on an error, it returns a result.
208pub 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  // set the verbosity level so subsequent CLI calls (xcode-script, android-studio-script) refer to it
226  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    // goblin spams an insane amount of really technical logs on the debug level so we're reducing one level.
234    // goblin was removed in 2.12 but is still used in apple-codesign so we keep this just in case.
235    .filter(
236      Some("goblin"),
237      verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
238    )
239    // handlebars is not that spammy but its debug logs are typically far from being helpful
240    .filter(
241      Some("handlebars"),
242      verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
243    )
244    // `ureq_proto` logs out every network packets at trace level
245    .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
303/// This maps the occurrence of `--verbose` flags to the correct log level
304fn verbosity_level(num: u8) -> Level {
305  match num {
306    0 => Level::Info,
307    1 => Level::Debug,
308    _ => Level::Trace,
309  }
310}
311
312/// The default string representation for `Level` is all uppercaps which doesn't mix well with the other printed actions.
313fn 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  // The `pipe` function sets the stdout and stderr to properly
325  // show the command output in the Node.js wrapper.
326  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    // wait for the readers to reach EOF so the output is complete
405    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}