Browse Source

feat(cli): Improve CLI logging (#4060)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Jonas Kruckenberg 3 years ago
parent
commit
35f2147161

+ 5 - 0
.changes/bundler-remove-verbose-option.md

@@ -0,0 +1,5 @@
+---
+"tauri-bundler": major
+---
+
+Remove `Settings::verbose` option. You may now bring your own `log` frontend to receive logging output from the bundler while remaining in control of verbosity and formatting.

+ 6 - 0
.changes/cli-improved-logging.md

@@ -0,0 +1,6 @@
+---
+"cli.rs": minor
+"cli.js": minor
+---
+
+Improve CLI's logging output, making use of the standard rust `log` system.

+ 5 - 6
tooling/bundler/Cargo.toml

@@ -23,24 +23,20 @@ exclude = [
 
 [dependencies]
 tauri-utils = { version = "1.0.0-rc.5", path = "../../core/tauri-utils", features = [ "resources" ] }
-ar = "0.9.0"
 icns = "0.3"
 image = "0.24.2"
 libflate = "1.2"
-md5 = "0.7.0"
 anyhow = "1.0"
 thiserror = "1.0"
 serde_json = "1.0"
 serde = { version = "1.0", features = [ "derive" ] }
 strsim = "0.10.0"
 tar = "0.4.38"
-termcolor = "1.1.3"
 toml = "0.5.9"
 walkdir = "2"
-handlebars = { version = "4.2" }
-zip = { version = "0.6" }
+handlebars = "4.2"
 tempfile = "3.3.0"
-os_pipe = "1"
+log = { version = "0.4.17", features = ["kv_unstable"]}
 
 [target."cfg(target_os = \"windows\")".dependencies]
 attohttpc = "0.19"
@@ -50,6 +46,7 @@ winreg = "0.10"
 sha2 = "0.10"
 hex = "0.4"
 glob = "0.3"
+zip = "0.6"
 
 [target."cfg(target_os = \"macos\")".dependencies]
 time = { version = "0.3", features = [ "formatting" ] }
@@ -60,6 +57,8 @@ regex = "1"
 
 [target."cfg(target_os = \"linux\")".dependencies]
 heck = "0.4"
+ar = "0.9.0"
+md5 = "0.7.0"
 
 [lib]
 name = "tauri_bundler"

+ 20 - 4
tooling/bundler/src/bundle.rs

@@ -22,10 +22,9 @@ pub use self::{
     Settings, SettingsBuilder, UpdaterSettings,
   },
 };
+use log::{info, warn};
 pub use settings::{WindowsSettings, WixLanguage, WixLanguageConfig, WixSettings};
 
-use common::{print_finished, print_info};
-
 use std::path::PathBuf;
 
 /// Generated bundle metadata.
@@ -63,7 +62,7 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
       // updater is dependant of multiple bundle, we send our bundles to prevent rebuilding
       PackageType::Updater => updater_bundle::bundle_project(&settings, &bundles)?,
       _ => {
-        print_info(&format!("ignoring {:?}", package_type))?;
+        warn!("ignoring {:?}", package_type);
         continue;
       }
     };
@@ -74,7 +73,24 @@ pub fn bundle_project(settings: Settings) -> crate::Result<Vec<Bundle>> {
     });
   }
 
-  print_finished(&bundles)?;
+  let pluralised = if bundles.len() == 1 {
+    "bundle"
+  } else {
+    "bundles"
+  };
+
+  let mut printable_paths = String::new();
+  for bundle in &bundles {
+    for path in &bundle.bundle_paths {
+      let mut note = "";
+      if bundle.package_type == crate::PackageType::Updater {
+        note = " (updater)";
+      }
+      printable_paths.push_str(&format!("        {}{}\n", path.display(), note));
+    }
+  }
+
+  info!(action = "Finished"; "{} {} at:\n{}", bundles.len(), pluralised, printable_paths);
 
   Ok(bundles)
 }

+ 25 - 73
tooling/bundler/src/bundle/common.rs

@@ -2,15 +2,15 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use crate::{CommandExt, Settings};
+use log::debug;
+
 use std::{
   ffi::OsStr,
   fs::{self, File},
-  io::{self, BufWriter, Write},
+  io::{self, BufWriter},
   path::Path,
-  process::{Command, Stdio},
+  process::{Command, Output},
 };
-use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
 
 /// Returns true if the path has a filename indicating that it is a high-desity
 /// "retina" icon.  Specifically, returns true the the file stem ends with
@@ -133,80 +133,32 @@ pub fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> {
   Ok(())
 }
 
-/// Prints a message to stderr, in the same format that `cargo` uses,
-/// indicating that we are creating a bundle with the given filename.
-pub fn print_bundling(filename: &str) -> crate::Result<()> {
-  print_progress("Bundling", filename)
-}
-
-/// Prints a message to stderr, in the same format that `cargo` uses,
-/// indicating that we have finished the the given bundles.
-pub fn print_finished(bundles: &[crate::bundle::Bundle]) -> crate::Result<()> {
-  let pluralised = if bundles.len() == 1 {
-    "bundle"
-  } else {
-    "bundles"
-  };
-  let msg = format!("{} {} at:", bundles.len(), pluralised);
-  print_progress("Finished", &msg)?;
-  for bundle in bundles {
-    for path in &bundle.bundle_paths {
-      let mut note = "";
-      if bundle.package_type == crate::PackageType::Updater {
-        note = " (updater)";
-      }
-      println!("        {}{}", path.display(), note,);
-    }
-  }
-  Ok(())
-}
-
-/// Prints a formatted bundle progress to stderr.
-fn print_progress(step: &str, msg: &str) -> crate::Result<()> {
-  let mut output = StandardStream::stderr(ColorChoice::Always);
-  let _ = output.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true));
-  write!(output, "    {}", step)?;
-  output.reset()?;
-  writeln!(output, " {}", msg)?;
-  output.flush()?;
-  Ok(())
+pub trait CommandExt {
+  fn output_ok(&mut self) -> crate::Result<Output>;
 }
 
