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 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#[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 _ => Ok(Self(
72 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 #[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,
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
179pub 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
202pub 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 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 .filter(
233 Some("goblin"),
234 verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
235 )
236 .filter(
238 Some("handlebars"),
239 verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
240 )
241 .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
300fn verbosity_level(num: u8) -> Level {
302 match num {
303 0 => Level::Info,
304 1 => Level::Debug,
305 _ => Level::Trace,
306 }
307}
308
309fn 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 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 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}