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
13use anyhow::Context;
14pub use anyhow::Result;
15
16mod acl;
17mod add;
18mod build;
19mod bundle;
20mod completions;
21mod dev;
22mod helpers;
23mod icon;
24mod info;
25mod init;
26mod inspect;
27mod interface;
28mod migrate;
29mod mobile;
30mod plugin;
31mod remove;
32mod signer;
33
34use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
35use env_logger::fmt::style::{AnsiColor, Style};
36use env_logger::Builder;
37use log::Level;
38use serde::{Deserialize, Serialize};
39use std::io::{BufReader, Write};
40use std::process::{exit, Command, ExitStatus, Output, Stdio};
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
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ConfigValue(pub(crate) serde_json::Value);
54
55impl FromStr for ConfigValue {
56 type Err = anyhow::Error;
57
58 fn from_str(config: &str) -> std::result::Result<Self, Self::Err> {
59 if config.starts_with('{') {
60 Ok(Self(
61 serde_json::from_str(config).context("invalid configuration JSON")?,
62 ))
63 } else {
64 let path = PathBuf::from(config);
65 if path.exists() {
66 let raw = &read_to_string(&path)
67 .with_context(|| format!("invalid configuration at file {config}"))?;
68 match path.extension() {
69 Some(ext) if ext == "toml" => Ok(Self(::toml::from_str(raw)?)),
70 Some(ext) if ext == "json5" => Ok(Self(::json5::from_str(raw)?)),
71 _ => Ok(Self(
73 match ::json5::from_str(raw) {
77 Ok(json5) => json5,
78 Err(_) => serde_json::from_str(raw)?,
79 },
80 )),
81 }
82 } else {
83 anyhow::bail!("provided configuration path does not exist")
84 }
85 }
86 }
87}
88
89#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
90pub enum RunMode {
91 Desktop,
92 #[cfg(target_os = "macos")]
93 Ios,
94 Android,
95}
96
97impl Display for RunMode {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 write!(
100 f,
101 "{}",
102 match self {
103 Self::Desktop => "desktop",
104 #[cfg(target_os = "macos")]
105 Self::Ios => "iOS",
106 Self::Android => "android",
107 }
108 )
109 }
110}
111
112#[derive(Deserialize)]
113pub struct VersionMetadata {
114 tauri: String,
115 #[serde(rename = "tauri-build")]
116 tauri_build: String,
117 #[serde(rename = "tauri-plugin")]
118 tauri_plugin: String,
119}
120
121#[derive(Deserialize)]
122pub struct PackageJson {
123 name: Option<String>,
124 version: Option<String>,
125 product_name: Option<String>,
126}
127
128#[derive(Parser)]
129#[clap(
130 author,
131 version,
132 about,
133 bin_name("cargo-tauri"),
134 subcommand_required(true),
135 arg_required_else_help(true),
136 propagate_version(true),
137 no_binary_name(true)
138)]
139pub(crate) struct Cli {
140 #[clap(short, long, global = true, action = ArgAction::Count)]
142 verbose: u8,
143 #[clap(subcommand)]
144 command: Commands,
145}
146
147#[derive(Subcommand)]
148enum Commands {
149 Init(init::Options),
150 Dev(dev::Options),
151 Build(build::Options),
152 Bundle(bundle::Options),
153 Android(mobile::android::Cli),
154 #[cfg(target_os = "macos")]
155 Ios(mobile::ios::Cli),
156 Migrate,
158 Info(info::Options),
159 Add(add::Options),
160 Remove(remove::Options),
161 Plugin(plugin::Cli),
162 Icon(icon::Options),
163 Signer(signer::Cli),
164 Completions(completions::Options),
165 Permission(acl::permission::Cli),
166 Capability(acl::capability::Cli),
167 Inspect(inspect::Cli),
168}
169
170fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
171 let mut app = I::command();
172 err.format(&mut app)
173}
174
175pub fn run<I, A>(args: I, bin_name: Option<String>)
188where
189 I: IntoIterator<Item = A>,
190 A: Into<OsString> + Clone,
191{
192 if let Err(e) = try_run(args, bin_name) {
193 let mut message = e.to_string();
194 if e.chain().count() > 1 {
195 message.push(':');
196 }
197 e.chain().skip(1).for_each(|cause| {
198 let m = cause.to_string();
199 if !message.contains(&m) {
200 message.push('\n');
201 message.push_str(" - ");
202 message.push_str(&m);
203 }
204 });
205 log::error!("{message}");
206 exit(1);
207 }
208}
209
210pub fn try_run<I, A>(args: I, bin_name: Option<String>) -> Result<()>
214where
215 I: IntoIterator<Item = A>,
216 A: Into<OsString> + Clone,
217{
218 let cli = match bin_name {
219 Some(bin_name) => Cli::command().bin_name(bin_name),
220 None => Cli::command(),
221 };
222 let cli_ = cli.clone();
223 let matches = cli.get_matches_from(args);
224
225 let res = Cli::from_arg_matches(&matches).map_err(format_error::<Cli>);
226 let cli = match res {
227 Ok(s) => s,
228 Err(e) => e.exit(),
229 };
230
231 let verbosity_number = std::env::var("TAURI_CLI_VERBOSITY")
232 .ok()
233 .and_then(|v| v.parse().ok())
234 .unwrap_or(cli.verbose);
235 std::env::set_var("TAURI_CLI_VERBOSITY", verbosity_number.to_string());
237
238 let mut builder = Builder::from_default_env();
239 let init_res = builder
240 .format_indent(Some(12))
241 .filter(None, verbosity_level(verbosity_number).to_level_filter())
242 .format(|f, record| {
243 let mut is_command_output = false;
244 if let Some(action) = record.key_values().get("action".into()) {
245 let action = action.to_cow_str().unwrap();
246 is_command_output = action == "stdout" || action == "stderr";
247 if !is_command_output {
248 let style = Style::new().fg_color(Some(AnsiColor::Green.into())).bold();
249
250 write!(f, "{style}{action:>12}{style:#} ")?;
251 }
252 } else {
253 let style = f.default_level_style(record.level()).bold();
254 write!(
255 f,
256 "{style}{:>12}{style:#} ",
257 prettyprint_level(record.level())
258 )?;
259 }
260
261 if !is_command_output && log::log_enabled!(Level::Debug) {
262 let style = Style::new().fg_color(Some(AnsiColor::Black.into()));
263
264 write!(f, "[{style}{}{style:#}] ", record.target())?;
265 }
266
267 writeln!(f, "{}", record.args())
268 })
269 .try_init();
270
271 if let Err(err) = init_res {
272 eprintln!("Failed to attach logger: {err}");
273 }
274
275 match cli.command {
276 Commands::Build(options) => build::command(options, cli.verbose)?,
277 Commands::Bundle(options) => bundle::command(options, cli.verbose)?,
278 Commands::Dev(options) => dev::command(options)?,
279 Commands::Add(options) => add::command(options)?,
280 Commands::Remove(options) => remove::command(options)?,
281 Commands::Icon(options) => icon::command(options)?,
282 Commands::Info(options) => info::command(options)?,
283 Commands::Init(options) => init::command(options)?,
284 Commands::Plugin(cli) => plugin::command(cli)?,
285 Commands::Signer(cli) => signer::command(cli)?,
286 Commands::Completions(options) => completions::command(options, cli_)?,
287 Commands::Permission(options) => acl::permission::command(options)?,
288 Commands::Capability(options) => acl::capability::command(options)?,
289 Commands::Android(c) => mobile::android::command(c, cli.verbose)?,
290 #[cfg(target_os = "macos")]
291 Commands::Ios(c) => mobile::ios::command(c, cli.verbose)?,
292 Commands::Migrate => migrate::command()?,
293 Commands::Inspect(cli) => inspect::command(cli)?,
294 }
295
296 Ok(())
297}
298
299fn verbosity_level(num: u8) -> Level {
301 match num {
302 0 => Level::Info,
303 1 => Level::Debug,
304 2.. => Level::Trace,
305 }
306}
307
308fn prettyprint_level(lvl: Level) -> &'static str {
310 match lvl {
311 Level::Error => "Error",
312 Level::Warn => "Warn",
313 Level::Info => "Info",
314 Level::Debug => "Debug",
315 Level::Trace => "Trace",
316 }
317}
318
319pub trait CommandExt {
320 fn piped(&mut self) -> std::io::Result<ExitStatus>;
323 fn output_ok(&mut self) -> crate::Result<Output>;
324}
325
326impl CommandExt for Command {
327 fn piped(&mut self) -> std::io::Result<ExitStatus> {
328 self.stdin(os_pipe::dup_stdin()?);
329 self.stdout(os_pipe::dup_stdout()?);
330 self.stderr(os_pipe::dup_stderr()?);
331 let program = self.get_program().to_string_lossy().into_owned();
332 log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
333
334 self.status()
335 }
336
337 fn output_ok(&mut self) -> crate::Result<Output> {
338 let program = self.get_program().to_string_lossy().into_owned();
339 log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
340
341 self.stdout(Stdio::piped());
342 self.stderr(Stdio::piped());
343
344 let mut child = self.spawn()?;
345
346 let mut stdout = child.stdout.take().map(BufReader::new).unwrap();
347 let stdout_lines = Arc::new(Mutex::new(Vec::new()));
348 let stdout_lines_ = stdout_lines.clone();
349 std::thread::spawn(move || {
350 let mut line = String::new();
351 let mut lines = stdout_lines_.lock().unwrap();
352 loop {
353 line.clear();
354 match stdout.read_line(&mut line) {
355 Ok(0) => break,
356 Ok(_) => {
357 log::debug!(action = "stdout"; "{}", line.trim_end());
358 lines.extend(line.as_bytes().to_vec());
359 }
360 Err(_) => (),
361 }
362 }
363 });
364
365 let mut stderr = child.stderr.take().map(BufReader::new).unwrap();
366 let stderr_lines = Arc::new(Mutex::new(Vec::new()));
367 let stderr_lines_ = stderr_lines.clone();
368 std::thread::spawn(move || {
369 let mut line = String::new();
370 let mut lines = stderr_lines_.lock().unwrap();
371 loop {
372 line.clear();
373 match stderr.read_line(&mut line) {
374 Ok(0) => break,
375 Ok(_) => {
376 log::debug!(action = "stderr"; "{}", line.trim_end());
377 lines.extend(line.as_bytes().to_vec());
378 }
379 Err(_) => (),
380 }
381 }
382 });
383
384 let status = child.wait()?;
385
386 let output = Output {
387 status,
388 stdout: std::mem::take(&mut *stdout_lines.lock().unwrap()),
389 stderr: std::mem::take(&mut *stderr_lines.lock().unwrap()),
390 };
391
392 if output.status.success() {
393 Ok(output)
394 } else {
395 Err(anyhow::anyhow!("failed to run {}", program))
396 }
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use clap::CommandFactory;
403
404 use crate::Cli;
405
406 #[test]
407 fn verify_cli() {
408 Cli::command().debug_assert();
409 }
410}