-/// Prints a warning message to stderr, in the same format that `cargo` uses.
-#[allow(dead_code)]
-pub fn print_warning(message: &str) -> crate::Result<()> {
-  let mut output = StandardStream::stderr(ColorChoice::Always);
-  let _ = output.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true));
-  write!(output, "warning:")?;
-  output.reset()?;
-  writeln!(output, " {}", message)?;
-  output.flush()?;
-  Ok(())
-}
+impl CommandExt for Command {
+  fn output_ok(&mut self) -> crate::Result<Output> {
+    debug!(action = "Running"; "Command `{} {}`", self.get_program().to_string_lossy(), self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{} {}", acc, arg)));
 
-/// Prints a Info message to stderr.
-pub fn print_info(message: &str) -> crate::Result<()> {
-  let mut output = StandardStream::stderr(ColorChoice::Always);
-  let _ = output.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true));
-  write!(output, "info:")?;
-  output.reset()?;
-  writeln!(output, " {}", message)?;
-  output.flush()?;
-  Ok(())
-}
+    let output = self.output()?;
 
-pub fn execute_with_verbosity(cmd: &mut Command, settings: &Settings) -> crate::Result<()> {
-  if settings.is_verbose() {
-    cmd.pipe()?;
-  } else {
-    cmd.stdout(Stdio::null()).stderr(Stdio::null());
-  }
-  let status = cmd.status().expect("failed to spawn command");
+    let stdout = String::from_utf8_lossy(&output.stdout);
+    if !stdout.is_empty() {
+      debug!("Stdout: {}", stdout);
+    }
+    let stderr = String::from_utf8_lossy(&output.stderr);
+    if !stderr.is_empty() {
+      debug!("Stderr: {}", stderr);
+    }
 
-  if status.success() {
-    Ok(())
-  } else {
-    Err(anyhow::anyhow!("command failed").into())
+    if output.status.success() {
+      Ok(output)
+    } else {
+      Err(crate::Error::GenericError(
+        String::from_utf8_lossy(&output.stderr).to_string(),
+      ))
+    }
   }
 }
 

+ 10 - 17
tooling/bundler/src/bundle/linux/appimage.rs

@@ -3,13 +3,13 @@
 // SPDX-License-Identifier: MIT
 
 use super::{
-  super::{common, path_utils},
+  super::{common::CommandExt, path_utils},
   debian,
 };
 use crate::Settings;
-
+use anyhow::Context;
 use handlebars::Handlebars;
-
+use log::info;
 use std::{
   collections::BTreeMap,
   fs::{remove_dir_all, write},
@@ -76,7 +76,9 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
 
   // create the shell script file in the target/ folder.
   let sh_file = output_path.join("build_appimage.sh");
-  common::print_bundling(appimage_path.file_name().unwrap().to_str().unwrap())?;
+
+  info!(action = "Bundling"; "{} ({})", appimage_filename, appimage_path.display());
+
   write(&sh_file, temp)?;
 
   // chmod script for execution
@@ -90,19 +92,10 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
     .expect("Failed to chmod script");
 
   // execute the shell script to build the appimage.
-  let mut cmd = Command::new(&sh_file);
-  cmd.current_dir(output_path);
-
-  common::execute_with_verbosity(&mut cmd, settings).map_err(|_| {
-    crate::Error::ShellScriptError(format!(
-      "error running appimage.sh{}",
-      if settings.is_verbose() {
-        ""
-      } else {
-        ", try running with --verbose to see command output"
-      }
-    ))
-  })?;
+  Command::new(&sh_file)
+    .current_dir(output_path)
+    .output_ok()
+    .context("error running appimage.sh")?;
 
   remove_dir_all(&package_dir)?;
   Ok(vec![appimage_path])

+ 5 - 3
tooling/bundler/src/bundle/linux/debian.rs

@@ -24,11 +24,11 @@
 
 use super::super::common;
 use crate::Settings;
-
 use anyhow::Context;
 use heck::ToKebabCase;
 use image::{self, codecs::png::PngDecoder, GenericImageView, ImageDecoder};
 use libflate::gzip;
+use log::info;
 use walkdir::WalkDir;
 
 use std::{
@@ -64,14 +64,16 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
     arch
   );
   let package_name = format!("{}.deb", package_base_name);
-  common::print_bundling(&package_name)?;
+
   let base_dir = settings.project_out_directory().join("bundle/deb");
   let package_dir = base_dir.join(&package_base_name);
   if package_dir.exists() {
     fs::remove_dir_all(&package_dir)
       .with_context(|| format!("Failed to remove old {}", package_base_name))?;
   }
-  let package_path = base_dir.join(package_name);
+  let package_path = base_dir.join(&package_name);
+
+  info!(action = "Bundling"; "{} ({})", package_name, package_path.display());
 
   let (data_dir, _) = generate_data(settings, &package_dir)
     .with_context(|| "Failed to build data folders and files")?;

+ 18 - 23
tooling/bundler/src/bundle/macos/app.rs

@@ -26,9 +26,10 @@ use super::{
   icon::create_icns_file,
   sign::{notarize, notarize_auth_args, sign},
 };
-use crate::Settings;
+use crate::{bundle::common::CommandExt, Settings};
 
 use anyhow::Context;
+use log::{info, warn};
 
 use std::{
   fs,
@@ -43,11 +44,14 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
   // we should use the bundle name (App name) as a MacOS standard.
   // version or platform shouldn't be included in the App name.
   let app_product_name = format!("{}.app", settings.product_name());
-  common::print_bundling(&app_product_name)?;
+
   let app_bundle_path = settings
     .project_out_directory()
     .join("bundle/macos")
     .join(&app_product_name);
+
+  info!(action = "Bundling"; "{} ({})", app_product_name, app_bundle_path.display());
+
   if app_bundle_path.exists() {
     fs::remove_dir_all(&app_bundle_path)
       .with_context(|| format!("Failed to remove old {}", app_product_name))?;
@@ -82,14 +86,14 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
 
   if let Some(identity) = &settings.macos().signing_identity {
     // sign application
-    sign(app_bundle_path.clone(), identity, &settings, true)?;
+    sign(app_bundle_path.clone(), identity, settings, true)?;
     // notarization is required for distribution
     match notarize_auth_args() {
       Ok(args) => {
         notarize(app_bundle_path.clone(), args, settings)?;
       }
       Err(e) => {
-        common::print_info(format!("skipping app notarization, {}", e.to_string()).as_str())?;
+        warn!("skipping app notarization, {}", e.to_string());
       }
     }
   }
@@ -115,10 +119,10 @@ fn create_info_plist(
   settings: &Settings,
 ) -> crate::Result<()> {
   let format = time::format_description::parse("[year][month][day].[hour][minute][second]")
-    .map_err(|e| time::error::Error::from(e))?;
+    .map_err(time::error::Error::from)?;
   let build_number = time::OffsetDateTime::now_utc()
     .format(&format)
-    .map_err(|e| time::error::Error::from(e))?;
+    .map_err(time::error::Error::from)?;
 
   let bundle_plist_path = bundle_dir.join("Info.plist");
   let file = &mut common::create_file(&bundle_plist_path)?;
@@ -233,23 +237,14 @@ fn create_info_plist(
   file.flush()?;
 
   if let Some(user_plist_path) = &settings.macos().info_plist_path {
-    let mut cmd = Command::new("/usr/libexec/PlistBuddy");
-    cmd.args(&[
-      "-c".into(),
-      format!("Merge {}", user_plist_path.display()),
-      bundle_plist_path.display().to_string(),
-    ]);
-
-    common::execute_with_verbosity(&mut cmd, settings).map_err(|_| {
-      crate::Error::ShellScriptError(format!(
-        "error running /usr/libexec/PlistBuddy{}",
-        if settings.is_verbose() {
-          ""
-        } else {
-          ", try running with --verbose to see command output"
-        }
-      ))
-    })?;
+    Command::new("/usr/libexec/PlistBuddy")
+      .args(&[
+        "-c".into(),
+        format!("Merge {}", user_plist_path.display()),
+        bundle_plist_path.display().to_string(),
+      ])
+      .output_ok()
+      .context("error running PlistBuddy")?;
   }
 
   Ok(())

+ 18 - 22
tooling/bundler/src/bundle/macos/dmg.rs

@@ -2,10 +2,15 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{super::common, app, icon::create_icns_file};
-use crate::{bundle::Bundle, PackageType::MacOsBundle, Settings};
+use super::{app, icon::create_icns_file};
+use crate::{
+  bundle::{common::CommandExt, Bundle},
+  PackageType::MacOsBundle,
+  Settings,
+};
 
 use anyhow::Context;
+use log::info;
 
 use std::{
   env,
@@ -60,7 +65,7 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
   // create paths for script
   let bundle_script_path = output_path.join("bundle_dmg.sh");
 
-  common::print_bundling(format!("{:?}", &dmg_path).as_str())?;
+  info!(action = "Bundling"; "{} ({})", dmg_name, dmg_path.display());
 
   // write the scripts
   write(
@@ -88,9 +93,9 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
 
   let mut args = vec![
     "--volname",
-    &product_name,
+    product_name,
     "--icon",
-    &product_name,
+    product_name,
     "180",
     "170",
     "--app-drop-link",
@@ -104,7 +109,7 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
   ];
 
   let icns_icon_path =
-    create_icns_file(&output_path, &settings)?.map(|path| path.to_string_lossy().to_string());
+    create_icns_file(&output_path, settings)?.map(|path| path.to_string_lossy().to_string());
   if let Some(icon) = &icns_icon_path {
     args.push("--volicon");
     args.push(icon);
@@ -129,30 +134,21 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
     }
   }
 
+  info!(action = "Running"; "bundle_dmg.sh");
+
   // execute the bundle script
-  let mut cmd = Command::new(&bundle_script_path);
-  cmd
+  Command::new(&bundle_script_path)
     .current_dir(bundle_dir.clone())
     .args(args)
-    .args(vec![dmg_name.as_str(), bundle_file_name.as_str()]);
-
-  common::print_info("running bundle_dmg.sh")?;
-  common::execute_with_verbosity(&mut cmd, &settings).map_err(|_| {
-    crate::Error::ShellScriptError(format!(
-      "error running bundle_dmg.sh{}",
-      if settings.is_verbose() {
-        ""
-      } else {
-        ", try running with --verbose to see command output"
-      }
-    ))
-  })?;
+    .args(vec![dmg_name.as_str(), bundle_file_name.as_str()])
+    .output_ok()
+    .context("error running bundle_dmg.sh")?;
 
   fs::rename(bundle_dir.join(dmg_name), dmg_path.clone())?;
 
   // Sign DMG if needed
   if let Some(identity) = &settings.macos().signing_identity {
-    super::sign::sign(dmg_path.clone(), identity, &settings, false)?;
+    super::sign::sign(dmg_path.clone(), identity, settings, false)?;
   }
   Ok(vec![dmg_path])
 }

+ 17 - 13
tooling/bundler/src/bundle/macos/ios.rs

@@ -16,6 +16,7 @@ use crate::{bundle::common, Settings};
 
 use anyhow::Context;
 use image::{self, codecs::png::PngDecoder, GenericImageView, ImageDecoder};
+use log::{info, warn};
 
 use std::{
   collections::BTreeSet,
@@ -28,40 +29,43 @@ use std::{
 /// Bundles the project.
 /// Returns a vector of PathBuf that shows where the .app was created.
 pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
-  common::print_warning("iOS bundle support is still experimental.")?;
+  warn!("iOS bundle support is still experimental.");
 
   let app_product_name = format!("{}.app", settings.product_name());
-  common::print_bundling(&app_product_name)?;
-  let bundle_dir = settings
+
+  let app_bundle_path = settings
     .project_out_directory()
     .join("bundle/ios")
     .join(&app_product_name);
-  if bundle_dir.exists() {
-    fs::remove_dir_all(&bundle_dir)
+
+  info!(action = "Bundling"; "{} ({})", app_product_name, app_bundle_path.display());
+
+  if app_bundle_path.exists() {
+    fs::remove_dir_all(&app_bundle_path)
       .with_context(|| format!("Failed to remove old {}", app_product_name))?;
   }
-  fs::create_dir_all(&bundle_dir)
-    .with_context(|| format!("Failed to create bundle directory at {:?}", bundle_dir))?;
+  fs::create_dir_all(&app_bundle_path)
+    .with_context(|| format!("Failed to create bundle directory at {:?}", app_bundle_path))?;
 
   for src in settings.resource_files() {
     let src = src?;
-    let dest = bundle_dir.join(tauri_utils::resources::resource_relpath(&src));
+    let dest = app_bundle_path.join(tauri_utils::resources::resource_relpath(&src));
     common::copy_file(&src, &dest)
       .with_context(|| format!("Failed to copy resource file {:?}", src))?;
   }
 
-  let icon_filenames =
-    generate_icon_files(&bundle_dir, settings).with_context(|| "Failed to create app icons")?;
-  generate_info_plist(&bundle_dir, settings, &icon_filenames)
+  let icon_filenames = generate_icon_files(&app_bundle_path, settings)
+    .with_context(|| "Failed to create app icons")?;
+  generate_info_plist(&app_bundle_path, settings, &icon_filenames)
     .with_context(|| "Failed to create Info.plist")?;
 
   for bin in settings.binaries() {
     let bin_path = settings.binary_path(bin);
-    common::copy_file(&bin_path, &bundle_dir.join(bin.name()))
+    common::copy_file(&bin_path, &app_bundle_path.join(bin.name()))
       .with_context(|| format!("Failed to copy binary from {:?}", bin_path))?;
   }
 
-  Ok(vec![bundle_dir])
+  Ok(vec![app_bundle_path])
 }
 
 /// Generate the icon files and store them under the `bundle_dir`.

+ 63 - 132
tooling/bundler/src/bundle/macos/sign.rs

@@ -2,15 +2,12 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use std::{
-  ffi::OsString,
-  fs::File,
-  io::prelude::*,
-  path::PathBuf,
-  process::{Command, Stdio},
-};
-
-use crate::{bundle::common, Settings};
+use std::ffi::OsString;
+use std::{fs::File, io::prelude::*, path::PathBuf, process::Command};
+
+use crate::{bundle::common::CommandExt, Settings};
+use anyhow::Context;
+use log::info;
 use regex::Regex;
 
 const KEYCHAIN_ID: &str = "tauri-build.keychain";
@@ -28,7 +25,7 @@ pub fn setup_keychain(
 ) -> crate::Result<()> {
   // we delete any previous version of our keychain if present
   delete_keychain();
-  common::print_info("setup keychain from environment variables...")?;
+  info!("setup keychain from environment variables...");
 
   let keychain_list_output = Command::new("security")
     .args(["list-keychain", "-d", "user"])
@@ -61,36 +58,22 @@ pub fn setup_keychain(
   let mut tmp_cert = File::create(cert_path_tmp.clone())?;
   tmp_cert.write_all(certificate_encoded)?;
 
-  let decode_certificate = Command::new("base64")
+  Command::new("base64")
     .args(["--decode", "-i", &cert_path_tmp, "-o", &cert_path])
-    .stderr(Stdio::piped())
-    .status()?;
-
-  if !decode_certificate.success() {
-    return Err(anyhow::anyhow!("failed to decode certificate",).into());
-  }
+    .output_ok()
+    .context("failed to decode certificate")?;
 
-  let create_key_chain = Command::new("security")
+  Command::new("security")
     .args(["create-keychain", "-p", KEYCHAIN_PWD, KEYCHAIN_ID])
-    .stdout(Stdio::piped())
-    .stderr(Stdio::piped())
-    .status()?;
+    .output_ok()
+    .context("failed to create keychain")?;
 
-  if !create_key_chain.success() {
-    return Err(anyhow::anyhow!("failed to create keychain",).into());
-  }
-
-  let unlock_keychain = Command::new("security")
+  Command::new("security")
     .args(["unlock-keychain", "-p", KEYCHAIN_PWD, KEYCHAIN_ID])
-    .stdout(Stdio::piped())
-    .stderr(Stdio::piped())
-    .status()?;
+    .output_ok()
+    .context("failed to set unlock keychain")?;
 
-  if !unlock_keychain.success() {
-    return Err(anyhow::anyhow!("failed to set unlock keychain",).into());
-  }
-
-  let import_certificate = Command::new("security")
+  Command::new("security")
     .args([
       "import",
       &cert_path,
@@ -105,30 +88,15 @@ pub fn setup_keychain(
       "-T",
       "/usr/bin/productbuild",
     ])
-    .stderr(Stdio::inherit())
-    .output()?;
+    .output_ok()
+    .context("failed to import keychain certificate")?;
 
-  if !import_certificate.status.success() {
-    return Err(
-      anyhow::anyhow!(format!(
-        "failed to import keychain certificate {:?}",
-        std::str::from_utf8(&import_certificate.stdout)
-      ))
-      .into(),
-    );
-  }
-
-  let settings_keychain = Command::new("security")
+  Command::new("security")
     .args(["set-keychain-settings", "-t", "3600", "-u", KEYCHAIN_ID])
-    .stdout(Stdio::piped())
-    .stderr(Stdio::piped())
-    .status()?;
-
-  if !settings_keychain.success() {
-    return Err(anyhow::anyhow!("failed to set keychain settings",).into());
-  }
+    .output_ok()
+    .context("failed to set keychain settings")?;
 
-  let partition_list = Command::new("security")
+  Command::new("security")
     .args([
       "set-key-partition-list",
       "-S",
@@ -138,13 +106,8 @@ pub fn setup_keychain(
       KEYCHAIN_PWD,
       KEYCHAIN_ID,
     ])
-    .stdout(Stdio::piped())
-    .stderr(Stdio::piped())
-    .status()?;
-
-  if !partition_list.success() {
-    return Err(anyhow::anyhow!("failed to set keychain settings",).into());
-  }
+    .output_ok()
+    .context("failed to set keychain settings")?;
 
   let current_keychains = String::from_utf8_lossy(&keychain_list_output.stdout)
     .split('\n')
@@ -156,17 +119,12 @@ pub fn setup_keychain(
     .filter(|l| !l.is_empty())
     .collect::<Vec<String>>();
 
-  let set_keychain_list_entry = Command::new("security")
+  Command::new("security")
     .args(["list-keychain", "-d", "user", "-s"])
     .args(current_keychains)
     .arg(KEYCHAIN_ID)
-    .stdout(Stdio::piped())
-    .stderr(Stdio::piped())
-    .status()?;
-
-  if !set_keychain_list_entry.success() {
-    return Err(anyhow::anyhow!("failed to list keychain",).into());
-  }
+    .output_ok()
+    .context("failed to list keychain")?;
 
   Ok(())
 }
@@ -176,9 +134,7 @@ pub fn delete_keychain() {
   let _ = Command::new("security")
     .arg("delete-keychain")
     .arg(KEYCHAIN_ID)
-    .stdout(Stdio::piped())
-    .stderr(Stdio::piped())
-    .status();
+    .output_ok();
 }
 
 pub fn sign(
@@ -187,6 +143,8 @@ pub fn sign(
   settings: &Settings,
   is_an_executable: bool,
 ) -> crate::Result<()> {
+  info!(action = "Signing"; "{} with identity \"{}\"", path_to_sign.display(), identity);
+
   let setup_keychain = if let (Some(certificate_encoded), Some(certificate_password)) = (
     std::env::var_os("APPLE_CERTIFICATE"),
     std::env::var_os("APPLE_CERTIFICATE_PASSWORD"),
@@ -222,7 +180,6 @@ fn try_sign(
   is_an_executable: bool,
   tauri_keychain: bool,
 ) -> crate::Result<()> {
-  common::print_info(format!(r#"signing with identity "{}""#, identity).as_str())?;
   let mut args = vec!["--force", "-s", identity];
 
   if tauri_keychain {
@@ -231,7 +188,7 @@ fn try_sign(
   }
 
   if let Some(entitlements_path) = &settings.macos().entitlements {
-    common::print_info(format!("using entitlements file at {}", entitlements_path).as_str())?;
+    info!("using entitlements file at {}", entitlements_path);
     args.push("--entitlements");
     args.push(entitlements_path);
   }
@@ -245,14 +202,11 @@ fn try_sign(
     args.push("--deep");
   }
 
-  let status = Command::new("codesign")
+  Command::new("codesign")
     .args(args)
     .arg(path_to_sign.to_string_lossy().to_string())
-    .status()?;
-
-  if !status.success() {
-    return Err(anyhow::anyhow!("failed to sign app").into());
-  }
+    .output_ok()
+    .context("failed to sign app")?;
 
   Ok(())
 }
@@ -287,18 +241,14 @@ pub fn notarize(
 
   // use ditto to create a PKZip almost identical to Finder
   // this remove almost 99% of false alarm in notarization
-  let zip_app = Command::new("ditto")
+  Command::new("ditto")
     .args(zip_args)
-    .stderr(Stdio::inherit())
-    .status()?;
-
-  if !zip_app.success() {
-    return Err(anyhow::anyhow!("failed to zip app with ditto").into());
-  }
+    .output_ok()
+    .context("failed to zip app with ditto")?;
 
   // sign the zip file
   if let Some(identity) = &settings.macos().signing_identity {
-    sign(zip_path.clone(), identity, &settings, false)?;
+    sign(zip_path.clone(), identity, settings, false)?;
   };
 
   let mut notarize_args = vec![
@@ -317,39 +267,27 @@ pub fn notarize(
     notarize_args.push(provider_short_name);
   }
 
-  common::print_info("notarizing app")?;
+  info!(action = "Notarizing"; "{}", app_bundle_path.display());
+
   let output = Command::new("xcrun")
     .args(notarize_args)
     .args(auth_args.clone())
-    .stderr(Stdio::inherit())
-    .output()?;
-
-  if !output.status.success() {
-    return Err(
-      anyhow::anyhow!(format!(
-        "failed to upload app to Apple's notarization servers. {}",
-        std::str::from_utf8(&output.stdout)?
-      ))
-      .into(),
-    );
-  }
+    .output_ok()
+    .context("failed to upload app to Apple's notarization servers.")?;
 
   let stdout = std::str::from_utf8(&output.stdout)?;
   if let Some(uuid) = Regex::new(r"\nRequestUUID = (.+?)\n")?
     .captures_iter(stdout)
     .next()
   {
-    common::print_info("notarization started; waiting for Apple response...")?;
+    info!("notarization started; waiting for Apple response...");
+
     let uuid = uuid[1].to_string();
-    get_notarization_status(uuid, auth_args)?;
+    get_notarization_status(uuid, auth_args, settings)?;
     staple_app(app_bundle_path.clone())?;
   } else {
     return Err(
-      anyhow::anyhow!(format!(
-        "failed to parse RequestUUID from upload output. {}",
-        stdout
-      ))
-      .into(),
+      anyhow::anyhow!("failed to parse RequestUUID from upload output. {}", stdout).into(),
     );
   }
 
@@ -366,36 +304,27 @@ fn staple_app(mut app_bundle_path: PathBuf) -> crate::Result<()> {
 
   app_bundle_path.pop();
 
-  let output = Command::new("xcrun")
+  Command::new("xcrun")
     .args(vec!["stapler", "staple", "-v", filename])
     .current_dir(app_bundle_path)
-    .stderr(Stdio::inherit())
-    .output()?;
+    .output_ok()
+    .context("failed to staple app.")?;
 
-  if !output.status.success() {
-    Err(
-      anyhow::anyhow!(format!(
-        "failed to staple app. {}",
-        std::str::from_utf8(&output.stdout)?
-      ))
-      .into(),
-    )
-  } else {
-    Ok(())
-  }
+  Ok(())
 }
 
-fn get_notarization_status(uuid: String, auth_args: Vec<String>) -> crate::Result<()> {
+fn get_notarization_status(
+  uuid: String,
+  auth_args: Vec<String>,
+  settings: &Settings,
+) -> crate::Result<()> {
   std::thread::sleep(std::time::Duration::from_secs(10));
-  let output = Command::new("xcrun")
+  let result = Command::new("xcrun")
     .args(vec!["altool", "--notarization-info", &uuid])
     .args(auth_args.clone())
-    .stderr(Stdio::inherit())
-    .output()?;
+    .output_ok();
 
-  if !output.status.success() {
-    get_notarization_status(uuid, auth_args)
-  } else {
+  if let Ok(output) = result {
     let stdout = std::str::from_utf8(&output.stdout)?;
     if let Some(status) = Regex::new(r"\n *Status: (.+?)\n")?
       .captures_iter(stdout)
@@ -403,7 +332,7 @@ fn get_notarization_status(uuid: String, auth_args: Vec<String>) -> crate::Resul
     {
       let status = status[1].to_string();
       if status == "in progress" {
-        get_notarization_status(uuid, auth_args)
+        get_notarization_status(uuid, auth_args, settings)
       } else if status == "invalid" {
         Err(
           anyhow::anyhow!(format!(
@@ -425,8 +354,10 @@ fn get_notarization_status(uuid: String, auth_args: Vec<String>) -> crate::Resul
         Ok(())
       }
     } else {
-      get_notarization_status(uuid, auth_args)
+      get_notarization_status(uuid, auth_args, settings)
     }
+  } else {
+    get_notarization_status(uuid, auth_args, settings)
   }
 }
 

+ 23 - 20
tooling/bundler/src/bundle/platform.rs

@@ -2,6 +2,8 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+use super::common::CommandExt;
+use log::warn;
 use std::process::Command;
 
 // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
@@ -36,29 +38,30 @@ fn parse_rust_cfg(cfg: String) -> RustCfg {
 /// * Errors:
 ///     * Unexpected system config
 pub fn target_triple() -> Result<String, crate::Error> {
-  let output = Command::new("rustc").args(&["--print", "cfg"]).output()?;
+  let arch_res = Command::new("rustc").args(&["--print", "cfg"]).output_ok();
 
-  let arch = if output.status.success() {
-    parse_rust_cfg(String::from_utf8_lossy(&output.stdout).into_owned())
+  let arch = match arch_res {
+    Ok(output) => parse_rust_cfg(String::from_utf8_lossy(&output.stdout).into_owned())
       .target_arch
-      .expect("could not find `target_arch` when running `rustc --print cfg`.")
-  } else {
-    super::common::print_info(&format!(
+      .expect("could not find `target_arch` when running `rustc --print cfg`."),
+    Err(err) => {
+      warn!(
       "failed to determine target arch using rustc, error: `{}`. The fallback is the architecture of the machine that compiled this crate.",
-      String::from_utf8_lossy(&output.stderr),
-    ))?;
-    if cfg!(target_arch = "x86") {
-      "i686".into()
-    } else if cfg!(target_arch = "x86_64") {
-      "x86_64".into()
-    } else if cfg!(target_arch = "arm") {
-      "armv7".into()
-    } else if cfg!(target_arch = "aarch64") {
-      "aarch64".into()
-    } else {
-      return Err(crate::Error::ArchError(String::from(
-        "Unable to determine target-architecture",
-      )));
+      err,
+    );
+      if cfg!(target_arch = "x86") {
+        "i686".into()
+      } else if cfg!(target_arch = "x86_64") {
+        "x86_64".into()
+      } else if cfg!(target_arch = "arm") {
+        "armv7".into()
+      } else if cfg!(target_arch = "aarch64") {
+        "aarch64".into()
+      } else {
+        return Err(crate::Error::ArchError(String::from(
+          "Unable to determine target-architecture",
+        )));
+      }
     }
   };
 

+ 0 - 17
tooling/bundler/src/bundle/settings.rs

@@ -372,8 +372,6 @@ pub struct Settings {
   package_types: Option<Vec<PackageType>>,
   /// the directory where the bundles will be placed.
   project_out_directory: PathBuf,
-  /// whether or not to enable verbose logging
-  is_verbose: bool,
   /// the bundle settings.
   bundle_settings: BundleSettings,
   /// the binaries to bundle.
@@ -386,7 +384,6 @@ pub struct Settings {
 #[derive(Default)]
 pub struct SettingsBuilder {
   project_out_directory: Option<PathBuf>,
-  verbose: bool,
   package_types: Option<Vec<PackageType>>,
   package_settings: Option<PackageSettings>,
   bundle_settings: BundleSettings,
@@ -409,13 +406,6 @@ impl SettingsBuilder {
     self
   }
 
-  /// Enables verbose output.
-  #[must_use]
-  pub fn verbose(mut self) -> Self {
-    self.verbose = true;
-    self
-  }
-
   /// Sets the package types to create.
   #[must_use]
   pub fn package_types(mut self, package_types: Vec<PackageType>) -> Self {
@@ -466,7 +456,6 @@ impl SettingsBuilder {
     Ok(Settings {
       package: self.package_settings.expect("package settings is required"),
       package_types: self.package_types,
-      is_verbose: self.verbose,
       project_out_directory: self
         .project_out_directory
         .expect("out directory is required"),
@@ -582,11 +571,6 @@ impl Settings {
     }
   }
 
-  /// Returns true if verbose logging is enabled
-  pub fn is_verbose(&self) -> bool {
-    self.is_verbose
-  }
-
   /// Returns the product name.
   pub fn product_name(&self) -> &str {
     &self.package.product_name
@@ -634,7 +618,6 @@ impl Settings {
           .to_string_lossy()
           .replace(&format!("-{}", self.target), ""),
       );
-      println!("{:?} {:?} {:?}", src, dest, self.target);
       common::copy_file(&src, &dest)?;
     }
     Ok(())

+ 10 - 7
tooling/bundler/src/bundle/updater_bundle.rs

@@ -12,16 +12,17 @@ use super::linux::appimage;
 
 #[cfg(target_os = "windows")]
 use super::windows::msi;
+use log::error;
 #[cfg(target_os = "windows")]
 use std::{fs::File, io::prelude::*};
 #[cfg(target_os = "windows")]
 use zip::write::FileOptions;
 
 use crate::{bundle::Bundle, Settings};
-use std::{fs, io::Write};
-
 use anyhow::Context;
+use log::info;
 use std::path::{Path, PathBuf};
+use std::{fs, io::Write};
 
 // Build update
 pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
@@ -30,7 +31,7 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
     let bundle_result = bundle_update(settings, bundles)?;
     Ok(bundle_result)
   } else {
-    common::print_info("Current platform do not support updates")?;
+    error!("Current platform do not support updates");
     Ok(vec![])
   }
 }
@@ -68,10 +69,11 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
 
   // Create our gzip file (need to send parent)
   // as we walk the source directory (source isnt added)
-  create_tar(&source_path, &osx_archived_path)
+  create_tar(source_path, &osx_archived_path)
     .with_context(|| "Failed to tar.gz update directory")?;
 
-  common::print_bundling(format!("{:?}", &osx_archived_path).as_str())?;
+  info!(action = "Bundling"; "{} ({})", osx_archived, osx_archived_path.display());
+
   Ok(vec![osx_archived_path])
 }
 
@@ -112,7 +114,8 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
   create_tar(source_path, &appimage_archived_path)
     .with_context(|| "Failed to tar.gz update directory")?;
 
-  common::print_bundling(format!("{:?}", &appimage_archived_path).as_str())?;
+  info!(action = "Bundling"; "{} ({})", appimage_archived, appimage_archived_path.display());
+
   Ok(vec![appimage_archived_path])
 }
 
@@ -141,7 +144,7 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
     let msi_archived = format!("{}.zip", source_path.display());
     let msi_archived_path = PathBuf::from(&msi_archived);
 
-    common::print_bundling(format!("{:?}", &msi_archived_path).as_str())?;
+    info!(action = "Bundling"; "{} ({})", msi_archived, msi_archived_path.display());
 
     // Create our gzip file
     create_zip(&source_path, &msi_archived_path).with_context(|| "Failed to zip update MSI")?;

+ 27 - 47
tooling/bundler/src/bundle/windows/msi/wix.rs

@@ -4,25 +4,25 @@
 
 use super::super::sign::{sign, SignParams};
 use crate::bundle::{
-  common,
+  common::CommandExt,
   path_utils::{copy_file, FileOpts},
   settings::Settings,
 };
-
+use anyhow::Context;
 use handlebars::{to_json, Handlebars};
+use log::info;
 use regex::Regex;
 use serde::{Deserialize, Serialize};
 use sha2::Digest;
-use uuid::Uuid;
-use zip::ZipArchive;
-
 use std::{
   collections::{BTreeMap, HashMap},
   fs::{create_dir_all, read_to_string, remove_dir_all, rename, write, File},
   io::{Cursor, Read, Write},
   path::{Path, PathBuf},
-  process::{Command, Stdio},
+  process::Command,
 };
+use uuid::Uuid;
+use zip::ZipArchive;
 
 // URLS for the WIX toolchain.  Can be used for crossplatform compilation.
 pub const WIX_URL: &str =
@@ -169,13 +169,13 @@ fn copy_icon(settings: &Settings, filename: &str, path: &Path) -> crate::Result<
 
 /// Function used to download Wix and VC_REDIST. Checks SHA256 to verify the download.
 fn download_and_verify(url: &str, hash: &str) -> crate::Result<Vec<u8>> {
-  common::print_info(format!("Downloading {}", url).as_str())?;
+  info!(action = "Downloading"; "{}", url);
 
   let response = attohttpc::get(url).send()?;
 
   let data: Vec<u8> = response.bytes()?;
 
-  common::print_info("validating hash")?;
+  info!("validating hash");
 
   let mut hasher = sha2::Sha256::new();
   hasher.update(&data);
@@ -257,11 +257,11 @@ fn generate_guid(key: &[u8]) -> Uuid {
 
 // Specifically goes and gets Wix and verifies the download via Sha256
 pub fn get_and_extract_wix(path: &Path) -> crate::Result<()> {
-  common::print_info("Verifying wix package")?;
+  info!("Verifying wix package");
 
   let data = download_and_verify(WIX_URL, WIX_SHA256)?;
 
-  common::print_info("extracting WIX")?;
+  info!("extracting WIX");
 
   extract_zip(&data, path)
 }
@@ -301,22 +301,15 @@ fn run_candle(
   ];
 
   let candle_exe = wix_toolset_path.join("candle.exe");
-  common::print_info(format!("running candle for {:?}", wxs_file_path).as_str())?;
 
-  let mut cmd = Command::new(&candle_exe);
-  cmd.args(&args).stdout(Stdio::piped()).current_dir(cwd);
+  info!(action = "Running"; "candle for {:?}", wxs_file_path);
+  Command::new(&candle_exe)
+    .args(&args)
+    .current_dir(cwd)
+    .output_ok()
+    .context("error running candle.exe")?;
 
-  common::print_info("running candle.exe")?;
-  common::execute_with_verbosity(&mut cmd, settings).map_err(|_| {
-    crate::Error::ShellScriptError(format!(
-      "error running candle.exe{}",
-      if settings.is_verbose() {
-        ""
-      } else {
-        ", try running with --verbose to see command output"
-      }
-    ))
-  })
+  Ok(())
 }
 
 /// Runs the Light.exe file. Light takes the generated code from Candle and produces an MSI Installer.
@@ -325,7 +318,6 @@ fn run_light(
   build_path: &Path,
   arguments: Vec<String>,
   output_path: &Path,
-  settings: &Settings,
 ) -> crate::Result<()> {
   let light_exe = wix_toolset_path.join("light.exe");
 
@@ -342,19 +334,13 @@ fn run_light(
     args.push(p);
   }
 
-  let mut cmd = Command::new(&light_exe);
-  cmd.args(&args).current_dir(build_path);
+  Command::new(&light_exe)
+    .args(&args)
+    .current_dir(build_path)
+    .output_ok()
+    .context("error running light.exe")?;
 
-  common::execute_with_verbosity(&mut cmd, settings).map_err(|_| {
-    crate::Error::ShellScriptError(format!(
-      "error running light.exe{}",
-      if settings.is_verbose() {
-        ""
-      } else {
-        ", try running with --verbose to see command output"
-      }
-    ))
-  })
+  Ok(())
 }
 
 // fn get_icon_data() -> crate::Result<()> {
@@ -378,7 +364,7 @@ pub fn build_wix_app_installer(
   };
 
   // target only supports x64.
-  common::print_info(format!("Target: {}", arch).as_str())?;
+  info!("Target: {}", arch);
 
   let main_binary = settings
     .binaries()
@@ -388,7 +374,7 @@ pub fn build_wix_app_installer(
   let app_exe_source = settings.binary_path(main_binary);
   let try_sign = |file_path: &PathBuf| -> crate::Result<()> {
     if let Some(certificate_thumbprint) = &settings.windows().certificate_thumbprint {
-      common::print_info(&format!("signing {}", file_path.display()))?;
+      info!(action = "Signing"; "{}", file_path.display());
       sign(
         &file_path,
         &SignParams {
@@ -699,15 +685,9 @@ pub fn build_wix_app_installer(
     let msi_path = app_installer_output_path(settings, &language)?;
     create_dir_all(msi_path.parent().unwrap())?;
 
-    common::print_info(format!("running light to produce {}", msi_path.display()).as_str())?;
+    info!(action = "Running"; "light to produce {}", msi_path.display());
 
-    run_light(
-      wix_toolset_path,
-      &output_path,
-      arguments,
-      &msi_output_path,
-      settings,
-    )?;
+    run_light(wix_toolset_path, &output_path, arguments, &msi_output_path)?;
     rename(&msi_output_path, &msi_path)?;
     try_sign(&msi_path)?;
     output_paths.push(msi_path);

+ 10 - 12
tooling/bundler/src/bundle/windows/sign.rs

@@ -2,14 +2,13 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+use crate::bundle::common::CommandExt;
+use bitness::{self, Bitness};
+use log::{debug, info};
 use std::{
   path::{Path, PathBuf},
   process::Command,
 };
-
-use crate::bundle::common;
-
-use bitness::{self, Bitness};
 use winreg::{
   enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY},
   RegKey,
@@ -93,9 +92,13 @@ pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
   // Convert path to string reference, as we need to pass it as a commandline parameter to signtool
   let path_str = path.as_ref().to_str().unwrap();
 
+  info!(action = "Signing"; "{} with identity \"{}\"", path_str, params.certificate_thumbprint);
+
   // Construct SignTool command
   let signtool = locate_signtool()?;
-  common::print_info(format!("running signtool {:?}", signtool).as_str())?;
+
+  debug!("Running signtool {:?}", signtool);
+
   let mut cmd = Command::new(signtool);
   cmd.arg("sign");
   cmd.args(&["/fd", &params.digest_algorithm]);
@@ -113,15 +116,10 @@ pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
   cmd.arg(path_str);
 
   // Execute SignTool command
-  let output = cmd.output()?;
-
-  if !output.status.success() {
-    let stderr = String::from_utf8_lossy(output.stderr.as_slice()).into_owned();
-    return Err(crate::Error::Sign(stderr));
-  }
+  let output = cmd.output_ok()?;
 
   let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
-  common::print_info(format!("{:?}", stdout).as_str())?;
+  info!("{:?}", stdout);
 
   Ok(())
 }

+ 1 - 0
tooling/bundler/src/error.rs

@@ -34,6 +34,7 @@ pub enum Error {
   #[error("`{0}`")]
   ConvertError(#[from] num::TryFromIntError),
   /// Zip error.
+  #[cfg(target_os = "windows")]
   #[error("`{0}`")]
   ZipError(#[from] zip::result::ZipError),
   /// Hex error.

+ 0 - 12
tooling/bundler/src/lib.rs

@@ -15,18 +15,6 @@
 //! - Windows
 //!   - MSI using WiX
 
-pub(crate) trait CommandExt {
-  fn pipe(&mut self) -> Result<&mut Self>;
-}
-
-impl CommandExt for std::process::Command {
-  fn pipe(&mut self) -> Result<&mut Self> {
-    self.stdout(os_pipe::dup_stdout()?);
-    self.stderr(os_pipe::dup_stderr()?);
-    Ok(self)
-  }
-}
-
 /// The bundle API.
 pub mod bundle;
 mod error;

+ 42 - 4
tooling/cli/Cargo.lock

@@ -664,6 +664,19 @@ version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
 
+[[package]]
+name = "env_logger"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
 [[package]]
 name = "exr"
 version = "1.4.2"
@@ -992,6 +1005,12 @@ dependencies = [
  "itoa 1.0.1",
 ]
 
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
 [[package]]
 name = "icns"
 version = "0.3.1"
@@ -1306,11 +1325,12 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.16"
+version = "0.4.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
 dependencies = [
  "cfg-if 1.0.0",
+ "value-bag",
 ]
 
 [[package]]
@@ -2628,6 +2648,12 @@ version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
 
+[[package]]
+name = "sval"
+version = "1.0.0-alpha.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08"
+
 [[package]]
 name = "syn"
 version = "1.0.90"
@@ -2679,8 +2705,8 @@ dependencies = [
  "icns",
  "image",
  "libflate",
+ "log",
  "md5",
- "os_pipe",
  "regex",
  "serde",
  "serde_json",
@@ -2689,7 +2715,6 @@ dependencies = [
  "tar",
  "tauri-utils",
  "tempfile",
- "termcolor",
  "thiserror",
  "time 0.3.9",
  "toml",
@@ -2710,6 +2735,7 @@ dependencies = [
  "ctrlc",
  "dialoguer",
  "encode_unicode",
+ "env_logger",
  "glob",
  "handlebars",
  "heck",
@@ -2718,6 +2744,7 @@ dependencies = [
  "json-patch",
  "lazy_static",
  "libc",
+ "log",
  "minisign",
  "notify",
  "once_cell",
@@ -3106,6 +3133,17 @@ dependencies = [
  "uuid 0.8.2",
 ]
 
+[[package]]
+name = "value-bag"
+version = "1.0.0-alpha.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55"
+dependencies = [
+ "ctor",
+ "sval",
+ "version_check",
+]
+
 [[package]]
 name = "vcpkg"
 version = "0.2.15"

+ 3 - 1
tooling/cli/Cargo.toml

@@ -63,6 +63,8 @@ url = { version = "2.2", features = [ "serde" ] }
 os_pipe = "1"
 ignore = "0.4"
 ctrlc = "3.2"
+log = { version = "0.4.17", features = ["kv_unstable", "kv_unstable_std"]}
+env_logger = "0.9.0"
 
 [target."cfg(windows)".dependencies]
 encode_unicode = "0.3"
@@ -75,7 +77,7 @@ heck = "0.4"
 libc = "0.2"
 
 [build-dependencies]
-tauri-utils = { version = "1.0.0-rc.0", features = [ "schema", "isolation" ], path = "../../core/tauri-utils" }
+tauri-utils = { version = "1.0.0-rc.5", features = [ "schema", "isolation" ], path = "../../core/tauri-utils" }
 schemars = { version = "0.8", features = [ "url" ] }
 serde = { version = "1.0", features = [ "derive" ] }
 serde_json = "1.0"

+ 1 - 0
tooling/cli/node/src/lib.rs

@@ -19,5 +19,6 @@ pub fn run(args: Vec<String>, bin_name: Option<String>, callback: JsFunction) ->
       ThreadsafeFunctionCallMode::Blocking,
     ),
   });
+
   Ok(())
 }

+ 9 - 14
tooling/cli/src/build.rs

@@ -8,13 +8,13 @@ use crate::helpers::{
   config::{get as get_config, AppUrl, WindowUrl},
   manifest::rewrite_manifest,
   updater_signature::sign_file_from_env_variables,
-  Logger,
 };
 use crate::{CommandExt, Result};
-use anyhow::Context;
+use anyhow::{bail, Context};
 use clap::Parser;
 #[cfg(target_os = "linux")]
 use heck::ToKebabCase;
+use log::{error, info};
 use std::{env::set_current_dir, fs::rename, path::PathBuf, process::Command};
 use tauri_bundler::bundle::{bundle_project, PackageType};
 
@@ -27,9 +27,6 @@ pub struct Options {
   /// Builds with the debug flag
   #[clap(short, long)]
   debug: bool,
-  /// Enables verbose logging
-  #[clap(short, long)]
-  verbose: bool,
   /// Target triple to build against.
   /// It must be one of the values outputted by `$rustc --print target-list` or `universal-apple-darwin` for an universal macOS application.
   /// Note that compiling an universal macOS application requires both `aarch64-apple-darwin` and `x86_64-apple-darwin` targets to be installed.
@@ -49,7 +46,6 @@ pub struct Options {
 }
 
 pub fn command(options: Options) -> Result<()> {
-  let logger = Logger::new("tauri:build");
   let merge_config = if let Some(config) = &options.config {
     Some(if config.starts_with('{') {
       config.to_string()
@@ -71,13 +67,13 @@ pub fn command(options: Options) -> Result<()> {
   let config_ = config_guard.as_ref().unwrap();
 
   if config_.tauri.bundle.identifier == "com.tauri.dev" {
-    logger.error("You must change the bundle identifier in `tauri.conf.json > tauri > bundle > identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.");
+    error!("You must change the bundle identifier in `tauri.conf.json > tauri > bundle > identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.");
     std::process::exit(1);
   }
 
   if let Some(before_build) = &config_.build.before_build_command {
     if !before_build.is_empty() {
-      logger.log(format!("Running `{}`", before_build));
+      info!(action = "Running"; "beforeBuildCommand `{}`", before_build);
       #[cfg(target_os = "windows")]
       let status = Command::new("cmd")
         .arg("/S")
@@ -97,12 +93,13 @@ pub fn command(options: Options) -> Result<()> {
         .pipe()?
         .status()
         .with_context(|| format!("failed to run `{}` with `sh -c`", before_build))?;
+
       if !status.success() {
-        return Err(anyhow::anyhow!(
+        bail!(
           "beforeDevCommand `{}` failed with exit code {}",
           before_build,
           status.code().unwrap_or_default()
-        ));
+        );
       }
     }
   }
@@ -335,7 +332,6 @@ pub fn command(options: Options) -> Result<()> {
       &manifest,
       config_,
       &out_dir,
-      options.verbose,
       package_types,
     )
     .with_context(|| "failed to build bundler settings")?;
@@ -375,10 +371,9 @@ fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
     "updater archives"
   };
   let msg = format!("{} {} at:", output_paths.len(), pluralised);
-  let logger = Logger::new("Signed");
-  logger.log(&msg);
+  info!("{}", msg);
   for path in output_paths {
-    println!("        {}", path.display());
+    info!("        {}", path.display());
   }
   Ok(())
 }

+ 10 - 12
tooling/cli/src/dev.rs

@@ -8,13 +8,13 @@ use crate::{
     command_env,
     config::{get as get_config, reload as reload_config, AppUrl, ConfigHandle, WindowUrl},
     manifest::{rewrite_manifest, Manifest},
-    Logger,
   },
   CommandExt, Result,
 };
 use clap::Parser;
 
 use anyhow::Context;
+use log::{error, info, warn};
 use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
 use once_cell::sync::OnceCell;
 use shared_child::SharedChild;
@@ -78,8 +78,6 @@ pub fn command(options: Options) -> Result<()> {
 }
 
 fn command_internal(options: Options) -> Result<()> {
-  let logger = Logger::new("tauri:dev");
-
   let tauri_path = tauri_dir();
   set_current_dir(&tauri_path).with_context(|| "failed to change current working directory")?;
   let merge_config = if let Some(config) = &options.config {
@@ -102,7 +100,7 @@ fn command_internal(options: Options) -> Result<()> {
     .before_dev_command
   {
     if !before_dev.is_empty() {
-      logger.log(format!("Running `{}`", before_dev));
+      info!(action = "Running"; "BeforeDevCommand (`{}`)", before_dev);
       #[cfg(target_os = "windows")]
       let mut command = {
         let mut command = Command::new("cmd");
@@ -132,13 +130,13 @@ fn command_internal(options: Options) -> Result<()> {
         .unwrap_or_else(|_| panic!("failed to run `{}`", before_dev));
       let child = Arc::new(child);
       let child_ = child.clone();
-      let logger_ = logger.clone();
+
       std::thread::spawn(move || {
         let status = child_
           .wait()
           .expect("failed to wait on \"beforeDevCommand\"");
         if !(status.success() || KILL_BEFORE_DEV_FLAG.get().unwrap().load(Ordering::Relaxed)) {
-          logger_.error("The \"beforeDevCommand\" terminated with a non-zero status code.");
+          error!("The \"beforeDevCommand\" terminated with a non-zero status code.");
           exit(status.code().unwrap_or(1));
         }
       });
@@ -238,17 +236,17 @@ fn command_internal(options: Options) -> Result<()> {
           break;
         }
         if i % 3 == 0 {
-          logger.warn(format!(
+          warn!(
             "Waiting for your frontend dev server to start on {}...",
             dev_server_url
-          ));
+          );
         }
         i += 1;
         if i == max_attempts {
-          logger.error(format!(
-          "Could not connect to `{}` after {}s. Please make sure that is the URL to your dev server.",
-          dev_server_url, i * sleep_interval.as_secs()
-        ));
+          error!(
+            "Could not connect to `{}` after {}s. Please make sure that is the URL to your dev server.",
+            dev_server_url, i * sleep_interval.as_secs()
+          );
           exit(1);
         }
         std::thread::sleep(sleep_interval);

+ 0 - 41
tooling/cli/src/helpers/logger.rs

@@ -1,41 +0,0 @@
-// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-License-Identifier: MIT
-
-use colored::Colorize;
-
-#[derive(Clone)]
-pub struct Logger<'a> {
-  context: &'a str,
-}
-
-impl<'a> Logger<'a> {
-  pub fn new(context: &'a str) -> Self {
-    Self { context }
-  }
-
-  pub fn log(&self, message: impl AsRef<str>) {
-    println!(
-      "{} {}",
-      format!("[{}]", self.context).green().bold(),
-      message.as_ref()
-    );
-  }
-
-  pub fn warn(&self, message: impl AsRef<str>) {
-    println!(
-      "{} {}",
-      format!("[{}]", self.context).yellow().bold(),
-      message.as_ref()
-    );
-  }
-
-  #[allow(dead_code)]
-  pub fn error(&self, message: impl AsRef<str>) {
-    println!(
-      "{} {}",
-      format!("[{}]", self.context).red().bold(),
-      message.as_ref()
-    );
-  }
-}

+ 0 - 3
tooling/cli/src/helpers/mod.rs

@@ -5,13 +5,10 @@
 pub mod app_paths;
 pub mod config;
 pub mod framework;
-mod logger;
 pub mod manifest;
 pub mod template;
 pub mod updater_signature;
 
-pub use logger::Logger;
-
 use std::{
   collections::HashMap,
   path::{Path, PathBuf},

+ 5 - 5
tooling/cli/src/init.rs

@@ -5,7 +5,7 @@
 use crate::{
   helpers::{
     framework::{infer_from_package_json as infer_framework, Framework},
-    resolve_tauri_path, template, Logger,
+    resolve_tauri_path, template,
   },
   VersionMetadata,
 };
@@ -24,6 +24,7 @@ use clap::Parser;
 use dialoguer::Input;
 use handlebars::{to_json, Handlebars};
 use include_dir::{include_dir, Dir};
+use log::warn;
 use serde::Deserialize;
 
 const TEMPLATE_DIR: Dir<'_> = include_dir!("templates/app");
@@ -127,16 +128,15 @@ impl Options {
 
 pub fn command(mut options: Options) -> Result<()> {
   options = options.load()?;
-  let logger = Logger::new("tauri:init");
 
   let template_target_path = PathBuf::from(&options.directory).join("src-tauri");
   let metadata = serde_json::from_str::<VersionMetadata>(include_str!("../metadata.json"))?;
 
   if template_target_path.exists() && !options.force {
-    logger.warn(format!(
+    warn!(
       "Tauri dir ({:?}) not empty. Run `init --force` to overwrite.",
       template_target_path
-    ));
+    );
   } else {
     let (tauri_dep, tauri_build_dep) = if let Some(tauri_path) = options.tauri_path {
       (
@@ -192,7 +192,7 @@ pub fn command(mut options: Options) -> Result<()> {
         .expect("Failed to render tauri.conf.json template"),
     )
     .unwrap();
-    if crate::TARGET == Some("node") {
+    if option_env!("TARGET") == Some("node") {
       let mut dir = current_dir().expect("failed to read cwd");
       let mut count = 0;
       let mut cli_node_module_path = None;

+ 0 - 5
tooling/cli/src/interface/mod.rs

@@ -17,7 +17,6 @@ pub fn get_bundler_settings(
   manifest: &Manifest,
   config: &Config,
   out_dir: &Path,
-  verbose: bool,
   package_types: Option<Vec<PackageType>>,
 ) -> crate::Result<Settings> {
   let mut settings_builder = SettingsBuilder::new()
@@ -27,10 +26,6 @@ pub fn get_bundler_settings(
     .project_out_directory(out_dir)
     .target(target);
 
-  if verbose {
-    settings_builder = settings_builder.verbose();
-  }
-
   if let Some(types) = package_types {
     settings_builder = settings_builder.package_types(types);
   }

+ 8 - 19
tooling/cli/src/interface/rust.rs

@@ -13,6 +13,7 @@ use std::{
 use anyhow::Context;
 #[cfg(target_os = "linux")]
 use heck::ToKebabCase;
+use log::warn;
 use serde::Deserialize;
 
 use crate::{
@@ -20,7 +21,6 @@ use crate::{
     app_paths::tauri_dir,
     config::{wix_settings, Config},
     manifest::Manifest,
-    Logger,
   },
   CommandExt,
 };
@@ -101,22 +101,12 @@ struct CargoConfig {
 }
 
 pub fn build_project(runner: String, args: Vec<String>) -> crate::Result<()> {
-  let mut command = Command::new(&runner);
-  command
+  Command::new(&runner)
     .args(&["build", "--features=custom-protocol"])
-    .args(args);
-
-  command.pipe()?;
-
-  let status = command
-    .status()
-    .with_context(|| format!("failed to run {}", runner))?;
-  if !status.success() {
-    return Err(anyhow::anyhow!(format!(
-      "Result of `{} build` operation was unsuccessful: {}",
-      runner, status
-    )));
-  }
+    .args(args)
+    .pipe()?
+    .output_ok()
+    .with_context(|| format!("Result of `{} build` operation was unsuccessful", runner))?;
 
   Ok(())
 }
@@ -359,7 +349,6 @@ fn get_target_dir(
 pub fn get_workspace_dir(current_dir: &Path) -> PathBuf {
   let mut dir = current_dir.to_path_buf();
   let project_path = dir.clone();
-  let logger = Logger::new("tauri:rust");
 
   while dir.pop() {
     if dir.join("Cargo.toml").exists() {
@@ -378,13 +367,13 @@ pub fn get_workspace_dir(current_dir: &Path) -> PathBuf {
           }
         }
         Err(e) => {
-          logger.warn(format!(
+          warn!(
               "Found `{}`, which may define a parent workspace, but \
             failed to parse it. If this is indeed a parent workspace, undefined behavior may occur: \
             \n    {:#}",
               dir.display(),
               e
-            ));
+            );
         }
       }
     }

+ 103 - 15
tooling/cli/src/lib.rs

@@ -14,22 +14,12 @@ mod plugin;
 mod signer;
 
 use clap::{FromArgMatches, IntoApp, Parser, Subcommand};
-
+use env_logger::fmt::Color;
+use env_logger::Builder;
+use log::{debug, log_enabled, Level};
 use std::ffi::OsString;
-
-const TARGET: Option<&str> = option_env!("TARGET");
-
-pub(crate) trait CommandExt {
-  fn pipe(&mut self) -> Result<&mut Self>;
-}
-
-impl CommandExt for std::process::Command {
-  fn pipe(&mut self) -> Result<&mut Self> {
-    self.stdout(os_pipe::dup_stdout()?);
-    self.stderr(os_pipe::dup_stderr()?);
-    Ok(self)
-  }
-}
+use std::io::Write;
+use std::process::{Command, Output};
 
 #[derive(serde::Deserialize)]
 pub struct VersionMetadata {
@@ -50,6 +40,9 @@ pub struct VersionMetadata {
   no_binary_name(true)
 )]
 struct Cli {
+  /// Enables verbose logging
+  #[clap(short, long, global = true, parse(from_occurrences))]
+  verbose: usize,
   #[clap(subcommand)]
   command: Commands,
 }
@@ -98,6 +91,42 @@ where
     Err(e) => e.exit(),
   };
 
+  let mut builder = Builder::from_default_env();
+  let init_res = builder
+    .format_indent(Some(12))
+    .filter(None, level_from_usize(cli.verbose).to_level_filter())
+    .format(|f, record| {
+      if let Some(action) = record.key_values().get("action".into()) {
+        let mut action_style = f.style();
+        action_style.set_color(Color::Green).set_bold(true);
+
+        write!(f, "{:>12} ", action_style.value(action.to_str().unwrap()))?;
+      } else {
+        let mut level_style = f.default_level_style(record.level());
+        level_style.set_bold(true);
+
+        write!(
+          f,
+          "{:>12} ",
+          level_style.value(prettyprint_level(record.level()))
+        )?;
+      }
+
+      if log_enabled!(Level::Debug) {
+        let mut target_style = f.style();
+        target_style.set_color(Color::Black);
+
+        write!(f, "[{}] ", target_style.value(record.target()))?;
+      }
+
+      writeln!(f, "{}", record.args())
+    })
+    .try_init();
+
+  if let Err(err) = init_res {
+    eprintln!("Failed to attach logger: {}", err);
+  }
+
   match cli.command {
     Commands::Build(options) => build::command(options)?,
     Commands::Dev(options) => dev::command(options)?,
@@ -109,3 +138,62 @@ where
 
   Ok(())
 }
+
+/// This maps the occurrence of `--verbose` flags to the correct log level
+fn level_from_usize(num: usize) -> Level {
+  match num {
+    0 => Level::Info,
+    1 => Level::Debug,
+    2.. => Level::Trace,
+    _ => panic!(),
+  }
+}
+
+/// The default string representation for `Level` is all uppercaps which doesn't mix well with the other printed actions.
+fn prettyprint_level(lvl: Level) -> &'static str {
+  match lvl {
+    Level::Error => "Error",
+    Level::Warn => "Warn",
+    Level::Info => "Info",
+    Level::Debug => "Debug",
+    Level::Trace => "Trace",
+  }
+}
+
+pub trait CommandExt {
+  // The `pipe` function sets the stdout and stderr to properly
+  // show the command output in the Node.js wrapper.
+  fn pipe(&mut self) -> Result<&mut Self>;
+  fn output_ok(&mut self) -> crate::Result<Output>;
+}
+
+impl CommandExt for Command {
+  fn pipe(&mut self) -> Result<&mut Self> {
+    self.stdout(os_pipe::dup_stdout()?);
+    self.stderr(os_pipe::dup_stderr()?);
+    Ok(self)
+  }
+
+  fn output_ok(&mut self) -> crate::Result<Output> {
+    debug!(action = "Running"; "Command `{} {}`", self.get_program().to_string_lossy(), self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{} {}", acc, arg)));
+
+    let output = self.output()?;
+
+    let stdout = String::from_utf8_lossy(&output.stdout);
+    if !stdout.is_empty() {
+      debug!("Stdout: {}", stdout);
+    }
+    let stderr = String::from_utf8_lossy(&output.stderr);
+    if !stderr.is_empty() {
+      debug!("Stderr: {}", stderr);
+    }
+
+    if output.status.success() {
+      Ok(output)
+    } else {
+      Err(anyhow::anyhow!(
+        String::from_utf8_lossy(&output.stderr).to_string()
+      ))
+    }
+  }
+}

+ 2 - 1
tooling/cli/src/main.rs

@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+use anyhow::Context;
 use std::env::args_os;
 use std::ffi::OsStr;
 use std::path::Path;
@@ -32,5 +33,5 @@ fn main() -> tauri_cli::Result<()> {
     }
   };
 
-  tauri_cli::run(args, bin_name)
+  tauri_cli::run(args, bin_name).context("Try running with --verbose to see command output")
 }

+ 3 - 6
tooling/cli/src/plugin/init.rs

@@ -4,7 +4,7 @@
 
 use crate::Result;
 use crate::{
-  helpers::{resolve_tauri_path, template, Logger},
+  helpers::{resolve_tauri_path, template},
   VersionMetadata,
 };
 use anyhow::Context;
@@ -12,6 +12,7 @@ use clap::Parser;
 use handlebars::{to_json, Handlebars};
 use heck::{ToKebabCase, ToSnakeCase};
 use include_dir::{include_dir, Dir};
+use log::warn;
 use std::{collections::BTreeMap, env::current_dir, fs::remove_dir_all, path::PathBuf};
 
 const BACKEND_PLUGIN_DIR: Dir<'_> = include_dir!("templates/plugin/backend");
@@ -55,17 +56,13 @@ impl Options {
 
 pub fn command(mut options: Options) -> Result<()> {
   options.load();
-  let logger = Logger::new("tauri:init:plugin");
   let template_target_path = PathBuf::from(options.directory).join(&format!(
     "tauri-plugin-{}",
     options.plugin_name.to_kebab_case()
   ));
   let metadata = serde_json::from_str::<VersionMetadata>(include_str!("../../metadata.json"))?;
   if template_target_path.exists() {
-    logger.warn(format!(
-      "Plugin dir ({:?}) not empty.",
-      template_target_path
-    ));
+    warn!("Plugin dir ({:?}) not empty.", template_target_path);
   } else {
     let (tauri_dep, tauri_example_dep, tauri_build_dep) =
       if let Some(tauri_path) = options.tauri_path {