tauri_cli/
lib.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5pub use anyhow::Result;
6
7mod build;
8mod completions;
9mod dev;
10mod helpers;
11mod icon;
12mod info;
13mod init;
14mod interface;
15mod plugin;
16mod signer;
17
18use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand};
19use env_logger::fmt::Color;
20use env_logger::Builder;
21use log::{debug, log_enabled, Level};
22use serde::Deserialize;
23use std::io::{BufReader, Write};
24use std::process::{exit, Command, ExitStatus, Output, Stdio};
25use std::{
26  ffi::OsString,
27  io::BufRead,
28  sync::{Arc, Mutex},
29};
30
31#[derive(Deserialize)]
32pub struct VersionMetadata {
33  tauri: String,
34  #[serde(rename = "tauri-build")]
35  tauri_build: String,
36}
37
38#[derive(Deserialize)]
39pub struct PackageJson {
40  name: Option<String>,
41  version: Option<String>,
42  product_name: Option<String>,
43}
44
45#[derive(Parser)]
46#[clap(
47  author,
48  version,
49  about,
50  bin_name("cargo-tauri"),
51  subcommand_required(true),
52  arg_required_else_help(true),
53  propagate_version(true),
54  no_binary_name(true)
55)]
56pub(crate) struct Cli {
57  /// Enables verbose logging
58  #[clap(short, long, global = true, action = ArgAction::Count)]
59  verbose: u8,
60  #[clap(subcommand)]
61  command: Commands,
62}
63
64#[derive(Subcommand)]
65enum Commands {
66  Build(build::Options),
67  Dev(dev::Options),
68  Icon(icon::Options),
69  Info(info::Options),
70  Init(init::Options),
71  Plugin(plugin::Cli),
72  Signer(signer::Cli),
73  Completions(completions::Options),
74}
75
76fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
77  let mut app = I::command();
78  err.format(&mut app)
79}
80
81/// Run the Tauri CLI with the passed arguments, exiting if an error occurs.
82///
83/// The passed arguments should have the binary argument(s) stripped out before being passed.
84///
85/// e.g.
86/// 1. `tauri-cli 1 2 3` -> `1 2 3`
87/// 2. `cargo tauri 1 2 3` -> `1 2 3`
88/// 3. `node tauri.js 1 2 3` -> `1 2 3`
89///
90/// The passed `bin_name` parameter should be how you want the help messages to display the command.
91/// This defaults to `cargo-tauri`, but should be set to how the program was called, such as
92/// `cargo tauri`.
93pub fn run<I, A>(args: I, bin_name: Option<String>)
94where
95  I: IntoIterator<Item = A>,
96  A: Into<OsString> + Clone,
97{
98  if let Err(e) = try_run(args, bin_name) {
99    log::error!("{:#}", e);
100    exit(1);
101  }
102}
103
104/// Run the Tauri CLI with the passed arguments.
105///
106/// It is similar to [`run`], but instead of exiting on an error, it returns a result.
107pub fn try_run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
108where
109  I: IntoIterator<Item = A>,
110  A: Into<OsString> + Clone,
111{
112  let cli = match bin_name {
113    Some(bin_name) => Cli::command().bin_name(bin_name),
114    None => Cli::command(),
115  };
116  let cli_ = cli.clone();
117  let matches = cli.get_matches_from(args);
118
119  let res = Cli::from_arg_matches(&matches).map_err(format_error::<Cli>);
120  let cli = match res {
121    Ok(s) => s,
122    Err(e) => e.exit(),
123  };
124
125  let mut builder = Builder::from_default_env();
126  let init_res = builder
127    .format_indent(Some(12))
128    .filter(None, verbosity_level(cli.verbose).to_level_filter())
129    .format(|f, record| {
130      let mut is_command_output = false;
131      if let Some(action) = record.key_values().get("action".into()) {
132        let action = action.to_cow_str().unwrap();
133        is_command_output = action == "stdout" || action == "stderr";
134        if !is_command_output {
135          let mut action_style = f.style();
136          action_style.set_color(Color::Green).set_bold(true);
137
138          write!(f, "{:>12} ", action_style.value(action))?;
139        }
140      } else {
141        let mut level_style = f.default_level_style(record.level());
142        level_style.set_bold(true);
143
144        write!(
145          f,
146          "{:>12} ",
147          level_style.value(prettyprint_level(record.level()))
148        )?;
149      }
150
151      if !is_command_output && log_enabled!(Level::Debug) {
152        let mut target_style = f.style();
153        target_style.set_color(Color::Black);
154
155        write!(f, "[{}] ", target_style.value(record.target()))?;
156      }
157
158      writeln!(f, "{}", record.args())
159    })
160    .try_init();
161
162  if let Err(err) = init_res {
163    eprintln!("Failed to attach logger: {err}");
164  }
165
166  match cli.command {
167    Commands::Build(options) => build::command(options, cli.verbose)?,
168    Commands::Dev(options) => dev::command(options)?,
169    Commands::Icon(options) => icon::command(options)?,
170    Commands::Info(options) => info::command(options)?,
171    Commands::Init(options) => init::command(options)?,
172    Commands::Plugin(cli) => plugin::command(cli)?,
173    Commands::Signer(cli) => signer::command(cli)?,
174    Commands::Completions(options) => completions::command(options, cli_)?,
175  }
176
177  Ok(())
178}
179
180/// This maps the occurrence of `--verbose` flags to the correct log level
181fn verbosity_level(num: u8) -> Level {
182  match num {
183    0 => Level::Info,
184    1 => Level::Debug,
185    2.. => Level::Trace,
186  }
187}
188
189/// The default string representation for `Level` is all uppercaps which doesn't mix well with the other printed actions.
190fn prettyprint_level(lvl: Level) -> &'static str {
191  match lvl {
192    Level::Error => "Error",
193    Level::Warn => "Warn",
194    Level::Info => "Info",
195    Level::Debug => "Debug",
196    Level::Trace => "Trace",
197  }
198}
199
200pub trait CommandExt {
201  // The `pipe` function sets the stdout and stderr to properly
202  // show the command output in the Node.js wrapper.
203  fn piped(&mut self) -> std::io::Result<ExitStatus>;
204  fn output_ok(&mut self) -> crate::Result<Output>;
205}
206
207impl CommandExt for Command {
208  fn piped(&mut self) -> std::io::Result<ExitStatus> {
209    self.stdout(os_pipe::dup_stdout()?);
210    self.stderr(os_pipe::dup_stderr()?);
211    let program = self.get_program().to_string_lossy().into_owned();
212    debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
213
214    self.status()
215  }
216
217  fn output_ok(&mut self) -> crate::Result<Output> {
218    let program = self.get_program().to_string_lossy().into_owned();
219    debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
220
221    self.stdout(Stdio::piped());
222    self.stderr(Stdio::piped());
223
224    let mut child = self.spawn()?;
225
226    let mut stdout = child.stdout.take().map(BufReader::new).unwrap();
227    let stdout_lines = Arc::new(Mutex::new(Vec::new()));
228    let stdout_lines_ = stdout_lines.clone();
229    std::thread::spawn(move || {
230      let mut line = String::new();
231      let mut lines = stdout_lines_.lock().unwrap();
232      loop {
233        line.clear();
234        match stdout.read_line(&mut line) {
235          Ok(0) => break,
236          Ok(_) => {
237            debug!(action = "stdout"; "{}", line.trim_end());
238            lines.extend(line.as_bytes().to_vec());
239          }
240          Err(_) => (),
241        }
242      }
243    });
244
245    let mut stderr = child.stderr.take().map(BufReader::new).unwrap();
246    let stderr_lines = Arc::new(Mutex::new(Vec::new()));
247    let stderr_lines_ = stderr_lines.clone();
248    std::thread::spawn(move || {
249      let mut line = String::new();
250      let mut lines = stderr_lines_.lock().unwrap();
251      loop {
252        line.clear();
253        match stderr.read_line(&mut line) {
254          Ok(0) => break,
255          Ok(_) => {
256            debug!(action = "stderr"; "{}", line.trim_end());
257            lines.extend(line.as_bytes().to_vec());
258          }
259          Err(_) => (),
260        }
261      }
262    });
263
264    let status = child.wait()?;
265
266    let output = Output {
267      status,
268      stdout: std::mem::take(&mut *stdout_lines.lock().unwrap()),
269      stderr: std::mem::take(&mut *stderr_lines.lock().unwrap()),
270    };
271
272    if output.status.success() {
273      Ok(output)
274    } else {
275      Err(anyhow::anyhow!("failed to run {}", program))
276    }
277  }
278}