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::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#[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 _ => Ok(Self(
78 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 #[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,
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
185pub 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
208pub 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 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 .filter(
238 Some("goblin"),
239 verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
240 )
241 .filter(
243 Some("handlebars"),
244 verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
245 )
246 .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
305fn verbosity_level(num: u8) -> Level {
307 match num {
308 0 => Level::Info,
309 1 => Level::Debug,
310 _ => Level::Trace,
311 }
312}
313
314fn 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 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}