1#![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,
43 fmt::Display,
44 fs::read_to_string,
45 io::BufRead,
46 path::PathBuf,
47 str::FromStr,
48 sync::{Arc, Mutex},
49};
50
51use crate::error::Context;
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ConfigValue(pub(crate) serde_json::Value);
56
57impl FromStr for ConfigValue {
58 type Err = Error;
59
60 fn from_str(config: &str) -> std::result::Result<Self, Self::Err> {
61 if config.starts_with('{') {
62 Ok(Self(serde_json::from_str(config).with_context(|| {
63 format!("failed to parse config `{config}` as JSON")
64 })?))
65 } else {
66 let path = PathBuf::from(config);
67 let raw =
68 read_to_string(&path).fs_context("failed to read configuration file", path.clone())?;
69
70 match path.extension().and_then(|ext| ext.to_str()) {
71 Some("toml") => Ok(Self(::toml::from_str(&raw).with_context(|| {
72 format!("failed to parse config at {} as TOML", path.display())
73 })?)),
74 Some("json5") => Ok(Self(::json5::from_str(&raw).with_context(|| {
75 format!("failed to parse config at {} as JSON5", path.display())
76 })?)),
77 _ => Ok(Self(
79 match ::json5::from_str(&raw) {
83 Ok(json5) => json5,
84 Err(_) => serde_json::from_str(&raw)
85 .with_context(|| format!("failed to parse config at {} as JSON", path.display()))?,
86 },
87 )),
88 }
89 }
90 }
91}
92
93#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
94pub enum RunMode {
95 Desktop,
96 #[cfg(target_os = "macos")]
97 Ios,
98 Android,
99}
100
101impl Display for RunMode {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 write!(
104 f,
105 "{}",
106 match self {
107 Self::Desktop => "desktop",
108 #[cfg(target_os = "macos")]
109 Self::Ios => "iOS",
110 Self::Android => "android",
111 }
112 )
113 }
114}
115
116#[derive(Deserialize)]
117pub struct VersionMetadata {
118 tauri: String,
119 #[serde(rename = "tauri-build")]
120 tauri_build: String,
121 #[serde(rename = "tauri-plugin")]
122 tauri_plugin: String,
123 #[serde(rename = "tauri-runtime-wry")]
124 tauri_runtime_wry: String,
125}
126
127#[derive(Deserialize)]
128pub struct PackageJson {
129 name: Option<String>,
130 version: Option<String>,
131 product_name: Option<String>,
132}
133
134#[derive(Parser)]
135#[clap(
136 author,
137 version,
138 about,
139 bin_name("cargo-tauri"),
140 subcommand_required(true),
141 arg_required_else_help(true),
142 propagate_version(true),
143 no_binary_name(true)
144)]
145pub(crate) struct Cli {
146 #[clap(short, long, global = true, action = ArgAction::Count)]
148 verbose: u8,
149 #[clap(subcommand)]
150 command: Commands,
151}
152
153#[derive(Subcommand)]
154enum Commands {
155 Init(init::Options),
156 Dev(dev::Options),
157 Build(build::Options),
158 Bundle(bundle::Options),
159 Android(mobile::android::Cli),
160 #[cfg(target_os = "macos")]
161 Ios(mobile::ios::Cli),
162 Migrate,
164 Info(info::Options),
165 Add(add::Options),
166 Remove(remove::Options),
167 Plugin(plugin::Cli),
168 Icon(icon::Options),
169 Signer(signer::Cli),
170 Completions(completions::Options),
171 Permission(acl::permission::Cli),
172 Capability(acl::capability::Cli),
173 Inspect(inspect::Cli),
174}
175
176fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
177 let mut app = I::command();
178 err.format(&mut app)
179}
180
181fn get_verbosity(cli_verbose: u8) -> u8 {
182 std::env::var("TAURI_CLI_VERBOSITY")
183 .ok()
184 .and_then(|v| v.parse().ok())
185 .unwrap_or(cli_verbose)
186}
187
188pub fn run<I, A>(args: I, bin_name: Option<String>)
201where
202 I: IntoIterator<Item = A>,
203 A: Into<OsString> + Clone,
204{
205 if let Err(e) = try_run(args, bin_name) {
206 log::error!("{e}");
207 exit(1);
208 }
209}
210
211pub fn try_run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
215where
216 I: IntoIterator<Item = A>,
217 A: Into<OsString> + Clone,
218{
219 let cli = match bin_name {
220 Some(bin_name) => Cli::command().bin_name(bin_name),
221 None => Cli::command(),
222 };
223 let cli_ = cli.clone();
224 let matches = cli.get_matches_from(args);
225
226 let res = Cli::from_arg_matches(&matches).map_err(format_error::<Cli>);
227 let cli = match res {
228 Ok(s) => s,
229 Err(e) => e.exit(),
230 };
231 let verbosity_number = get_verbosity(cli.verbose);
233 unsafe { std::env::set_var("TAURI_CLI_VERBOSITY", verbosity_number.to_string()) };
234
235 let mut builder = Builder::from_default_env();
236 if let Err(err) = builder
237 .format_indent(Some(12))
238 .filter(None, verbosity_level(verbosity_number).to_level_filter())
239 .filter(
242 Some("goblin"),
243 verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
244 )
245 .filter(
247 Some("handlebars"),
248 verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
249 )
250 .filter(
252 Some("ureq_proto"),
253 verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
254 )
255 .format(|f, record| {
256 let mut is_command_output = false;
257 if let Some(action) = record.key_values().get("action".into()) {
258 let action = action.to_cow_str().unwrap();
259 is_command_output = action == "stdout" || action == "stderr";
260 if !is_command_output {
261 let style = Style::new().fg_color(Some(AnsiColor::Green.into())).bold();
262 write!(f, "{style}{action:>12}{style:#} ")?;
263 }
264 } else {
265 let style = f.default_level_style(record.level()).bold();
266 write!(
267 f,
268 "{style}{:>12}{style:#} ",
269 prettyprint_level(record.level())
270 )?;
271 }
272
273 if !is_command_output && log::log_enabled!(Level::Debug) {
274 let style = Style::new().fg_color(Some(AnsiColor::Black.into()));
275 write!(f, "[{style}{}{style:#}] ", record.target())?;
276 }
277
278 writeln!(f, "{}", record.args())
279 })
280 .try_init()
281 {
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
309fn verbosity_level(num: u8) -> Level {
311 match num {
312 0 => Level::Info,
313 1 => Level::Debug,
314 _ => Level::Trace,
315 }
316}
317
318fn 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 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
342 let program = self.get_program().to_string_lossy().into_owned();
343 let args = self
344 .get_args()
345 .map(|a| a.to_string_lossy())
346 .collect::<Vec<_>>()
347 .join(" ");
348
349 log::debug!(action = "Running"; "Command `{program} {args}`");
350 self.status()
351 }
352
353 fn output_ok(&mut self) -> crate::Result<Output> {
354 let program = self.get_program().to_string_lossy().into_owned();
355 let args = self
356 .get_args()
357 .map(|a| a.to_string_lossy())
358 .collect::<Vec<_>>()
359 .join(" ");
360 let cmdline = format!("{program} {args}");
361 log::debug!(action = "Running"; "Command `{cmdline}`");
362
363 self.stdout(Stdio::piped());
364 self.stderr(Stdio::piped());
365
366 let mut child = self
367 .spawn()
368 .with_context(|| format!("failed to run command `{cmdline}`"))?;
369
370 let mut stdout = child.stdout.take().map(BufReader::new).unwrap();
371 let stdout_lines = Arc::new(Mutex::new(Vec::new()));
372 let stdout_lines_ = stdout_lines.clone();
373 std::thread::spawn(move || {
374 let mut line = String::new();
375 if let Ok(mut lines) = stdout_lines_.lock() {
376 loop {
377 line.clear();
378 match stdout.read_line(&mut line) {
379 Ok(0) => break,
380 Ok(_) => {
381 log::debug!(action = "stdout"; "{}", line.trim_end());
382 lines.extend(line.as_bytes());
383 }
384 Err(_) => (),
385 }
386 }
387 }
388 });
389
390 let mut stderr = child.stderr.take().map(BufReader::new).unwrap();
391 let stderr_lines = Arc::new(Mutex::new(Vec::new()));
392 let stderr_lines_ = stderr_lines.clone();
393 std::thread::spawn(move || {
394 let mut line = String::new();
395 if let Ok(mut lines) = stderr_lines_.lock() {
396 loop {
397 line.clear();
398 match stderr.read_line(&mut line) {
399 Ok(0) => break,
400 Ok(_) => {
401 log::debug!(action = "stderr"; "{}", line.trim_end());
402 lines.extend(line.as_bytes());
403 }
404 Err(_) => (),
405 }
406 }
407 }
408 });
409
410 let status = child
411 .wait()
412 .with_context(|| format!("failed to run command `{cmdline}`"))?;
413
414 let output = Output {
415 status,
416 stdout: std::mem::take(&mut *stdout_lines.lock().unwrap()),
417 stderr: std::mem::take(&mut *stderr_lines.lock().unwrap()),
418 };
419
420 if output.status.success() {
421 Ok(output)
422 } else {
423 Err(crate::error::Error::CommandFailed {
424 command: cmdline,
425 error: std::io::Error::other(String::from_utf8_lossy(&output.stderr).to_string()),
426 })
427 }
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use clap::CommandFactory;
434
435 use crate::Cli;
436
437 #[test]
438 fn verify_cli() {
439 Cli::command().debug_assert();
440 }
441
442 #[test]
443 fn help_output_includes_build() {
444 let help = Cli::command().render_help().to_string();
445 assert!(help.contains("Build"));
446 }
447}