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