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 match path.extension() {
69 Some(ext) if ext == "toml" => {
70 Ok(Self(::toml::from_str(&raw).with_context(|| {
71 format!("failed to parse config at {} as TOML", path.display())
72 })?))
73 }
74 Some(ext) if ext == "json5" => {
75 Ok(Self(::json5::from_str(&raw).with_context(|| {
76 format!("failed to parse config at {} as JSON5", path.display())
77 })?))
78 }
79 _ => Ok(Self(
81 match ::json5::from_str(&raw) {
85 Ok(json5) => json5,
86 Err(_) => serde_json::from_str(&raw)
87 .with_context(|| format!("failed to parse config at {} as JSON", path.display()))?,
88 },
89 )),
90 }
91 }
92 }
93}
94
95#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
96pub enum RunMode {
97 Desktop,
98 #[cfg(target_os = "macos")]
99 Ios,
100 Android,
101}
102
103impl Display for RunMode {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 write!(
106 f,
107 "{}",
108 match self {
109 Self::Desktop => "desktop",
110 #[cfg(target_os = "macos")]
111 Self::Ios => "iOS",
112 Self::Android => "android",
113 }
114 )
115 }
116}
117
118#[derive(Deserialize)]
119pub struct VersionMetadata {
120 tauri: String,
121 #[serde(rename = "tauri-build")]
122 tauri_build: String,
123 #[serde(rename = "tauri-plugin")]
124 tauri_plugin: 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
181pub fn run<I, A>(args: I, bin_name: Option<String>)
194where
195 I: IntoIterator<Item = A>,
196 A: Into<OsString> + Clone,
197{
198 if let Err(e) = try_run(args, bin_name) {
199 log::error!("{e}");
200 exit(1);
201 }
202}
203
204pub fn try_run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
208where
209 I: IntoIterator<Item = A>,
210 A: Into<OsString> + Clone,
211{
212 let cli = match bin_name {
213 Some(bin_name) => Cli::command().bin_name(bin_name),
214 None => Cli::command(),
215 };
216 let cli_ = cli.clone();
217 let matches = cli.get_matches_from(args);
218
219 let res = Cli::from_arg_matches(&matches).map_err(format_error::<Cli>);
220 let cli = match res {
221 Ok(s) => s,
222 Err(e) => e.exit(),
223 };
224
225 let verbosity_number = std::env::var("TAURI_CLI_VERBOSITY")
226 .ok()
227 .and_then(|v| v.parse().ok())
228 .unwrap_or(cli.verbose);
229 std::env::set_var("TAURI_CLI_VERBOSITY", verbosity_number.to_string());
231
232 let mut builder = Builder::from_default_env();
233 let init_res = 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 .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
254 write!(f, "{style}{action:>12}{style:#} ")?;
255 }
256 } else {
257 let style = f.default_level_style(record.level()).bold();
258 write!(
259 f,
260 "{style}{:>12}{style:#} ",
261 prettyprint_level(record.level())
262 )?;
263 }
264
265 if !is_command_output && log::log_enabled!(Level::Debug) {
266 let style = Style::new().fg_color(Some(AnsiColor::Black.into()));
267
268 write!(f, "[{style}{}{style:#}] ", record.target())?;
269 }
270
271 writeln!(f, "{}", record.args())
272 })
273 .try_init();
274
275 if let Err(err) = init_res {
276 eprintln!("Failed to attach logger: {err}");
277 }
278
279 match cli.command {
280 Commands::Build(options) => build::command(options, cli.verbose)?,
281 Commands::Bundle(options) => bundle::command(options, cli.verbose)?,
282 Commands::Dev(options) => dev::command(options)?,
283 Commands::Add(options) => add::command(options)?,
284 Commands::Remove(options) => remove::command(options)?,
285 Commands::Icon(options) => icon::command(options)?,
286 Commands::Info(options) => info::command(options)?,
287 Commands::Init(options) => init::command(options)?,
288 Commands::Plugin(cli) => plugin::command(cli)?,
289 Commands::Signer(cli) => signer::command(cli)?,
290 Commands::Completions(options) => completions::command(options, cli_)?,
291 Commands::Permission(options) => acl::permission::command(options)?,
292 Commands::Capability(options) => acl::capability::command(options)?,
293 Commands::Android(c) => mobile::android::command(c, cli.verbose)?,
294 #[cfg(target_os = "macos")]
295 Commands::Ios(c) => mobile::ios::command(c, cli.verbose)?,
296 Commands::Migrate => migrate::command()?,
297 Commands::Inspect(cli) => inspect::command(cli)?,
298 }
299
300 Ok(())
301}
302
303fn verbosity_level(num: u8) -> Level {
305 match num {
306 0 => Level::Info,
307 1 => Level::Debug,
308 2.. => Level::Trace,
309 }
310}
311
312fn prettyprint_level(lvl: Level) -> &'static str {
314 match lvl {
315 Level::Error => "Error",
316 Level::Warn => "Warn",
317 Level::Info => "Info",
318 Level::Debug => "Debug",
319 Level::Trace => "Trace",
320 }
321}
322
323pub trait CommandExt {
324 fn piped(&mut self) -> std::io::Result<ExitStatus>;
327 fn output_ok(&mut self) -> crate::Result<Output>;
328}
329
330impl CommandExt for Command {
331 fn piped(&mut self) -> std::io::Result<ExitStatus> {
332 self.stdin(os_pipe::dup_stdin()?);
333 self.stdout(os_pipe::dup_stdout()?);
334 self.stderr(os_pipe::dup_stderr()?);
335 let program = self.get_program().to_string_lossy().into_owned();
336 log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
337
338 self.status()
339 }
340
341 fn output_ok(&mut self) -> crate::Result<Output> {
342 let program = self.get_program().to_string_lossy().into_owned();
343 let args = self
344 .get_args()
345 .map(|arg| arg.to_string_lossy())
346 .fold(String::new(), |acc, arg| format!("{acc} {arg}"));
347 let cmdline = format!("{program} {args}");
348 log::debug!(action = "Running"; "Command `{cmdline}`");
349
350 self.stdout(Stdio::piped());
351 self.stderr(Stdio::piped());
352
353 let mut child = self
354 .spawn()
355 .with_context(|| format!("failed to run command `{cmdline}`"))?;
356
357 let mut stdout = child.stdout.take().map(BufReader::new).unwrap();
358 let stdout_lines = Arc::new(Mutex::new(Vec::new()));
359 let stdout_lines_ = stdout_lines.clone();
360 std::thread::spawn(move || {
361 let mut line = String::new();
362 let mut lines = stdout_lines_.lock().unwrap();
363 loop {
364 line.clear();
365 match stdout.read_line(&mut line) {
366 Ok(0) => break,
367 Ok(_) => {
368 log::debug!(action = "stdout"; "{}", line.trim_end());
369 lines.extend(line.as_bytes().to_vec());
370 }
371 Err(_) => (),
372 }
373 }
374 });
375
376 let mut stderr = child.stderr.take().map(BufReader::new).unwrap();
377 let stderr_lines = Arc::new(Mutex::new(Vec::new()));
378 let stderr_lines_ = stderr_lines.clone();
379 std::thread::spawn(move || {
380 let mut line = String::new();
381 let mut lines = stderr_lines_.lock().unwrap();
382 loop {
383 line.clear();
384 match stderr.read_line(&mut line) {
385 Ok(0) => break,
386 Ok(_) => {
387 log::debug!(action = "stderr"; "{}", line.trim_end());
388 lines.extend(line.as_bytes().to_vec());
389 }
390 Err(_) => (),
391 }
392 }
393 });
394
395 let status = child
396 .wait()
397 .with_context(|| format!("failed to run command `{cmdline}`"))?;
398
399 let output = Output {
400 status,
401 stdout: std::mem::take(&mut *stdout_lines.lock().unwrap()),
402 stderr: std::mem::take(&mut *stderr_lines.lock().unwrap()),
403 };
404
405 if output.status.success() {
406 Ok(output)
407 } else {
408 crate::error::bail!(
409 "failed to run command `{cmdline}`: command exited with status code {}",
410 output.status.code().unwrap_or(-1)
411 );
412 }
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use clap::CommandFactory;
419
420 use crate::Cli;
421
422 #[test]
423 fn verify_cli() {
424 Cli::command().debug_assert();
425 }
426}