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