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
13use anyhow::Context;
14pub use anyhow::Result;
15
16mod acl;
17mod add;
18mod build;
19mod bundle;
20mod completions;
21mod dev;
22mod helpers;
23mod icon;
24mod info;
25mod init;
26mod inspect;
27mod interface;
28mod migrate;
29mod mobile;
30mod plugin;
31mod remove;
32mod signer;
33
34use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
35use env_logger::fmt::style::{AnsiColor, Style};
36use env_logger::Builder;
37use log::Level;
38use serde::{Deserialize, Serialize};
39use std::io::{BufReader, Write};
40use std::process::{exit, Command, ExitStatus, Output, Stdio};
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
51/// Tauri configuration argument option.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ConfigValue(pub(crate) serde_json::Value);
54
55impl FromStr for ConfigValue {
56  type Err = anyhow::Error;
57
58  fn from_str(config: &str) -> std::result::Result<Self, Self::Err> {
59    if config.starts_with('{') {
60      Ok(Self(
61        serde_json::from_str(config).context("invalid configuration JSON")?,
62      ))
63    } else {
64      let path = PathBuf::from(config);
65      if path.exists() {
66        let raw = &read_to_string(&path)
67          .with_context(|| format!("invalid configuration at file {config}"))?;
68        match path.extension() {
69          Some(ext) if ext == "toml" => Ok(Self(::toml::from_str(raw)?)),
70          Some(ext) if ext == "json5" => Ok(Self(::json5::from_str(raw)?)),
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            },
80          )),
81        }
82      } else {
83        anyhow::bail!("provided configuration path does not exist")
84      }
85    }
86  }
87}
88
89#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
90pub enum RunMode {
91  Desktop,
92  #[cfg(target_os = "macos")]
93  Ios,
94  Android,
95}
96
97impl Display for RunMode {
98  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99    write!(
100      f,
101      "{}",
102      match self {
103        Self::Desktop => "desktop",
104        #[cfg(target_os = "macos")]
105        Self::Ios => "iOS",
106        Self::Android => "android",
107      }
108    )
109  }
110}
111
112#[derive(Deserialize)]
113pub struct VersionMetadata {
114  tauri: String,
115  #[serde(rename = "tauri-build")]
116  tauri_build: String,
117  #[serde(rename = "tauri-plugin")]
118  tauri_plugin: 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
175/// Run the Tauri CLI with the passed arguments, exiting if an error occurs.
176///
177/// The passed arguments should have the binary argument(s) stripped out before being passed.
178///
179/// e.g.
180/// 1. `tauri-cli 1 2 3` -> `1 2 3`
181/// 2. `cargo tauri 1 2 3` -> `1 2 3`
182/// 3. `node tauri.js 1 2 3` -> `1 2 3`
183///
184/// The passed `bin_name` parameter should be how you want the help messages to display the command.
185/// This defaults to `cargo-tauri`, but should be set to how the program was called, such as
186/// `cargo tauri`.
187pub fn run<I, A>(args: I, bin_name: Option<String>)
188where
189  I: IntoIterator<Item = A>,
190  A: Into<OsString> + Clone,
191{
192  if let Err(e) = try_run(args, bin_name) {
193    let mut message = e.to_string();
194    if e.chain().count() > 1 {
195      message.push(':');
196    }
197    e.chain().skip(1).for_each(|cause| {
198      let m = cause.to_string();
199      if !message.contains(&m) {
200        message.push('\n');
201        message.push_str("    - ");
202        message.push_str(&m);
203      }
204    });
205    log::error!("{message}");
206    exit(1);
207  }
208}
209
210/// Run the Tauri CLI with the passed arguments.
211///
212/// It is similar to [`run`], but instead of exiting on an error, it returns a result.
213pub fn try_run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
214where
215  I: IntoIterator<Item = A>,
216  A: Into<OsString> + Clone,
217{
218  let cli = match bin_name {
219    Some(bin_name) => Cli::command().bin_name(bin_name),
220    None => Cli::command(),
221  };
222  let cli_ = cli.clone();
223  let matches = cli.get_matches_from(args);
224
225  let res = Cli::from_arg_matches(&matches).map_err(format_error::<Cli>);
226  let cli = match res {
227    Ok(s) => s,
228    Err(e) => e.exit(),
229  };
230
231  let verbosity_number = std::env::var("TAURI_CLI_VERBOSITY")
232    .ok()
233    .and_then(|v| v.parse().ok())
234    .unwrap_or(cli.verbose);
235  // set the verbosity level so subsequent CLI calls (xcode-script, android-studio-script) refer to it
236  std::env::set_var("TAURI_CLI_VERBOSITY", verbosity_number.to_string());
237
238  let mut builder = Builder::from_default_env();
239  let init_res = builder
240    .format_indent(Some(12))
241    .filter(None, verbosity_level(verbosity_number).to_level_filter())
242    // golbin spams an insane amount of really technical logs on the debug level so we're reducing one level
243    .filter(
244      Some("goblin"),
245      verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
246    )
247    // handlebars is not that spammy but its debug logs are typically far from being helpful
248    .filter(
249      Some("handlebars"),
250      verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
251    )
252    .format(|f, record| {
253      let mut is_command_output = false;
254      if let Some(action) = record.key_values().get("action".into()) {
255        let action = action.to_cow_str().unwrap();
256        is_command_output = action == "stdout" || action == "stderr";
257        if !is_command_output {
258          let style = Style::new().fg_color(Some(AnsiColor::Green.into())).bold();
259
260          write!(f, "{style}{action:>12}{style:#} ")?;
261        }
262      } else {
263        let style = f.default_level_style(record.level()).bold();
264        write!(
265          f,
266          "{style}{:>12}{style:#} ",
267          prettyprint_level(record.level())
268        )?;
269      }
270
271      if !is_command_output && log::log_enabled!(Level::Debug) {
272        let style = Style::new().fg_color(Some(AnsiColor::Black.into()));
273
274        write!(f, "[{style}{}{style:#}] ", record.target())?;
275      }
276
277      writeln!(f, "{}", record.args())
278    })
279    .try_init();
280
281  if let Err(err) = init_res {
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    2.. => 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    let program = self.get_program().to_string_lossy().into_owned();
342    log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
343
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    log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
350
351    self.stdout(Stdio::piped());
352    self.stderr(Stdio::piped());
353
354    let mut child = self.spawn()?;
355
356    let mut stdout = child.stdout.take().map(BufReader::new).unwrap();
357    let stdout_lines = Arc::new(Mutex::new(Vec::new()));
358    let stdout_lines_ = stdout_lines.clone();
359    std::thread::spawn(move || {
360      let mut line = String::new();
361      let mut lines = stdout_lines_.lock().unwrap();
362      loop {
363        line.clear();
364        match stdout.read_line(&mut line) {
365          Ok(0) => break,
366          Ok(_) => {
367            log::debug!(action = "stdout"; "{}", line.trim_end());
368            lines.extend(line.as_bytes().to_vec());
369          }
370          Err(_) => (),
371        }
372      }
373    });
374
375    let mut stderr = child.stderr.take().map(BufReader::new).unwrap();
376    let stderr_lines = Arc::new(Mutex::new(Vec::new()));
377    let stderr_lines_ = stderr_lines.clone();
378    std::thread::spawn(move || {
379      let mut line = String::new();
380      let mut lines = stderr_lines_.lock().unwrap();
381      loop {
382        line.clear();
383        match stderr.read_line(&mut line) {
384          Ok(0) => break,
385          Ok(_) => {
386            log::debug!(action = "stderr"; "{}", line.trim_end());
387            lines.extend(line.as_bytes().to_vec());
388          }
389          Err(_) => (),
390        }
391      }
392    });
393
394    let status = child.wait()?;
395
396    let output = Output {
397      status,
398      stdout: std::mem::take(&mut *stdout_lines.lock().unwrap()),
399      stderr: std::mem::take(&mut *stderr_lines.lock().unwrap()),
400    };
401
402    if output.status.success() {
403      Ok(output)
404    } else {
405      Err(anyhow::anyhow!("failed to run {}", program))
406    }
407  }
408}
409
410#[cfg(test)]
411mod tests {
412  use clap::CommandFactory;
413
414  use crate::Cli;
415
416  #[test]
417  fn verify_cli() {
418    Cli::command().debug_assert();
419  }
420}