diff --git a/.changes/improve-errors.md b/.changes/improve-errors.md new file mode 100644 index 000000000..7a8cfe734 --- /dev/null +++ b/.changes/improve-errors.md @@ -0,0 +1,8 @@ +--- +"@tauri-apps/cli": minor:enhance +"tauri-cli": minor:enhance +"tauri-bundler": minor:enhance +--- + +Improve error messages with more context. + diff --git a/Cargo.lock b/Cargo.lock index 4ebbc36d8..1725eaf66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8615,7 +8615,6 @@ dependencies = [ name = "tauri-cli" version = "2.8.4" dependencies = [ - "anyhow", "ar", "axum", "base64 0.22.1", @@ -8681,6 +8680,7 @@ dependencies = [ "tauri-macos-sign", "tauri-utils", "tempfile", + "thiserror 2.0.12", "tokio", "toml 0.9.4", "toml_edit 0.23.2", @@ -8774,7 +8774,6 @@ dependencies = [ name = "tauri-macos-sign" version = "2.2.0" dependencies = [ - "anyhow", "apple-codesign", "chrono", "dirs 6.0.0", @@ -8787,6 +8786,7 @@ dependencies = [ "serde", "serde_json", "tempfile", + "thiserror 2.0.12", "x509-certificate 0.23.1", ] diff --git a/bench/src/run_benchmark.rs b/bench/src/run_benchmark.rs index 12b9b1204..41190486a 100644 --- a/bench/src/run_benchmark.rs +++ b/bench/src/run_benchmark.rs @@ -375,7 +375,7 @@ fn main() -> Result<()> { if let Some(filename) = bench_file.to_str() { utils::write_json(filename, &serde_json::to_value(&new_data)?) .context("failed to write benchmark results to file")?; - println!("Results written to: {}", filename); + println!("Results written to: {filename}"); } else { eprintln!("Cannot write bench.json, path contains invalid UTF-8"); } diff --git a/crates/tauri-bundler/Cargo.toml b/crates/tauri-bundler/Cargo.toml index 171d38ab3..d625da5ba 100644 --- a/crates/tauri-bundler/Cargo.toml +++ b/crates/tauri-bundler/Cargo.toml @@ -20,8 +20,8 @@ tauri-utils = { version = "2.7.0", path = "../tauri-utils", features = [ ] } image = "0.25" flate2 = "1" -anyhow = "1" thiserror = "2" +anyhow = "1" serde_json = "1" serde = { version = "1", features = ["derive"] } strsim = "0.11" diff --git a/crates/tauri-bundler/src/bundle.rs b/crates/tauri-bundler/src/bundle.rs index 113b591ff..ad4852f38 100644 --- a/crates/tauri-bundler/src/bundle.rs +++ b/crates/tauri-bundler/src/bundle.rs @@ -49,8 +49,6 @@ pub use self::{ Settings, SettingsBuilder, Size, UpdaterSettings, }, }; -#[cfg(target_os = "macos")] -use anyhow::Context; pub use settings::{NsisSettings, WindowsSettings, WixLanguage, WixLanguageConfig, WixSettings}; use std::{ @@ -223,24 +221,24 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { .map(|b| b.bundle_paths) { for app_bundle_path in &app_bundle_paths { + use crate::error::ErrorExt; + log::info!(action = "Cleaning"; "{}", app_bundle_path.display()); match app_bundle_path.is_dir() { true => std::fs::remove_dir_all(app_bundle_path), false => std::fs::remove_file(app_bundle_path), } - .with_context(|| { - format!( - "Failed to clean the app bundle at {}", - app_bundle_path.display() - ) - })? + .fs_context( + "failed to clean the app bundle", + app_bundle_path.to_path_buf(), + )?; } } } } if bundles.is_empty() { - return Err(anyhow::anyhow!("No bundles were built").into()); + return Ok(bundles); } let bundles_wo_updater = bundles diff --git a/crates/tauri-bundler/src/bundle/linux/appimage.rs b/crates/tauri-bundler/src/bundle/linux/appimage.rs index 33ca30831..1dd93d96e 100644 --- a/crates/tauri-bundler/src/bundle/linux/appimage.rs +++ b/crates/tauri-bundler/src/bundle/linux/appimage.rs @@ -6,10 +6,10 @@ use super::debian; use crate::{ bundle::settings::Arch, + error::{Context, ErrorExt}, utils::{fs_utils, http_utils::download, CommandExt}, Settings, }; -use anyhow::Context; use std::{ fs, path::{Path, PathBuf}, @@ -124,13 +124,13 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { // xdg-open will be handled by the `files` config instead if settings.deep_link_protocols().is_some() && !app_dir_usr_bin.join("xdg-open").exists() { fs::copy("/usr/bin/xdg-mime", app_dir_usr_bin.join("xdg-mime")) - .context("xdg-mime binary not found")?; + .fs_context("xdg-mime binary not found", "/usr/bin/xdg-mime".to_string())?; } // we also check if the user may have provided their own copy already if settings.appimage().bundle_xdg_open && !app_dir_usr_bin.join("xdg-open").exists() { fs::copy("/usr/bin/xdg-open", app_dir_usr_bin.join("xdg-open")) - .context("xdg-open binary not found")?; + .fs_context("xdg-open binary not found", "/usr/bin/xdg-open".to_string())?; } let search_dirs = [ diff --git a/crates/tauri-bundler/src/bundle/linux/debian.rs b/crates/tauri-bundler/src/bundle/linux/debian.rs index 6f8bad688..a99409728 100644 --- a/crates/tauri-bundler/src/bundle/linux/debian.rs +++ b/crates/tauri-bundler/src/bundle/linux/debian.rs @@ -24,8 +24,12 @@ // generate postinst or prerm files. use super::freedesktop; -use crate::{bundle::settings::Arch, utils::fs_utils, Settings}; -use anyhow::Context; +use crate::{ + bundle::settings::Arch, + error::{Context, ErrorExt}, + utils::fs_utils, + Settings, +}; use flate2::{write::GzEncoder, Compression}; use tar::HeaderMode; use walkdir::WalkDir; @@ -64,30 +68,32 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { 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}"))?; + fs::remove_dir_all(&package_dir).fs_context( + "Failed to Remove old package directory", + package_dir.clone(), + )?; } let package_path = base_dir.join(&package_name); log::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")?; + let (data_dir, _) = + generate_data(settings, &package_dir).context("Failed to build data folders and files")?; fs_utils::copy_custom_files(&settings.deb().files, &data_dir) - .with_context(|| "Failed to copy custom files")?; + .context("Failed to copy custom files")?; // Generate control files. let control_dir = package_dir.join("control"); generate_control_file(settings, arch, &control_dir, &data_dir) - .with_context(|| "Failed to create control file")?; - generate_scripts(settings, &control_dir).with_context(|| "Failed to create control scripts")?; - generate_md5sums(&control_dir, &data_dir).with_context(|| "Failed to create md5sums file")?; + .context("Failed to create control file")?; + generate_scripts(settings, &control_dir).context("Failed to create control scripts")?; + generate_md5sums(&control_dir, &data_dir).context("Failed to create md5sums file")?; // Generate `debian-binary` file; see // http://www.tldp.org/HOWTO/Debian-Binary-Package-Building-HOWTO/x60.html#AEN66 let debian_binary_path = package_dir.join("debian-binary"); create_file_with_data(&debian_binary_path, "2.0\n") - .with_context(|| "Failed to create debian-binary file")?; + .context("Failed to create debian-binary file")?; // Apply tar/gzip/ar to create the final package file. let control_tar_gz_path = diff --git a/crates/tauri-bundler/src/bundle/linux/freedesktop/mod.rs b/crates/tauri-bundler/src/bundle/linux/freedesktop/mod.rs index 4a7c08917..976d9649e 100644 --- a/crates/tauri-bundler/src/bundle/linux/freedesktop/mod.rs +++ b/crates/tauri-bundler/src/bundle/linux/freedesktop/mod.rs @@ -21,12 +21,12 @@ use std::fs::{read_to_string, File}; use std::io::BufReader; use std::path::{Path, PathBuf}; -use anyhow::Context; use handlebars::Handlebars; use image::{self, codecs::png::PngDecoder, ImageDecoder}; use serde::Serialize; use crate::{ + error::Context, utils::{self, fs_utils}, Settings, }; @@ -114,11 +114,13 @@ pub fn generate_desktop_file( if let Some(template) = custom_template_path { handlebars .register_template_string("main.desktop", read_to_string(template)?) - .with_context(|| "Failed to setup custom handlebar template")?; + .map_err(Into::into) + .context("Failed to setup custom handlebar template")?; } else { handlebars .register_template_string("main.desktop", include_str!("./main.desktop")) - .with_context(|| "Failed to setup default handlebar template")?; + .map_err(Into::into) + .context("Failed to setup default handlebar template")?; } #[derive(Serialize)] diff --git a/crates/tauri-bundler/src/bundle/linux/rpm.rs b/crates/tauri-bundler/src/bundle/linux/rpm.rs index 2944b5a6e..05835737b 100644 --- a/crates/tauri-bundler/src/bundle/linux/rpm.rs +++ b/crates/tauri-bundler/src/bundle/linux/rpm.rs @@ -3,9 +3,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::{bundle::settings::Arch, Settings}; +use crate::{bundle::settings::Arch, error::ErrorExt, Settings}; -use anyhow::Context; use rpm::{self, signature::pgp, Dependency, FileMode, FileOptions}; use std::{ env, @@ -48,10 +47,13 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { let base_dir = settings.project_out_directory().join("bundle/rpm"); 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}"))?; + fs::remove_dir_all(&package_dir).fs_context( + "Failed to remove old package directory", + package_dir.clone(), + )?; } - fs::create_dir_all(&package_dir)?; + fs::create_dir_all(&package_dir) + .fs_context("Failed to create package directory", package_dir.clone())?; let package_path = base_dir.join(&package_name); log::info!(action = "Bundling"; "{} ({})", package_name, package_path.display()); diff --git a/crates/tauri-bundler/src/bundle/macos/app.rs b/crates/tauri-bundler/src/bundle/macos/app.rs index 3a52f7762..87e900ac9 100644 --- a/crates/tauri-bundler/src/bundle/macos/app.rs +++ b/crates/tauri-bundler/src/bundle/macos/app.rs @@ -24,16 +24,15 @@ use super::{ icon::create_icns_file, - sign::{notarize, notarize_auth, notarize_without_stapling, sign, NotarizeAuthError, SignTarget}, + sign::{notarize, notarize_auth, notarize_without_stapling, sign, SignTarget}, }; use crate::{ + error::{Context, ErrorExt, NotarizeAuthError}, utils::{fs_utils, CommandExt}, Error::GenericError, Settings, }; -use anyhow::Context; - use std::{ ffi::OsStr, fs, @@ -65,12 +64,16 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { log::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::remove_dir_all(&app_bundle_path).fs_context( + "failed to remove old app bundle", + app_bundle_path.to_path_buf(), + )?; } let bundle_directory = app_bundle_path.join("Contents"); - fs::create_dir_all(&bundle_directory) - .with_context(|| format!("Failed to create bundle directory at {bundle_directory:?}"))?; + fs::create_dir_all(&bundle_directory).fs_context( + "failed to create bundle directory", + bundle_directory.to_path_buf(), + )?; let resources_dir = bundle_directory.join("Resources"); let bin_dir = bundle_directory.join("MacOS"); @@ -134,7 +137,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { } Err(e) => { if matches!(e, NotarizeAuthError::MissingTeamId) { - return Err(anyhow::anyhow!("{e}").into()); + return Err(e.into()); } else { log::warn!("skipping app notarization, {}", e.to_string()); } @@ -401,8 +404,10 @@ fn copy_frameworks_to_bundle( return Ok(paths); } let dest_dir = bundle_directory.join("Frameworks"); - fs::create_dir_all(bundle_directory) - .with_context(|| format!("Failed to create Frameworks directory at {dest_dir:?}"))?; + fs::create_dir_all(&dest_dir).fs_context( + "failed to create Frameworks directory", + dest_dir.to_path_buf(), + )?; for framework in frameworks.iter() { if framework.ends_with(".framework") { let src_path = PathBuf::from(framework); diff --git a/crates/tauri-bundler/src/bundle/macos/dmg/mod.rs b/crates/tauri-bundler/src/bundle/macos/dmg/mod.rs index eda756329..2c756faee 100644 --- a/crates/tauri-bundler/src/bundle/macos/dmg/mod.rs +++ b/crates/tauri-bundler/src/bundle/macos/dmg/mod.rs @@ -6,12 +6,11 @@ use super::{app, icon::create_icns_file}; use crate::{ bundle::{settings::Arch, Bundle}, + error::{Context, ErrorExt}, utils::CommandExt, PackageType, Settings, }; -use anyhow::Context; - use std::{ env, fs::{self, write}, @@ -68,10 +67,9 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result< for path in &[&support_directory_path, &output_path] { if path.exists() { - fs::remove_dir_all(path).with_context(|| format!("Failed to remove old {dmg_name}"))?; + fs::remove_dir_all(path).fs_context("failed to remove old dmg", path.to_path_buf())?; } - fs::create_dir_all(path) - .with_context(|| format!("Failed to create output directory at {path:?}"))?; + fs::create_dir_all(path).fs_context("failed to create output directory", path.to_path_buf())?; } // create paths for script diff --git a/crates/tauri-bundler/src/bundle/macos/ios.rs b/crates/tauri-bundler/src/bundle/macos/ios.rs index 1fbf8e28b..e2a028113 100644 --- a/crates/tauri-bundler/src/bundle/macos/ios.rs +++ b/crates/tauri-bundler/src/bundle/macos/ios.rs @@ -14,11 +14,11 @@ // explanation. use crate::{ + error::{Context, ErrorExt}, utils::{self, fs_utils}, Settings, }; -use anyhow::Context; use image::{codecs::png::PngDecoder, GenericImageView, ImageDecoder}; use std::{ @@ -44,11 +44,15 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { log::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::remove_dir_all(&app_bundle_path).fs_context( + "failed to remove old app bundle", + app_bundle_path.to_path_buf(), + )?; } - fs::create_dir_all(&app_bundle_path) - .with_context(|| format!("Failed to create bundle directory at {app_bundle_path:?}"))?; + fs::create_dir_all(&app_bundle_path).fs_context( + "failed to create bundle directory", + app_bundle_path.to_path_buf(), + )?; for src in settings.resource_files() { let src = src?; diff --git a/crates/tauri-bundler/src/bundle/macos/sign.rs b/crates/tauri-bundler/src/bundle/macos/sign.rs index 052779c0c..195908ff9 100644 --- a/crates/tauri-bundler/src/bundle/macos/sign.rs +++ b/crates/tauri-bundler/src/bundle/macos/sign.rs @@ -9,7 +9,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::Settings; +use crate::{error::NotarizeAuthError, Settings}; pub struct SignTarget { pub path: PathBuf, @@ -23,11 +23,14 @@ pub fn keychain(identity: Option<&str>) -> crate::Result crate::Result<()> { - tauri_macos_sign::notarize(keychain, &app_bundle_path, credentials).map_err(Into::into) + tauri_macos_sign::notarize(keychain, &app_bundle_path, credentials) + .map_err(Box::new) + .map_err(Into::into) } pub fn notarize_without_stapling( @@ -77,19 +84,10 @@ pub fn notarize_without_stapling( credentials: &tauri_macos_sign::AppleNotarizationCredentials, ) -> crate::Result<()> { tauri_macos_sign::notarize_without_stapling(keychain, &app_bundle_path, credentials) + .map_err(Box::new) .map_err(Into::into) } -#[derive(Debug, thiserror::Error)] -pub enum NotarizeAuthError { - #[error( - "The team ID is now required for notarization with app-specific password as authentication. Please set the `APPLE_TEAM_ID` environment variable. You can find the team ID in https://developer.apple.com/account#MembershipDetailsCard." - )] - MissingTeamId, - #[error(transparent)] - Anyhow(#[from] anyhow::Error), -} - pub fn notarize_auth() -> Result { match ( @@ -106,10 +104,18 @@ pub fn notarize_auth() -> Result Err(NotarizeAuthError::MissingTeamId), _ => { - match (var_os("APPLE_API_KEY"), var_os("APPLE_API_ISSUER"), var("APPLE_API_KEY_PATH")) { + match ( + var_os("APPLE_API_KEY"), + var_os("APPLE_API_ISSUER"), + var("APPLE_API_KEY_PATH"), + ) { (Some(key_id), Some(issuer), Ok(key_path)) => { - Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey { key_id, key: tauri_macos_sign::ApiKey::Path( key_path.into()), issuer }) - }, + Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey { + key_id, + key: tauri_macos_sign::ApiKey::Path(key_path.into()), + issuer, + }) + } (Some(key_id), Some(issuer), Err(_)) => { let mut api_key_file_name = OsString::from("AuthKey_"); api_key_file_name.push(&key_id); @@ -131,12 +137,18 @@ pub fn notarize_auth() -> Result Err(anyhow::anyhow!("no APPLE_ID & APPLE_PASSWORD & APPLE_TEAM_ID or APPLE_API_KEY & APPLE_API_ISSUER & APPLE_API_KEY_PATH environment variables found").into()) + _ => Err(NotarizeAuthError::MissingCredentials), } } } diff --git a/crates/tauri-bundler/src/bundle/settings.rs b/crates/tauri-bundler/src/bundle/settings.rs index 8703b5bb3..be22a94a7 100644 --- a/crates/tauri-bundler/src/bundle/settings.rs +++ b/crates/tauri-bundler/src/bundle/settings.rs @@ -4,8 +4,7 @@ // SPDX-License-Identifier: MIT use super::category::AppCategory; -use crate::{bundle::platform::target_triple, utils::fs_utils}; -use anyhow::Context; +use crate::{bundle::platform::target_triple, error::Context, utils::fs_utils}; pub use tauri_utils::config::WebviewInstallMode; use tauri_utils::{ config::{ @@ -969,7 +968,6 @@ impl Settings { .iter() .find(|bin| bin.main) .context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file") - .map_err(Into::into) } /// Returns the file name of the binary being bundled. @@ -979,7 +977,6 @@ impl Settings { .iter_mut() .find(|bin| bin.main) .context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file") - .map_err(Into::into) } /// Returns the file name of the binary being bundled. @@ -990,7 +987,6 @@ impl Settings { .find(|bin| bin.main) .context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file") .map(|b| b.name()) - .map_err(Into::into) } /// Returns the path to the specified binary. diff --git a/crates/tauri-bundler/src/bundle/updater_bundle.rs b/crates/tauri-bundler/src/bundle/updater_bundle.rs index 9355f266d..cf822e3cd 100644 --- a/crates/tauri-bundler/src/bundle/updater_bundle.rs +++ b/crates/tauri-bundler/src/bundle/updater_bundle.rs @@ -11,6 +11,7 @@ use crate::{ }, Bundle, }, + error::{Context, ErrorExt}, utils::fs_utils, Settings, }; @@ -22,7 +23,6 @@ use std::{ path::{Path, PathBuf}, }; -use anyhow::Context; use zip::write::SimpleFileOptions; // Build update @@ -216,7 +216,9 @@ pub fn create_zip(src_file: &Path, dst_file: &Path) -> crate::Result { .unix_permissions(0o755); zip.start_file(file_name.to_string_lossy(), options)?; - let mut f = File::open(src_file)?; + let mut f = + File::open(src_file).fs_context("failed to open updater ZIP file", src_file.to_path_buf())?; + let mut buffer = Vec::new(); f.read_to_end(&mut buffer)?; zip.write_all(&buffer)?; diff --git a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs index 84ed46c33..be0624f16 100644 --- a/crates/tauri-bundler/src/bundle/windows/msi/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/msi/mod.rs @@ -14,13 +14,13 @@ use crate::{ }, }, }, + error::Context, utils::{ fs_utils::copy_file, http_utils::{download_and_verify, extract_zip, HashAlgorithm}, CommandExt, }, }; -use anyhow::{bail, Context}; use handlebars::{html_escape, to_json, Handlebars}; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -279,37 +279,40 @@ fn clear_env_for_wix(cmd: &mut Command) { } } -fn validate_wix_version(version_str: &str) -> anyhow::Result<()> { +fn validate_wix_version(version_str: &str) -> crate::Result<()> { let components = version_str .split('.') .flat_map(|c| c.parse::().ok()) .collect::>(); - anyhow::ensure!( - components.len() >= 3, - "app wix version should be in the format major.minor.patch.build (build is optional)" - ); + if components.len() < 3 { + crate::error::bail!( + "app wix version should be in the format major.minor.patch.build (build is optional)" + ); + } if components[0] > 255 { - bail!("app version major number cannot be greater than 255"); + crate::error::bail!("app version major number cannot be greater than 255"); } if components[1] > 255 { - bail!("app version minor number cannot be greater than 255"); + crate::error::bail!("app version minor number cannot be greater than 255"); } if components[2] > 65535 { - bail!("app version patch number cannot be greater than 65535"); + crate::error::bail!("app version patch number cannot be greater than 65535"); } if components.len() == 4 && components[3] > 65535 { - bail!("app version build number cannot be greater than 65535"); + crate::error::bail!("app version build number cannot be greater than 65535"); } Ok(()) } // WiX requires versions to be numeric only in a `major.minor.patch.build` format -fn convert_version(version_str: &str) -> anyhow::Result { - let version = semver::Version::parse(version_str).context("invalid app version")?; +fn convert_version(version_str: &str) -> crate::Result { + let version = semver::Version::parse(version_str) + .map_err(Into::into) + .context("invalid app version")?; if !version.build.is_empty() { let build = version.build.parse::(); if build.map(|b| b <= 65535).unwrap_or_default() { @@ -318,7 +321,7 @@ fn convert_version(version_str: &str) -> anyhow::Result { version.major, version.minor, version.patch, version.build )); } else { - bail!("optional build metadata in app version must be numeric-only and cannot be greater than 65535 for msi target"); + crate::error::bail!("optional build metadata in app version must be numeric-only and cannot be greater than 65535 for msi target"); } } @@ -330,7 +333,7 @@ fn convert_version(version_str: &str) -> anyhow::Result { version.major, version.minor, version.patch, version.pre )); } else { - bail!("optional pre-release identifier in app version must be numeric-only and cannot be greater than 65535 for msi target"); + crate::error::bail!("optional pre-release identifier in app version must be numeric-only and cannot be greater than 65535 for msi target"); } } @@ -387,11 +390,7 @@ fn run_candle( cmd.arg(ext); } clear_env_for_wix(&mut cmd); - cmd - .args(&args) - .current_dir(cwd) - .output_ok() - .context("error running candle.exe")?; + cmd.args(&args).current_dir(cwd).output_ok()?; Ok(()) } @@ -416,11 +415,7 @@ fn run_light( cmd.arg(ext); } clear_env_for_wix(&mut cmd); - cmd - .args(&args) - .current_dir(build_path) - .output_ok() - .context("error running light.exe")?; + cmd.args(&args).current_dir(build_path).output_ok()?; Ok(()) } @@ -472,8 +467,7 @@ pub fn build_wix_app_installer( // when we're performing code signing, we'll sign some WiX DLLs, so we make a local copy let wix_toolset_path = if settings.windows().can_sign() { let wix_path = output_path.join("wix"); - crate::utils::fs_utils::copy_dir(wix_toolset_path, &wix_path) - .context("failed to copy wix directory")?; + crate::utils::fs_utils::copy_dir(wix_toolset_path, &wix_path)?; wix_path } else { wix_toolset_path.to_path_buf() diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs index edd4acbe1..d63a93241 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs @@ -13,15 +13,16 @@ use crate::{ }, }, }, + error::ErrorExt, utils::{ http_utils::{download_and_verify, verify_file_hash, HashAlgorithm}, CommandExt, }, - Settings, + Error, Settings, }; use tauri_utils::display_path; -use anyhow::Context; +use crate::error::Context; use handlebars::{to_json, Handlebars}; use tauri_utils::config::{NSISInstallerMode, NsisCompression, WebviewInstallMode}; @@ -105,8 +106,9 @@ pub fn bundle_project(settings: &Settings, updater: bool) -> crate::Result c Ok(()) } -fn try_add_numeric_build_number(version_str: &str) -> anyhow::Result { - let version = semver::Version::parse(version_str).context("invalid app version")?; +fn try_add_numeric_build_number(version_str: &str) -> crate::Result { + let version = semver::Version::parse(version_str) + .map_err(|error| Error::GenericError(format!("invalid app version: {error}")))?; if !version.build.is_empty() { let build = version.build.parse::(); if build.is_ok() { @@ -199,31 +202,39 @@ fn build_nsis_app_installer( .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from("/usr/share/nsis")); #[cfg(target_os = "macos")] - let system_nsis_toolset_path = std::env::var_os("NSIS_PATH") - .map(PathBuf::from) - .ok_or_else(|| anyhow::anyhow!("failed to resolve NSIS path")) - .or_else(|_| { - let mut makensis_path = - which::which("makensis").context("failed to resolve `makensis`; did you install nsis? See https://tauri.app/distribute/windows-installer/#install-nsis for more information")?; - // homebrew installs it as a symlink - if makensis_path.is_symlink() { - // read_link might return a path relative to makensis_path so we must use join() and canonicalize - makensis_path = makensis_path - .parent() - .context("missing makensis parent")? - .join(std::fs::read_link(&makensis_path).context("failed to resolve makensis symlink")?) - .canonicalize() - .context("failed to resolve makensis path")?; - } - // file structure: - // ├── bin - // │ ├── makensis - // ├── share - // │ ├── nsis - let bin_folder = makensis_path.parent().context("missing makensis parent")?; - let root_folder = bin_folder.parent().context("missing makensis root")?; - crate::Result::Ok(root_folder.join("share").join("nsis")) + let system_nsis_toolset_path = std::env::var_os("NSIS_PATH") + .map(PathBuf::from) + .context("failed to resolve NSIS path") + .or_else(|_| { + let mut makensis_path = which::which("makensis").map_err(|error| Error::CommandFailed { + command: "makensis".to_string(), + error: std::io::Error::other(format!("failed to find makensis: {error}")), })?; + // homebrew installs it as a symlink + if makensis_path.is_symlink() { + // read_link might return a path relative to makensis_path so we must use join() and canonicalize + makensis_path = makensis_path + .parent() + .context("missing makensis parent")? + .join( + std::fs::read_link(&makensis_path) + .fs_context("failed to resolve makensis symlink", makensis_path.clone())?, + ) + .canonicalize() + .fs_context( + "failed to canonicalize makensis path", + makensis_path.clone(), + )?; + } + // file structure: + // ├── bin + // │ ├── makensis + // ├── share + // │ ├── nsis + let bin_folder = makensis_path.parent().context("missing makensis parent")?; + let root_folder = bin_folder.parent().context("missing makensis root")?; + crate::Result::Ok(root_folder.join("share").join("nsis")) + })?; #[cfg(windows)] let system_nsis_toolset_path = nsis_toolset_path.to_path_buf(); @@ -636,7 +647,10 @@ fn build_nsis_app_installer( .env_remove("NSISCONFDIR") .current_dir(output_path) .piped() - .context("error running makensis.exe")?; + .map_err(|error| Error::CommandFailed { + command: "makensis.exe".to_string(), + error, + })?; fs::rename(nsis_output_path, &nsis_installer_path)?; @@ -808,7 +822,11 @@ fn generate_estimated_size( .chain(resources.keys()) { size += std::fs::metadata(k) - .with_context(|| format!("when getting size of {}", k.display()))? + .map_err(|error| Error::Fs { + context: "when getting size of", + path: k.to_path_buf(), + error, + })? .len(); } Ok(size / 1024) diff --git a/crates/tauri-bundler/src/bundle/windows/util.rs b/crates/tauri-bundler/src/bundle/windows/util.rs index 0f3e9bde5..3685d0c06 100644 --- a/crates/tauri-bundler/src/bundle/windows/util.rs +++ b/crates/tauri-bundler/src/bundle/windows/util.rs @@ -27,17 +27,14 @@ pub fn webview2_guid_path(url: &str) -> crate::Result<(String, String)> { let response = agent.head(url).call().map_err(Box::new)?; let final_url = response.get_uri().to_string(); let remaining_url = final_url.strip_prefix(WEBVIEW2_URL_PREFIX).ok_or_else(|| { - anyhow::anyhow!( - "WebView2 URL prefix mismatch. Expected `{}`, found `{}`.", - WEBVIEW2_URL_PREFIX, - final_url - ) + crate::Error::GenericError(format!( + "WebView2 URL prefix mismatch. Expected `{WEBVIEW2_URL_PREFIX}`, found `{final_url}`." + )) })?; let (guid, filename) = remaining_url.split_once('/').ok_or_else(|| { - anyhow::anyhow!( - "WebView2 URL format mismatch. Expected `/`, found `{}`.", - remaining_url - ) + crate::Error::GenericError(format!( + "WebView2 URL format mismatch. Expected `/`, found `{remaining_url}`." + )) })?; Ok((guid.into(), filename.into())) } diff --git a/crates/tauri-bundler/src/error.rs b/crates/tauri-bundler/src/error.rs index e39242eed..3a8fe82dd 100644 --- a/crates/tauri-bundler/src/error.rs +++ b/crates/tauri-bundler/src/error.rs @@ -3,17 +3,45 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{io, num, path}; +use std::{ + fmt::Display, + io, num, + path::{self, PathBuf}, +}; use thiserror::Error as DeriveError; /// Errors returned by the bundler. #[derive(Debug, DeriveError)] #[non_exhaustive] pub enum Error { + /// Error with context. Created by the [`Context`] trait. + #[error("{0}: {1}")] + Context(String, Box), + /// File system error. + #[error("{context} {path}: {error}")] + Fs { + /// Context of the error. + context: &'static str, + /// Path that was accessed. + path: PathBuf, + /// Error that occurred. + error: io::Error, + }, + /// Child process error. + #[error("failed to run command {command}: {error}")] + CommandFailed { + /// Command that failed. + command: String, + /// Error that occurred. + error: io::Error, + }, /// Error running tauri_utils API. #[error("{0}")] Resource(#[from] tauri_utils::Error), /// Bundler error. + /// + /// This variant is no longer used as this crate no longer uses anyhow. + // TODO(v3): remove this variant #[error("{0:#}")] BundlerError(#[from] anyhow::Error), /// I/O error. @@ -133,7 +161,110 @@ pub enum Error { #[cfg(target_os = "linux")] #[error("{0}")] RpmError(#[from] rpm::Error), + /// Failed to notarize application. + #[cfg(target_os = "macos")] + #[error("failed to notarize app: {0}")] + AppleNotarization(#[from] NotarizeAuthError), + /// Failed to codesign application. + #[cfg(target_os = "macos")] + #[error("failed codesign application: {0}")] + AppleCodesign(#[from] Box), + /// Handlebars template error. + #[error(transparent)] + Template(#[from] handlebars::TemplateError), + /// Semver error. + #[error("`{0}`")] + SemverError(#[from] semver::Error), +} + +#[cfg(target_os = "macos")] +#[allow(clippy::enum_variant_names)] +#[derive(Debug, thiserror::Error)] +pub enum NotarizeAuthError { + #[error( + "The team ID is now required for notarization with app-specific password as authentication. Please set the `APPLE_TEAM_ID` environment variable. You can find the team ID in https://developer.apple.com/account#MembershipDetailsCard." + )] + MissingTeamId, + #[error("could not find API key file. Please set the APPLE_API_KEY_PATH environment variables to the path to the {file_name} file")] + MissingApiKey { file_name: String }, + #[error("no APPLE_ID & APPLE_PASSWORD & APPLE_TEAM_ID or APPLE_API_KEY & APPLE_API_ISSUER & APPLE_API_KEY_PATH environment variables found")] + MissingCredentials, } /// Convenient type alias of Result type. pub type Result = std::result::Result; + +pub trait Context { + // Required methods + fn context(self, context: C) -> Result + where + C: Display + Send + Sync + 'static; + fn with_context(self, f: F) -> Result + where + C: Display + Send + Sync + 'static, + F: FnOnce() -> C; +} + +impl Context for Result { + fn context(self, context: C) -> Result + where + C: Display + Send + Sync + 'static, + { + self.map_err(|e| Error::Context(context.to_string(), Box::new(e))) + } + + fn with_context(self, f: F) -> Result + where + C: Display + Send + Sync + 'static, + F: FnOnce() -> C, + { + self.map_err(|e| Error::Context(f().to_string(), Box::new(e))) + } +} + +impl Context for Option { + fn context(self, context: C) -> Result + where + C: Display + Send + Sync + 'static, + { + self.ok_or_else(|| Error::GenericError(context.to_string())) + } + + fn with_context(self, f: F) -> Result + where + C: Display + Send + Sync + 'static, + F: FnOnce() -> C, + { + self.ok_or_else(|| Error::GenericError(f().to_string())) + } +} + +pub trait ErrorExt { + fn fs_context(self, context: &'static str, path: impl Into) -> Result; +} + +impl ErrorExt for std::result::Result { + fn fs_context(self, context: &'static str, path: impl Into) -> Result { + self.map_err(|error| Error::Fs { + context, + path: path.into(), + error, + }) + } +} + +#[allow(unused)] +macro_rules! bail { + ($msg:literal $(,)?) => { + return Err(crate::Error::GenericError($msg.into())) + }; + ($err:expr $(,)?) => { + return Err(crate::Error::GenericError($err)) + }; + ($fmt:expr, $($arg:tt)*) => { + return Err(crate::Error::GenericError(format!($fmt, $($arg)*))) + }; +} + +#[allow(unused)] +pub(crate) use bail; diff --git a/crates/tauri-cli/Cargo.toml b/crates/tauri-cli/Cargo.toml index a1e32c001..b8bf475c4 100644 --- a/crates/tauri-cli/Cargo.toml +++ b/crates/tauri-cli/Cargo.toml @@ -46,7 +46,7 @@ jsonrpsee-ws-client = { version = "0.24", default-features = false } sublime_fuzzy = "0.7" clap_complete = "4" clap = { version = "4", features = ["derive", "env"] } -anyhow = "1" +thiserror = "2" tauri-bundler = { version = "2.6.1", default-features = false, path = "../tauri-bundler" } colored = "2" serde = { version = "1", features = ["derive"] } diff --git a/crates/tauri-cli/src/acl/capability/new.rs b/crates/tauri-cli/src/acl/capability/new.rs index e3618dde3..aceddc05d 100644 --- a/crates/tauri-cli/src/acl/capability/new.rs +++ b/crates/tauri-cli/src/acl/capability/new.rs @@ -9,6 +9,7 @@ use tauri_utils::acl::capability::{Capability, PermissionEntry}; use crate::{ acl::FileFormat, + error::ErrorExt, helpers::{app_paths::tauri_dir, prompts}, Result, }; @@ -106,7 +107,9 @@ pub fn command(options: Options) -> Result<()> { }; let path = match options.out { - Some(o) => o.canonicalize()?, + Some(o) => o + .canonicalize() + .fs_context("failed to canonicalize capability file path", o.clone())?, None => { let dir = tauri_dir(); let capabilities_dir = dir.join("capabilities"); @@ -125,17 +128,21 @@ pub fn command(options: Options) -> Result<()> { ); let overwrite = prompts::confirm(&format!("{msg}, overwrite?"), Some(false))?; if overwrite { - std::fs::remove_file(&path)?; + std::fs::remove_file(&path).fs_context("failed to remove capability file", path.clone())?; } else { - anyhow::bail!(msg); + crate::error::bail!(msg); } } if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; + std::fs::create_dir_all(parent).fs_context( + "failed to create capability directory", + parent.to_path_buf(), + )?; } - std::fs::write(&path, options.format.serialize(&capability)?)?; + std::fs::write(&path, options.format.serialize(&capability)?) + .fs_context("failed to write capability file", path.clone())?; log::info!(action = "Created"; "capability at {}", dunce::simplified(&path).display()); diff --git a/crates/tauri-cli/src/acl/mod.rs b/crates/tauri-cli/src/acl/mod.rs index 099ff56fb..c830034bc 100644 --- a/crates/tauri-cli/src/acl/mod.rs +++ b/crates/tauri-cli/src/acl/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use crate::error::Context; use serde::Serialize; use std::fmt::Display; @@ -33,8 +34,8 @@ impl FileFormat { pub fn serialize(&self, s: &S) -> crate::Result { let contents = match self { - Self::Json => serde_json::to_string_pretty(s)?, - Self::Toml => toml_edit::ser::to_string_pretty(s)?, + Self::Json => serde_json::to_string_pretty(s).context("failed to serialize JSON")?, + Self::Toml => toml_edit::ser::to_string_pretty(s).context("failed to serialize TOML")?, }; Ok(contents) } diff --git a/crates/tauri-cli/src/acl/permission/add.rs b/crates/tauri-cli/src/acl/permission/add.rs index 7a6bdf42e..5b7260732 100644 --- a/crates/tauri-cli/src/acl/permission/add.rs +++ b/crates/tauri-cli/src/acl/permission/add.rs @@ -7,6 +7,7 @@ use std::path::Path; use clap::Parser; use crate::{ + error::{Context, ErrorExt}, helpers::{app_paths::resolve_tauri_dir, prompts}, Result, }; @@ -100,7 +101,9 @@ impl TomlOrJson { fn to_string(&self) -> Result { Ok(match self { TomlOrJson::Toml(t) => t.to_string(), - TomlOrJson::Json(j) => serde_json::to_string_pretty(&j)?, + TomlOrJson::Json(j) => { + serde_json::to_string_pretty(&j).context("failed to serialize JSON")? + } }) } } @@ -131,12 +134,12 @@ pub struct Options { pub fn command(options: Options) -> Result<()> { let dir = match resolve_tauri_dir() { Some(t) => t, - None => std::env::current_dir()?, + None => std::env::current_dir().context("failed to resolve current directory")?, }; let capabilities_dir = dir.join("capabilities"); if !capabilities_dir.exists() { - anyhow::bail!( + crate::error::bail!( "Couldn't find capabilities directory at {}", dunce::simplified(&capabilities_dir).display() ); @@ -148,7 +151,11 @@ pub fn command(options: Options) -> Result<()> { .split_once(':') .and_then(|(plugin, _permission)| known_plugins.get(&plugin)); - let capabilities_iter = std::fs::read_dir(&capabilities_dir)? + let capabilities_iter = std::fs::read_dir(&capabilities_dir) + .fs_context( + "failed to read capabilities directory", + capabilities_dir.clone(), + )? .flatten() .filter(|e| e.file_type().map(|e| e.is_file()).unwrap_or_default()) .filter_map(|e| { @@ -240,7 +247,7 @@ pub fn command(options: Options) -> Result<()> { )?; if selections.is_empty() { - anyhow::bail!("You did not select any capabilities to update"); + crate::error::bail!("You did not select any capabilities to update"); } selections @@ -252,7 +259,7 @@ pub fn command(options: Options) -> Result<()> { }; if capabilities.is_empty() { - anyhow::bail!("Could not find a capability to update"); + crate::error::bail!("Could not find a capability to update"); } for (capability, path) in &mut capabilities { @@ -265,7 +272,8 @@ pub fn command(options: Options) -> Result<()> { ); } else { capability.insert_permission(options.identifier.clone()); - std::fs::write(&*path, capability.to_string()?)?; + std::fs::write(&*path, capability.to_string()?) + .fs_context("failed to write capability file", path.clone())?; log::info!(action = "Added"; "permission `{}` to `{}` at {}", options.identifier, capability.identifier(), dunce::simplified(path).display()); } } diff --git a/crates/tauri-cli/src/acl/permission/ls.rs b/crates/tauri-cli/src/acl/permission/ls.rs index 69611e3f9..7cb7c3d1b 100644 --- a/crates/tauri-cli/src/acl/permission/ls.rs +++ b/crates/tauri-cli/src/acl/permission/ls.rs @@ -4,7 +4,11 @@ use clap::Parser; -use crate::{helpers::app_paths::tauri_dir, Result}; +use crate::{ + error::{Context, ErrorExt}, + helpers::app_paths::tauri_dir, + Result, +}; use colored::Colorize; use tauri_utils::acl::{manifest::Manifest, APP_ACL_KEY}; @@ -29,8 +33,10 @@ pub fn command(options: Options) -> Result<()> { .join("acl-manifests.json"); if acl_manifests_path.exists() { - let plugin_manifest_json = read_to_string(&acl_manifests_path)?; - let acl = serde_json::from_str::>(&plugin_manifest_json)?; + let plugin_manifest_json = read_to_string(&acl_manifests_path) + .fs_context("failed to read plugin manifest", acl_manifests_path.clone())?; + let acl = serde_json::from_str::>(&plugin_manifest_json) + .context("failed to parse plugin manifest as JSON")?; for (key, manifest) in acl { if options @@ -147,6 +153,6 @@ pub fn command(options: Options) -> Result<()> { Ok(()) } else { - anyhow::bail!("permission file not found, please build your application once first") + crate::error::bail!("permission file not found, please build your application once first") } } diff --git a/crates/tauri-cli/src/acl/permission/new.rs b/crates/tauri-cli/src/acl/permission/new.rs index 5c366a251..cb17fa357 100644 --- a/crates/tauri-cli/src/acl/permission/new.rs +++ b/crates/tauri-cli/src/acl/permission/new.rs @@ -8,6 +8,7 @@ use clap::Parser; use crate::{ acl::FileFormat, + error::{Context, ErrorExt}, helpers::{app_paths::resolve_tauri_dir, prompts}, Result, }; @@ -67,11 +68,13 @@ pub fn command(options: Options) -> Result<()> { }; let path = match options.out { - Some(o) => o.canonicalize()?, + Some(o) => o + .canonicalize() + .fs_context("failed to canonicalize permission file path", o.clone())?, None => { let dir = match resolve_tauri_dir() { Some(t) => t, - None => std::env::current_dir()?, + None => std::env::current_dir().context("failed to resolve current directory")?, }; let permissions_dir = dir.join("permissions"); permissions_dir.join(format!( @@ -89,24 +92,31 @@ pub fn command(options: Options) -> Result<()> { ); let overwrite = prompts::confirm(&format!("{msg}, overwrite?"), Some(false))?; if overwrite { - std::fs::remove_file(&path)?; + std::fs::remove_file(&path).fs_context("failed to remove permission file", path.clone())?; } else { - anyhow::bail!(msg); + crate::error::bail!(msg); } } if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; + std::fs::create_dir_all(parent).fs_context( + "failed to create permission directory", + parent.to_path_buf(), + )?; } std::fs::write( &path, - options.format.serialize(&PermissionFile { - default: None, - set: Vec::new(), - permission: vec![permission], - })?, - )?; + options + .format + .serialize(&PermissionFile { + default: None, + set: Vec::new(), + permission: vec![permission], + }) + .context("failed to serialize permission")?, + ) + .fs_context("failed to write permission file", path.clone())?; log::info!(action = "Created"; "permission at {}", dunce::simplified(&path).display()); diff --git a/crates/tauri-cli/src/acl/permission/rm.rs b/crates/tauri-cli/src/acl/permission/rm.rs index 2ad232bfa..52f770f85 100644 --- a/crates/tauri-cli/src/acl/permission/rm.rs +++ b/crates/tauri-cli/src/acl/permission/rm.rs @@ -7,11 +7,21 @@ use std::path::Path; use clap::Parser; use tauri_utils::acl::{manifest::PermissionFile, PERMISSION_SCHEMA_FILE_NAME}; -use crate::{acl::FileFormat, helpers::app_paths::resolve_tauri_dir, Result}; +use crate::{ + acl::FileFormat, + error::{Context, ErrorExt}, + helpers::app_paths::resolve_tauri_dir, + Result, +}; fn rm_permission_files(identifier: &str, dir: &Path) -> Result<()> { - for entry in std::fs::read_dir(dir)?.flatten() { - let file_type = entry.file_type()?; + for entry in std::fs::read_dir(dir) + .fs_context("failed to read permissions directory", dir.to_path_buf())? + .flatten() + { + let file_type = entry + .file_type() + .fs_context("failed to get permission file type", entry.path())?; let path = entry.path(); if file_type.is_dir() { rm_permission_files(identifier, &path)?; @@ -27,12 +37,21 @@ fn rm_permission_files(identifier: &str, dir: &Path) -> Result<()> { let (mut permission_file, format): (PermissionFile, FileFormat) = match path.extension().and_then(|o| o.to_str()) { Some("toml") => { - let content = std::fs::read_to_string(&path)?; - (toml::from_str(&content)?, FileFormat::Toml) + let content = std::fs::read_to_string(&path) + .fs_context("failed to read permission file", path.clone())?; + ( + toml::from_str(&content).context("failed to deserialize permission file")?, + FileFormat::Toml, + ) } Some("json") => { - let content = std::fs::read(&path)?; - (serde_json::from_slice(&content)?, FileFormat::Json) + let content = + std::fs::read(&path).fs_context("failed to read permission file", path.clone())?; + ( + serde_json::from_slice(&content) + .context("failed to parse permission file as JSON")?, + FileFormat::Json, + ) } _ => { continue; @@ -63,10 +82,16 @@ fn rm_permission_files(identifier: &str, dir: &Path) -> Result<()> { && permission_file.set.is_empty() && permission_file.permission.is_empty() { - std::fs::remove_file(&path)?; + std::fs::remove_file(&path).fs_context("failed to remove permission file", path.clone())?; log::info!(action = "Removed"; "file {}", dunce::simplified(&path).display()); } else if updated { - std::fs::write(&path, format.serialize(&permission_file)?)?; + std::fs::write( + &path, + format + .serialize(&permission_file) + .context("failed to serialize permission")?, + ) + .fs_context("failed to write permission file", path.clone())?; log::info!(action = "Removed"; "permission {identifier} from {}", dunce::simplified(&path).display()); } } @@ -76,13 +101,19 @@ fn rm_permission_files(identifier: &str, dir: &Path) -> Result<()> { } fn rm_permission_from_capabilities(identifier: &str, dir: &Path) -> Result<()> { - for entry in std::fs::read_dir(dir)?.flatten() { - let file_type = entry.file_type()?; + for entry in std::fs::read_dir(dir) + .fs_context("failed to read capabilities directory", dir.to_path_buf())? + .flatten() + { + let file_type = entry + .file_type() + .fs_context("failed to get capability file type", entry.path())?; if file_type.is_file() { let path = entry.path(); match path.extension().and_then(|o| o.to_str()) { Some("toml") => { - let content = std::fs::read_to_string(&path)?; + let content = std::fs::read_to_string(&path) + .fs_context("failed to read capability file", path.clone())?; if let Ok(mut value) = content.parse::() { if let Some(permissions) = value.get_mut("permissions").and_then(|p| p.as_array_mut()) { let prev_len = permissions.len(); @@ -98,14 +129,16 @@ fn rm_permission_from_capabilities(identifier: &str, dir: &Path) -> Result<()> { _ => false, }); if prev_len != permissions.len() { - std::fs::write(&path, value.to_string())?; + std::fs::write(&path, value.to_string()) + .fs_context("failed to write capability file", path.clone())?; log::info!(action = "Removed"; "permission from capability at {}", dunce::simplified(&path).display()); } } } } Some("json") => { - let content = std::fs::read(&path)?; + let content = + std::fs::read(&path).fs_context("failed to read capability file", path.clone())?; if let Ok(mut value) = serde_json::from_slice::(&content) { if let Some(permissions) = value.get_mut("permissions").and_then(|p| p.as_array_mut()) { let prev_len = permissions.len(); @@ -121,7 +154,12 @@ fn rm_permission_from_capabilities(identifier: &str, dir: &Path) -> Result<()> { _ => false, }); if prev_len != permissions.len() { - std::fs::write(&path, serde_json::to_vec_pretty(&value)?)?; + std::fs::write( + &path, + serde_json::to_vec_pretty(&value) + .context("failed to serialize capability JSON")?, + ) + .fs_context("failed to write capability file", path.clone())?; log::info!(action = "Removed"; "permission from capability at {}", dunce::simplified(&path).display()); } } @@ -152,7 +190,9 @@ pub struct Options { } pub fn command(options: Options) -> Result<()> { - let permissions_dir = std::env::current_dir()?.join("permissions"); + let permissions_dir = std::env::current_dir() + .context("failed to resolve current directory")? + .join("permissions"); if permissions_dir.exists() { rm_permission_files(&options.identifier, &permissions_dir)?; } diff --git a/crates/tauri-cli/src/add.rs b/crates/tauri-cli/src/add.rs index 302bd026c..8a45c0880 100644 --- a/crates/tauri-cli/src/add.rs +++ b/crates/tauri-cli/src/add.rs @@ -8,6 +8,7 @@ use regex::Regex; use crate::{ acl, + error::ErrorExt, helpers::{ app_paths::{resolve_frontend_dir, tauri_dir}, cargo, @@ -64,7 +65,7 @@ pub fn run(options: Options) -> Result<()> { }; if !is_known && (options.tag.is_some() || options.rev.is_some() || options.branch.is_some()) { - anyhow::bail!( + crate::error::bail!( "Git options --tag, --rev and --branch can only be used with official Tauri plugins" ); } @@ -114,7 +115,7 @@ pub fn run(options: Options) -> Result<()> { format!("tauri-apps/tauri-plugin-{plugin}#{branch}") } (None, None, None, None) => npm_name, - _ => anyhow::bail!("Only one of --tag, --rev and --branch can be specified"), + _ => crate::error::bail!("Only one of --tag, --rev and --branch can be specified"), }; manager.install(&[npm_spec], tauri_dir)?; } @@ -141,9 +142,10 @@ pub fn run(options: Options) -> Result<()> { }; let plugin_init = format!(".plugin(tauri_plugin_{plugin_snake_case}::{plugin_init_fn})"); - let re = Regex::new(r"(tauri\s*::\s*Builder\s*::\s*default\(\))(\s*)")?; + let re = Regex::new(r"(tauri\s*::\s*Builder\s*::\s*default\(\))(\s*)").unwrap(); for file in [tauri_dir.join("src/main.rs"), tauri_dir.join("src/lib.rs")] { - let contents = std::fs::read_to_string(&file)?; + let contents = + std::fs::read_to_string(&file).fs_context("failed to read Rust entry point", file.clone())?; if contents.contains(&plugin_init) { log::info!( @@ -157,7 +159,7 @@ pub fn run(options: Options) -> Result<()> { let out = re.replace(&contents, format!("$1$2{plugin_init}$2")); log::info!("Adding plugin to {}", file.display()); - std::fs::write(file, out.as_bytes())?; + std::fs::write(&file, out.as_bytes()).fs_context("failed to write plugin init code", file)?; if !options.no_fmt { // reformat code with rustfmt diff --git a/crates/tauri-cli/src/build.rs b/crates/tauri-cli/src/build.rs index 288518d87..12accb16f 100644 --- a/crates/tauri-cli/src/build.rs +++ b/crates/tauri-cli/src/build.rs @@ -4,6 +4,7 @@ use crate::{ bundle::BundleFormat, + error::{Context, ErrorExt}, helpers::{ self, app_paths::{frontend_dir, tauri_dir}, @@ -13,7 +14,6 @@ use crate::{ interface::{rust::get_cargo_target_dir, AppInterface, Interface}, ConfigValue, Result, }; -use anyhow::Context; use clap::{ArgAction, Parser}; use std::env::set_current_dir; use tauri_utils::config::RunnerConfig; @@ -160,7 +160,7 @@ pub fn setup( } } - set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?; + set_current_dir(tauri_path).context("failed to set current directory")?; let config_guard = config.lock().unwrap(); let config_ = config_guard.as_ref().unwrap(); @@ -170,7 +170,7 @@ pub fn setup( .unwrap_or_else(|| "tauri.conf.json".into()); if config_.identifier == "com.tauri.dev" { - anyhow::bail!( + crate::error::bail!( "You must change the bundle identifier in `{bundle_identifier_source} identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.", ); } @@ -180,7 +180,7 @@ pub fn setup( .chars() .any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.')) { - anyhow::bail!( + crate::error::bail!( "The bundle identifier \"{}\" set in `{} identifier`. The bundle identifier string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.).", config_.identifier, bundle_identifier_source @@ -206,15 +206,20 @@ pub fn setup( .and_then(|p| p.canonicalize().ok()) .map(|p| p.join(web_asset_path.file_name().unwrap())) .unwrap_or_else(|| std::env::current_dir().unwrap().join(web_asset_path)); - return Err(anyhow::anyhow!( - "Unable to find your web assets, did you forget to build your web app? Your frontendDist is set to \"{}\" (which is `{}`).", - web_asset_path.display(), absolute_path.display(), - )); + crate::error::bail!( + "Unable to find your web assets, did you forget to build your web app? Your frontendDist is set to \"{}\" (which is `{}`).", + web_asset_path.display(), absolute_path.display(), + ); } - if web_asset_path.canonicalize()?.file_name() == Some(std::ffi::OsStr::new("src-tauri")) { - return Err(anyhow::anyhow!( + if web_asset_path + .canonicalize() + .fs_context("failed to canonicalize path", web_asset_path.to_path_buf())? + .file_name() + == Some(std::ffi::OsStr::new("src-tauri")) + { + crate::error::bail!( "The configured frontendDist is the `src-tauri` folder. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > frontendDist`.", - )); + ); } // Issue #13287 - Allow the use of target dir inside frontendDist/distDir @@ -238,11 +243,11 @@ pub fn setup( } if !out_folders.is_empty() { - return Err(anyhow::anyhow!( + crate::error::bail!( "The configured frontendDist includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > frontendDist`.", out_folders, if out_folders.len() == 1 { "folder" } else { "folders" } - )); + ); } } diff --git a/crates/tauri-cli/src/bundle.rs b/crates/tauri-cli/src/bundle.rs index 23999082f..e48aee197 100644 --- a/crates/tauri-cli/src/bundle.rs +++ b/crates/tauri-cli/src/bundle.rs @@ -8,12 +8,12 @@ use std::{ sync::OnceLock, }; -use anyhow::Context; use clap::{builder::PossibleValue, ArgAction, Parser, ValueEnum}; use tauri_bundler::PackageType; use tauri_utils::platform::Target; use crate::{ + error::{Context, ErrorExt}, helpers::{ self, app_paths::tauri_dir, @@ -28,11 +28,11 @@ use crate::{ pub struct BundleFormat(PackageType); impl FromStr for BundleFormat { - type Err = anyhow::Error; + type Err = crate::Error; fn from_str(s: &str) -> crate::Result { PackageType::from_short_name(s) .map(Self) - .ok_or_else(|| anyhow::anyhow!("unknown bundle format {s}")) + .with_context(|| format!("unknown bundle format {s}")) } } @@ -139,8 +139,7 @@ pub fn command(options: Options, verbosity: u8) -> crate::Result<()> { )?; let tauri_path = tauri_dir(); - std::env::set_current_dir(tauri_path) - .with_context(|| "failed to change current working directory")?; + std::env::set_current_dir(tauri_path).context("failed to set current directory")?; let config_guard = config.lock().unwrap(); let config_ = config_guard.as_ref().unwrap(); @@ -214,12 +213,7 @@ pub fn bundle( _ => log::Level::Trace, }); - let bundles = tauri_bundler::bundle_project(&settings) - .map_err(|e| match e { - tauri_bundler::Error::BundlerError(e) => e, - e => anyhow::anyhow!("{e:#}"), - }) - .with_context(|| "failed to bundle project")?; + let bundles = tauri_bundler::bundle_project(&settings).map_err(Box::new)?; sign_updaters(settings, bundles, ci)?; @@ -260,7 +254,8 @@ fn sign_updaters( // check if pubkey points to a file... let maybe_path = Path::new(pubkey); let pubkey = if maybe_path.exists() { - std::fs::read_to_string(maybe_path)? + std::fs::read_to_string(maybe_path) + .fs_context("failed to read pubkey from file", maybe_path.to_path_buf())? } else { pubkey.to_string() }; @@ -272,12 +267,15 @@ fn sign_updaters( // get the private key let private_key = std::env::var("TAURI_SIGNING_PRIVATE_KEY") - .map_err(|_| anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable."))?; + .ok() + .context("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable.")?; // check if private_key points to a file... let maybe_path = Path::new(&private_key); let private_key = if maybe_path.exists() { - std::fs::read_to_string(maybe_path) - .with_context(|| format!("faild to read {}", maybe_path.display()))? + std::fs::read_to_string(maybe_path).fs_context( + "failed to read private key from file", + maybe_path.to_path_buf(), + )? } else { private_key }; @@ -315,11 +313,11 @@ fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> { }; let mut printable_paths = String::new(); for path in output_paths { - writeln!( + let _ = writeln!( printable_paths, " {}", tauri_utils::display_path(path) - )?; + ); } log::info!( action = "Finished"; "{finished_bundles} {pluralised} at:\n{printable_paths}"); } diff --git a/crates/tauri-cli/src/completions.rs b/crates/tauri-cli/src/completions.rs index 593052568..d49600505 100644 --- a/crates/tauri-cli/src/completions.rs +++ b/crates/tauri-cli/src/completions.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::Result; -use anyhow::Context; +use crate::{error::ErrorExt, Result}; use clap::{Command, Parser}; use clap_complete::{generate, Shell}; @@ -95,7 +94,7 @@ pub fn command(options: Options, cmd: Command) -> Result<()> { let completions = get_completions(options.shell, cmd)?; if let Some(output) = options.output { - write(output, completions).context("failed to write to output path")?; + write(&output, completions).fs_context("failed to write to completions", output)?; } else { print!("{completions}"); } diff --git a/crates/tauri-cli/src/dev.rs b/crates/tauri-cli/src/dev.rs index 5fabb4a17..9179e9cfd 100644 --- a/crates/tauri-cli/src/dev.rs +++ b/crates/tauri-cli/src/dev.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT use crate::{ + error::{Context, ErrorExt}, helpers::{ app_paths::{frontend_dir, tauri_dir}, command_env, @@ -12,10 +13,9 @@ use crate::{ }, info::plugins::check_mismatched_packages, interface::{AppInterface, ExitReason, Interface}, - CommandExt, ConfigValue, Result, + CommandExt, ConfigValue, Error, Result, }; -use anyhow::{bail, Context}; use clap::{ArgAction, Parser}; use shared_child::SharedChild; use tauri_utils::{config::RunnerConfig, platform::Target}; @@ -143,7 +143,7 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand } }); - set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?; + set_current_dir(tauri_path).context("failed to set current directory")?; if let Some(before_dev) = config .lock() @@ -190,15 +190,15 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand }; if wait { - let status = command.piped().with_context(|| { - format!( - "failed to run `{}` with `{}`", - before_dev, + let status = command.piped().map_err(|error| Error::CommandFailed { + command: format!( + "`{before_dev}` with `{}`", if cfg!(windows) { "cmd /S /C" } else { "sh -c" } - ) + ), + error, })?; if !status.success() { - bail!( + crate::error::bail!( "beforeDevCommand `{}` failed with exit code {}", before_dev, status.code().unwrap_or_default() @@ -206,8 +206,8 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand } } else { command.stdin(Stdio::piped()); - command.stdout(os_pipe::dup_stdout()?); - command.stderr(os_pipe::dup_stderr()?); + command.stdout(os_pipe::dup_stdout().unwrap()); + command.stderr(os_pipe::dup_stderr().unwrap()); let child = SharedChild::spawn(&mut command) .unwrap_or_else(|_| panic!("failed to run `{before_dev}`")); @@ -278,13 +278,16 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand if !options.no_dev_server && dev_url.is_none() { if let Some(FrontendDist::Directory(path)) = &frontend_dist { if path.exists() { - let path = path.canonicalize()?; + let path = path + .canonicalize() + .fs_context("failed to canonicalize path", path.to_path_buf())?; let ip = options .host .unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1).into()); - let server_url = builtin_dev_server::start(path, ip, options.port)?; + let server_url = builtin_dev_server::start(path, ip, options.port) + .context("failed to start builtin dev server")?; let server_url = format!("http://{server_url}"); dev_url = Some(server_url.parse().unwrap()); @@ -312,7 +315,7 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand let addrs = match host { url::Host::Domain(domain) => { use std::net::ToSocketAddrs; - addrs = (domain, port).to_socket_addrs()?; + addrs = (domain, port).to_socket_addrs().unwrap(); addrs.as_slice() } url::Host::Ipv4(ip) => { diff --git a/crates/tauri-cli/src/dev/builtin_dev_server.rs b/crates/tauri-cli/src/dev/builtin_dev_server.rs index b570b3bfe..067a64f67 100644 --- a/crates/tauri-cli/src/dev/builtin_dev_server.rs +++ b/crates/tauri-cli/src/dev/builtin_dev_server.rs @@ -18,6 +18,8 @@ use std::{ use tauri_utils::mime_type::MimeType; use tokio::sync::broadcast::{channel, Sender}; +use crate::error::ErrorExt; + const RELOAD_SCRIPT: &str = include_str!("./auto-reload.js"); #[derive(Clone)] @@ -29,7 +31,8 @@ struct ServerState { pub fn start>(dir: P, ip: IpAddr, port: Option) -> crate::Result { let dir = dir.as_ref(); - let dir = dunce::canonicalize(dir)?; + let dir = + dunce::canonicalize(dir).fs_context("failed to canonicalize path", dir.to_path_buf())?; // bind port and tcp listener let auto_port = port.is_none(); @@ -37,12 +40,12 @@ pub fn start>(dir: P, ip: IpAddr, port: Option) -> crate::Re let (tcp_listener, address) = loop { let address = SocketAddr::new(ip, port); if let Ok(tcp) = std::net::TcpListener::bind(address) { - tcp.set_nonblocking(true)?; + tcp.set_nonblocking(true).unwrap(); break (tcp, address); } if !auto_port { - anyhow::bail!("Couldn't bind to {port} on {ip}"); + crate::error::bail!("Couldn't bind to {port} on {ip}"); } port += 1; @@ -152,11 +155,11 @@ fn inject_address(html_bytes: Vec, address: &SocketAddr) -> Vec { } fn fs_read_scoped(path: PathBuf, scope: &Path) -> crate::Result> { - let path = dunce::canonicalize(path)?; + let path = dunce::canonicalize(&path).fs_context("failed to canonicalize path", path.clone())?; if path.starts_with(scope) { - std::fs::read(path).map_err(Into::into) + std::fs::read(&path).fs_context("failed to read file", path.clone()) } else { - anyhow::bail!("forbidden path") + crate::error::bail!("forbidden path") } } diff --git a/crates/tauri-cli/src/error.rs b/crates/tauri-cli/src/error.rs new file mode 100644 index 000000000..33d1dfdb6 --- /dev/null +++ b/crates/tauri-cli/src/error.rs @@ -0,0 +1,105 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{fmt::Display, path::PathBuf}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{0}: {1}")] + Context(String, Box), + #[error("{0}")] + GenericError(String), + #[error("failed to bundle project {0}")] + Bundler(#[from] Box), + #[error("{context} {path}: {error}")] + Fs { + context: &'static str, + path: PathBuf, + error: std::io::Error, + }, + #[error("failed to run command {command}: {error}")] + CommandFailed { + command: String, + error: std::io::Error, + }, + #[cfg(target_os = "macos")] + #[error(transparent)] + MacosSign(#[from] Box), +} + +/// Convenient type alias of Result type. +pub type Result = std::result::Result; + +pub trait Context { + // Required methods + fn context(self, context: C) -> Result + where + C: Display + Send + Sync + 'static; + fn with_context(self, f: F) -> Result + where + C: Display + Send + Sync + 'static, + F: FnOnce() -> C; +} + +impl Context for std::result::Result { + fn context(self, context: C) -> Result + where + C: Display + Send + Sync + 'static, + { + self.map_err(|e| Error::Context(context.to_string(), Box::new(e))) + } + + fn with_context(self, f: F) -> Result + where + C: Display + Send + Sync + 'static, + F: FnOnce() -> C, + { + self.map_err(|e| Error::Context(f().to_string(), Box::new(e))) + } +} + +impl Context for Option { + fn context(self, context: C) -> Result + where + C: Display + Send + Sync + 'static, + { + self.ok_or_else(|| Error::GenericError(context.to_string())) + } + + fn with_context(self, f: F) -> Result + where + C: Display + Send + Sync + 'static, + F: FnOnce() -> C, + { + self.ok_or_else(|| Error::GenericError(f().to_string())) + } +} + +pub trait ErrorExt { + fn fs_context(self, context: &'static str, path: impl Into) -> Result; +} + +impl ErrorExt for std::result::Result { + fn fs_context(self, context: &'static str, path: impl Into) -> Result { + self.map_err(|error| Error::Fs { + context, + path: path.into(), + error, + }) + } +} + +macro_rules! bail { + ($msg:literal $(,)?) => { + return Err(crate::Error::GenericError($msg.into())) + }; + ($err:expr $(,)?) => { + return Err(crate::Error::GenericError($err)) + }; + ($fmt:expr, $($arg:tt)*) => { + return Err(crate::Error::GenericError(format!($fmt, $($arg)*))) + }; +} + +pub(crate) use bail; diff --git a/crates/tauri-cli/src/helpers/cargo.rs b/crates/tauri-cli/src/helpers/cargo.rs index ffa4493b7..9f990f79a 100644 --- a/crates/tauri-cli/src/helpers/cargo.rs +++ b/crates/tauri-cli/src/helpers/cargo.rs @@ -4,7 +4,7 @@ use std::process::Command; -use anyhow::Context; +use crate::Error; #[derive(Debug, Default, Clone, Copy)] pub struct CargoInstallOptions<'a> { @@ -41,7 +41,7 @@ pub fn install_one(options: CargoInstallOptions) -> crate::Result<()> { cargo.args(["--branch", branch]); } (None, None, None) => {} - _ => anyhow::bail!("Only one of --tag, --rev and --branch can be specified"), + _ => crate::error::bail!("Only one of --tag, --rev and --branch can be specified"), }; } @@ -54,9 +54,12 @@ pub fn install_one(options: CargoInstallOptions) -> crate::Result<()> { } log::info!("Installing Cargo dependency \"{}\"...", options.name); - let status = cargo.status().context("failed to run `cargo add`")?; + let status = cargo.status().map_err(|error| Error::CommandFailed { + command: "cargo add".to_string(), + error, + })?; if !status.success() { - anyhow::bail!("Failed to install Cargo dependency"); + crate::error::bail!("Failed to install Cargo dependency"); } Ok(()) @@ -84,9 +87,12 @@ pub fn uninstall_one(options: CargoUninstallOptions) -> crate::Result<()> { } log::info!("Uninstalling Cargo dependency \"{}\"...", options.name); - let status = cargo.status().context("failed to run `cargo remove`")?; + let status = cargo.status().map_err(|error| Error::CommandFailed { + command: "cargo remove".to_string(), + error, + })?; if !status.success() { - anyhow::bail!("Failed to remove Cargo dependency"); + crate::error::bail!("Failed to remove Cargo dependency"); } Ok(()) diff --git a/crates/tauri-cli/src/helpers/config.rs b/crates/tauri-cli/src/helpers/config.rs index c8ae52f74..f21ce4a4b 100644 --- a/crates/tauri-cli/src/helpers/config.rs +++ b/crates/tauri-cli/src/helpers/config.rs @@ -17,6 +17,8 @@ use std::{ sync::{Arc, Mutex, OnceLock}, }; +use crate::error::Context; + pub const MERGE_CONFIG_EXTENSION_NAME: &str = "--config"; pub struct ConfigMetadata { @@ -156,7 +158,8 @@ fn get_internal( let tauri_dir = super::app_paths::tauri_dir(); let (mut config, config_path) = - tauri_utils::config::parse::parse_value(target, tauri_dir.join("tauri.conf.json"))?; + tauri_utils::config::parse::parse_value(target, tauri_dir.join("tauri.conf.json")) + .context("failed to parse config")?; let config_file_name = config_path.file_name().unwrap().to_string_lossy(); let mut extensions = HashMap::new(); @@ -167,7 +170,8 @@ fn get_internal( .map(ToString::to_string); if let Some((platform_config, config_path)) = - tauri_utils::config::parse::read_platform(target, tauri_dir)? + tauri_utils::config::parse::read_platform(target, tauri_dir) + .context("failed to parse platform config")? { merge(&mut config, &platform_config); extensions.insert( @@ -191,7 +195,8 @@ fn get_internal( if config_path.extension() == Some(OsStr::new("json")) || config_path.extension() == Some(OsStr::new("json5")) { - let schema: JsonValue = serde_json::from_str(include_str!("../../config.schema.json"))?; + let schema: JsonValue = serde_json::from_str(include_str!("../../config.schema.json")) + .context("failed to parse config schema")?; let validator = jsonschema::validator_for(&schema).expect("Invalid schema"); let mut errors = validator.iter_errors(&config).peekable(); if errors.peek().is_some() { @@ -211,11 +216,11 @@ fn get_internal( // the `Config` deserializer for `package > version` can resolve the version from a path relative to the config path // so we actually need to change the current working directory here - let current_dir = current_dir()?; - set_current_dir(config_path.parent().unwrap())?; - let config: Config = serde_json::from_value(config)?; + let current_dir = current_dir().context("failed to resolve current directory")?; + set_current_dir(config_path.parent().unwrap()).context("failed to set current directory")?; + let config: Config = serde_json::from_value(config).context("failed to parse config")?; // revert to previous working directory - set_current_dir(current_dir)?; + set_current_dir(current_dir).context("failed to set current directory")?; for (plugin, conf) in &config.plugins.0 { set_var( @@ -223,7 +228,7 @@ fn get_internal( "TAURI_{}_PLUGIN_CONFIG", plugin.to_uppercase().replace('-', "_") ), - serde_json::to_string(&conf)?, + serde_json::to_string(&conf).context("failed to serialize config")?, ); } @@ -254,7 +259,7 @@ pub fn reload(merge_configs: &[&serde_json::Value]) -> crate::Result crate::Result Result { acquire(msg, path, &|| try_lock_exclusive(&f), &|| { @@ -203,16 +210,18 @@ fn acquire( Err(e) => { if !error_contended(&e) { - let e = anyhow::Error::from(e); - let cx = format!("failed to lock file: {}", path.display()); - return Err(e.context(cx)); + return Err(Error::Fs { + context: "failed to lock file", + path: path.to_path_buf(), + error: e, + }); } } } let msg = format!("waiting for file lock on {msg}"); log::info!(action = "Blocking"; "{}", &msg); - lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?; + lock_block().fs_context("failed to lock file", path.to_path_buf())?; return Ok(()); #[cfg(all(target_os = "linux", not(target_env = "musl")))] diff --git a/crates/tauri-cli/src/helpers/fs.rs b/crates/tauri-cli/src/helpers/fs.rs index dd7491db1..776441f5a 100644 --- a/crates/tauri-cli/src/helpers/fs.rs +++ b/crates/tauri-cli/src/helpers/fs.rs @@ -2,20 +2,29 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use anyhow::Result; +use crate::{error::ErrorExt, Error}; use std::path::Path; -pub fn copy_file(from: impl AsRef, to: impl AsRef) -> Result<()> { +pub fn copy_file(from: impl AsRef, to: impl AsRef) -> crate::Result<()> { let from = from.as_ref(); let to = to.as_ref(); if !from.exists() { - return Err(anyhow::anyhow!("{:?} does not exist", from)); + Err(Error::Fs { + context: "failed to copy file", + path: from.to_path_buf(), + error: std::io::Error::new(std::io::ErrorKind::NotFound, "source does not exist"), + })?; } if !from.is_file() { - return Err(anyhow::anyhow!("{:?} is not a file", from)); + Err(Error::Fs { + context: "failed to copy file", + path: from.to_path_buf(), + error: std::io::Error::other("not a file"), + })?; } let dest_dir = to.parent().expect("No data in parent"); - std::fs::create_dir_all(dest_dir)?; - std::fs::copy(from, to)?; + std::fs::create_dir_all(dest_dir) + .fs_context("failed to create directory", dest_dir.to_path_buf())?; + std::fs::copy(from, to).fs_context("failed to copy file", from.to_path_buf())?; Ok(()) } diff --git a/crates/tauri-cli/src/helpers/mod.rs b/crates/tauri-cli/src/helpers/mod.rs index 08f03f037..238128686 100644 --- a/crates/tauri-cli/src/helpers/mod.rs +++ b/crates/tauri-cli/src/helpers/mod.rs @@ -24,9 +24,10 @@ use std::{ process::Command, }; -use anyhow::Context; use tauri_utils::config::HookCommand; +#[cfg(not(target_os = "windows"))] +use crate::Error; use crate::{ interface::{AppInterface, Interface}, CommandExt, @@ -98,7 +99,10 @@ pub fn run_hook( .current_dir(cwd) .envs(env) .piped() - .with_context(|| format!("failed to run `{script}` with `cmd /C`"))?; + .map_err(|error| crate::error::Error::CommandFailed { + command: script.clone(), + error, + })?; #[cfg(not(target_os = "windows"))] let status = Command::new("sh") .arg("-c") @@ -106,10 +110,13 @@ pub fn run_hook( .current_dir(cwd) .envs(env) .piped() - .with_context(|| format!("failed to run `{script}` with `sh -c`"))?; + .map_err(|error| Error::CommandFailed { + command: script.clone(), + error, + })?; if !status.success() { - anyhow::bail!( + crate::error::bail!( "{} `{}` failed with exit code {}", name, script, @@ -123,6 +130,7 @@ pub fn run_hook( #[cfg(target_os = "macos")] pub fn strip_semver_prerelease_tag(version: &mut semver::Version) -> crate::Result<()> { + use crate::error::Context; if !version.pre.is_empty() { if let Some((_prerelease_tag, number)) = version.pre.as_str().to_string().split_once('.') { version.pre = semver::Prerelease::EMPTY; @@ -134,7 +142,11 @@ pub fn strip_semver_prerelease_tag(version: &mut semver::Version) -> crate::Resu format!(".{}", version.build.as_str()) } )) - .with_context(|| format!("bundle version {number:?} prerelease is invalid"))?; + .with_context(|| { + format!( + "failed to parse {version} as semver: bundle version {number:?} prerelease is invalid" + ) + })?; } } diff --git a/crates/tauri-cli/src/helpers/npm.rs b/crates/tauri-cli/src/helpers/npm.rs index fe162cdb3..a39c5ffd0 100644 --- a/crates/tauri-cli/src/helpers/npm.rs +++ b/crates/tauri-cli/src/helpers/npm.rs @@ -2,10 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use anyhow::Context; use serde::Deserialize; -use crate::helpers::cross_command; +use crate::{ + error::{Context, Error}, + helpers::cross_command, +}; use std::{collections::HashMap, fmt::Display, path::Path, process::Command}; pub fn manager_version(package_manager: &str) -> Option { @@ -151,10 +153,13 @@ impl PackageManager { let status = command .current_dir(frontend_dir) .status() - .with_context(|| format!("failed to run {self}"))?; + .map_err(|error| Error::CommandFailed { + command: format!("failed to run {self}"), + error, + })?; if !status.success() { - anyhow::bail!("Failed to install NPM {dependencies_str}"); + crate::error::bail!("Failed to install NPM {dependencies_str}"); } Ok(()) @@ -189,10 +194,13 @@ impl PackageManager { .args(dependencies) .current_dir(frontend_dir) .status() - .with_context(|| format!("failed to run {self}"))?; + .map_err(|error| Error::CommandFailed { + command: format!("failed to run {self}"), + error, + })?; if !status.success() { - anyhow::bail!("Failed to remove NPM {dependencies_str}"); + crate::error::bail!("Failed to remove NPM {dependencies_str}"); } Ok(()) @@ -211,7 +219,11 @@ impl PackageManager { .arg(name) .args(["--depth", "0"]) .current_dir(frontend_dir) - .output()?, + .output() + .map_err(|error| Error::CommandFailed { + command: "yarn list --pattern".to_string(), + error, + })?, None, ), PackageManager::YarnBerry => ( @@ -220,7 +232,11 @@ impl PackageManager { .arg(name) .arg("--json") .current_dir(frontend_dir) - .output()?, + .output() + .map_err(|error| Error::CommandFailed { + command: "yarn info --json".to_string(), + error, + })?, Some(regex::Regex::new("\"Version\":\"([\\da-zA-Z\\-\\.]+)\"").unwrap()), ), PackageManager::Pnpm => ( @@ -229,7 +245,11 @@ impl PackageManager { .arg(name) .args(["--parseable", "--depth", "0"]) .current_dir(frontend_dir) - .output()?, + .output() + .map_err(|error| Error::CommandFailed { + command: "pnpm list --parseable --depth 0".to_string(), + error, + })?, None, ), // Bun and Deno don't support `list` command @@ -239,7 +259,11 @@ impl PackageManager { .arg(name) .args(["version", "--depth", "0"]) .current_dir(frontend_dir) - .output()?, + .output() + .map_err(|error| Error::CommandFailed { + command: "npm list --version --depth 0".to_string(), + error, + })?, None, ), }; @@ -270,14 +294,22 @@ impl PackageManager { .args(packages) .args(["--json", "--depth", "0"]) .current_dir(frontend_dir) - .output()?, + .output() + .map_err(|error| Error::CommandFailed { + command: "pnpm list --json --depth 0".to_string(), + error, + })?, // Bun and Deno don't support `list` command PackageManager::Npm | PackageManager::Bun | PackageManager::Deno => cross_command("npm") .arg("list") .args(packages) .args(["--json", "--depth", "0"]) .current_dir(frontend_dir) - .output()?, + .output() + .map_err(|error| Error::CommandFailed { + command: "npm list --json --depth 0".to_string(), + error, + })?, }; let mut versions = HashMap::new(); @@ -300,7 +332,7 @@ impl PackageManager { version: String, } - let json: ListOutput = serde_json::from_str(&stdout)?; + let json: ListOutput = serde_json::from_str(&stdout).context("failed to parse npm list")?; for (package, dependency) in json.dependencies.into_iter().chain(json.dev_dependencies) { let version = dependency.version; if let Ok(version) = semver::Version::parse(&version) { @@ -322,7 +354,11 @@ fn yarn_package_versions( .args(packages) .args(["--json", "--depth", "0"]) .current_dir(frontend_dir) - .output()?; + .output() + .map_err(|error| Error::CommandFailed { + command: "yarn list --json --depth 0".to_string(), + error, + })?; let mut versions = HashMap::new(); let stdout = String::from_utf8_lossy(&output.stdout); @@ -371,7 +407,11 @@ fn yarn_berry_package_versions( let output = cross_command("yarn") .args(["info", "--json"]) .current_dir(frontend_dir) - .output()?; + .output() + .map_err(|error| Error::CommandFailed { + command: "yarn info --json".to_string(), + error, + })?; let mut versions = HashMap::new(); let stdout = String::from_utf8_lossy(&output.stdout); diff --git a/crates/tauri-cli/src/helpers/pbxproj.rs b/crates/tauri-cli/src/helpers/pbxproj.rs index 4e141b89f..28c36c7d9 100644 --- a/crates/tauri-cli/src/helpers/pbxproj.rs +++ b/crates/tauri-cli/src/helpers/pbxproj.rs @@ -8,9 +8,12 @@ use std::{ path::{Path, PathBuf}, }; +use crate::error::ErrorExt; + pub fn parse>(path: P) -> crate::Result { let path = path.as_ref(); - let pbxproj = std::fs::read_to_string(path)?; + let pbxproj = + std::fs::read_to_string(path).fs_context("failed to read pbxproj file", path.to_path_buf())?; let mut proj = Pbxproj { path: path.to_owned(), @@ -171,7 +174,7 @@ enum State { } pub struct Pbxproj { - path: PathBuf, + pub path: PathBuf, raw_lines: Vec, pub xc_build_configuration: BTreeMap, pub xc_configuration_list: BTreeMap, diff --git a/crates/tauri-cli/src/helpers/prompts.rs b/crates/tauri-cli/src/helpers/prompts.rs index 37621981b..b96db3828 100644 --- a/crates/tauri-cli/src/helpers/prompts.rs +++ b/crates/tauri-cli/src/helpers/prompts.rs @@ -4,7 +4,7 @@ use std::{fmt::Display, str::FromStr}; -use crate::Result; +use crate::{error::Context, Result}; pub fn input( prompt: &str, @@ -32,7 +32,7 @@ where builder .interact_text() .map(|t: T| if t.ne("") { Some(t) } else { None }) - .map_err(Into::into) + .context("failed to prompt input") } } @@ -42,7 +42,7 @@ pub fn confirm(prompt: &str, default: Option) -> Result { if let Some(default) = default { builder = builder.default(default); } - builder.interact().map_err(Into::into) + builder.interact().context("failed to prompt confirm") } pub fn multiselect( @@ -57,5 +57,5 @@ pub fn multiselect( if let Some(defaults) = defaults { builder = builder.defaults(defaults); } - builder.interact().map_err(Into::into) + builder.interact().context("failed to prompt multi-select") } diff --git a/crates/tauri-cli/src/helpers/template.rs b/crates/tauri-cli/src/helpers/template.rs index 6817978e7..6555aec03 100644 --- a/crates/tauri-cli/src/helpers/template.rs +++ b/crates/tauri-cli/src/helpers/template.rs @@ -13,6 +13,8 @@ use include_dir::Dir; use serde::Serialize; use serde_json::value::{Map, Value as JsonValue}; +use crate::error::ErrorExt; + /// Map of template variable names and values. #[derive(Clone, Debug)] #[repr(transparent)] @@ -74,13 +76,17 @@ pub fn render_with_generator< file_path.set_extension("toml"); } } - if let Some(mut output_file) = out_file_generator(file_path)? { + if let Some(mut output_file) = out_file_generator(file_path.clone()) + .fs_context("failed to generate output file", file_path.clone())? + { if let Some(utf8) = file.contents_utf8() { handlebars .render_template_to_write(utf8, &data, &mut output_file) .expect("Failed to render template"); } else { - output_file.write_all(file.contents())?; + output_file + .write_all(file.contents()) + .fs_context("failed to write template", file_path.clone())?; } } } diff --git a/crates/tauri-cli/src/helpers/updater_signature.rs b/crates/tauri-cli/src/helpers/updater_signature.rs index 7c0299e08..b7f03cafc 100644 --- a/crates/tauri-cli/src/helpers/updater_signature.rs +++ b/crates/tauri-cli/src/helpers/updater_signature.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use anyhow::Context; use base64::Engine; use minisign::{ sign, KeyPair as KP, PublicKey, PublicKeyBox, SecretKey, SecretKeyBox, SignatureBox, @@ -15,6 +14,8 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use crate::error::{Context, ErrorExt}; + /// A key pair (`PublicKey` and `SecretKey`). #[derive(Clone, Debug)] pub struct KeyPair { @@ -24,9 +25,9 @@ pub struct KeyPair { fn create_file(path: &Path) -> crate::Result> { if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; + fs::create_dir_all(parent).fs_context("failed to create directory", parent.to_path_buf())?; } - let file = File::create(path)?; + let file = File::create(path).fs_context("failed to create file", path.to_path_buf())?; Ok(BufWriter::new(file)) } @@ -48,8 +49,12 @@ pub fn generate_key(password: Option) -> crate::Result { /// Transform a base64 String to readable string for the main signer pub fn decode_key>(base64_key: S) -> crate::Result { - let decoded_str = &base64::engine::general_purpose::STANDARD.decode(base64_key)?[..]; - Ok(String::from(str::from_utf8(decoded_str)?)) + let decoded_str = &base64::engine::general_purpose::STANDARD + .decode(base64_key) + .context("failed to decode base64 key")?[..]; + Ok(String::from( + str::from_utf8(decoded_str).context("failed to convert base64 to utf8")?, + )) } /// Save KeyPair to disk @@ -69,28 +74,43 @@ where if sk_path.exists() { if !force { - return Err(anyhow::anyhow!( + crate::error::bail!( "Key generation aborted:\n{} already exists\nIf you really want to overwrite the existing key pair, add the --force switch to force this operation.", sk_path.display() - )); + ); } else { - std::fs::remove_file(sk_path)?; + std::fs::remove_file(sk_path) + .fs_context("failed to remove secret key file", sk_path.to_path_buf())?; } } if pk_path.exists() { - std::fs::remove_file(pk_path)?; + std::fs::remove_file(pk_path) + .fs_context("failed to remove public key file", pk_path.to_path_buf())?; } - let mut sk_writer = create_file(sk_path)?; - write!(sk_writer, "{key:}")?; - sk_writer.flush()?; + let write_file = |mut writer: BufWriter, contents: &str| -> std::io::Result<()> { + write!(writer, "{contents:}")?; + writer.flush()?; + Ok(()) + }; - let mut pk_writer = create_file(pk_path)?; - write!(pk_writer, "{pubkey:}")?; - pk_writer.flush()?; + write_file(create_file(sk_path)?, key) + .fs_context("failed to write secret key", sk_path.to_path_buf())?; - Ok((fs::canonicalize(sk_path)?, fs::canonicalize(pk_path)?)) + write_file(create_file(pk_path)?, pubkey) + .fs_context("failed to write public key", pk_path.to_path_buf())?; + + Ok(( + fs::canonicalize(sk_path).fs_context( + "failed to canonicalize secret key path", + sk_path.to_path_buf(), + )?, + fs::canonicalize(pk_path).fs_context( + "failed to canonicalize public key path", + pk_path.to_path_buf(), + )?, + )) } /// Sign files @@ -104,8 +124,6 @@ where extension.push(".sig"); let signature_path = bin_path.with_extension(extension); - let mut signature_box_writer = create_file(&signature_path)?; - let trusted_comment = format!( "timestamp:{}\tfile:{}", unix_timestamp(), @@ -120,13 +138,20 @@ where data_reader, Some(trusted_comment.as_str()), Some("signature from tauri secret key"), - )?; + ) + .context("failed to sign file")?; let encoded_signature = base64::engine::general_purpose::STANDARD.encode(signature_box.to_string()); - signature_box_writer.write_all(encoded_signature.as_bytes())?; - signature_box_writer.flush()?; - Ok((fs::canonicalize(&signature_path)?, signature_box)) + std::fs::write(&signature_path, encoded_signature.as_bytes()) + .fs_context("failed to write signature file", signature_path.clone())?; + Ok(( + fs::canonicalize(&signature_path).fs_context( + "failed to canonicalize signature file", + signature_path.clone(), + )?, + signature_box, + )) } /// Gets the updater secret key from the given private key and password. @@ -148,7 +173,9 @@ pub fn pub_key>(public_key: S) -> crate::Result { let decoded_publick = decode_key(public_key).context("failed to decode base64 pubkey")?; let pk_box = PublicKeyBox::from_string(&decoded_publick).context("failed to load updater pubkey")?; - let pk = pk_box.into_public_key()?; + let pk = pk_box + .into_public_key() + .context("failed to convert updater pubkey")?; Ok(pk) } @@ -168,7 +195,7 @@ where let file = OpenOptions::new() .read(true) .open(data_path) - .map_err(|e| minisign::PError::new(minisign::ErrorKind::Io, e))?; + .fs_context("failed to open data file", data_path.to_path_buf())?; Ok(BufReader::new(file)) } diff --git a/crates/tauri-cli/src/icon.rs b/crates/tauri-cli/src/icon.rs index 5b2dd8f0f..e072d2c5a 100644 --- a/crates/tauri-cli/src/icon.rs +++ b/crates/tauri-cli/src/icon.rs @@ -2,7 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::{helpers::app_paths::tauri_dir, Result}; +use crate::{ + error::{Context, Error, ErrorExt}, + helpers::app_paths::tauri_dir, + Result, +}; use std::{ collections::HashMap, @@ -13,7 +17,6 @@ use std::{ sync::Arc, }; -use anyhow::Context; use clap::Parser; use icns::{IconFamily, IconType}; use image::{ @@ -162,12 +165,15 @@ fn read_source(path: PathBuf) -> Result { } else { Ok(Source::DynamicImage(DynamicImage::ImageRgba8( open(&path) - .context(format!("Can't read and decode source image: {:?}", path))? + .context(format!( + "failed to read and decode source image {}", + path.display() + ))? .into_rgba8(), ))) } } else { - anyhow::bail!("Error loading image"); + crate::error::bail!("Error loading image"); } } @@ -181,7 +187,12 @@ fn parse_bg_color(bg_color_string: &String) -> Result> { (color.alpha * 255.) as u8, ]) }) - .map_err(|_| anyhow::anyhow!("failed to parse color {}", bg_color_string))?; + .map_err(|_e| { + Error::Context( + format!("failed to parse color {bg_color_string}"), + "invalid RGBA color".into(), + ) + })?; Ok(bg_color) } @@ -194,7 +205,7 @@ pub fn command(options: Options) -> Result<()> { }); let png_icon_sizes = options.png.unwrap_or_default(); - create_dir_all(&out_dir).context("Can't create output directory")?; + create_dir_all(&out_dir).fs_context("Can't create output directory", &out_dir)?; let manifest = if input.extension().is_some_and(|ext| ext == "json") { parse_manifest(&input).map(Some)? @@ -220,7 +231,7 @@ pub fn command(options: Options) -> Result<()> { let source = read_source(default_icon)?; if source.height() != source.width() { - anyhow::bail!("Source image must be square"); + crate::error::bail!("Source image must be square"); } if png_icon_sizes.is_empty() { @@ -256,9 +267,12 @@ pub fn command(options: Options) -> Result<()> { fn parse_manifest(manifest_path: &Path) -> Result { let manifest: Manifest = serde_json::from_str( &std::fs::read_to_string(manifest_path) - .with_context(|| format!("cannot read manifest file {}", manifest_path.display()))?, + .fs_context("cannot read manifest file", manifest_path)?, ) - .with_context(|| format!("failed to parse manifest file {}", manifest_path.display()))?; + .context(format!( + "failed to parse manifest file {}", + manifest_path.display() + ))?; log::debug!("Read manifest file from {}", manifest_path.display()); Ok(manifest) } @@ -285,27 +299,34 @@ fn icns(source: &Source, out_dir: &Path) -> Result<()> { let mut family = IconFamily::new(); - for (name, entry) in entries { + for (_name, entry) in entries { let size = entry.size; let mut buf = Vec::new(); let image = source.resize_exact(size)?; - write_png(image.as_bytes(), &mut buf, size)?; + write_png(image.as_bytes(), &mut buf, size).context("failed to write output file")?; - let image = icns::Image::read_png(&buf[..])?; + let image = icns::Image::read_png(&buf[..]).context("failed to read output file")?; family .add_icon_with_type( &image, IconType::from_ostype(entry.ostype.parse().unwrap()).unwrap(), ) - .with_context(|| format!("Can't add {name} to Icns Family"))?; + .context("failed to add icon to Icns Family")?; } - let mut out_file = BufWriter::new(File::create(out_dir.join("icon.icns"))?); - family.write(&mut out_file)?; - out_file.flush()?; + let icns_path = out_dir.join("icon.icns"); + let mut out_file = BufWriter::new( + File::create(&icns_path).fs_context("failed to create output file", &icns_path)?, + ); + family + .write(&mut out_file) + .fs_context("failed to write output file", &icns_path)?; + out_file + .flush() + .fs_context("failed to flush output file", &icns_path)?; Ok(()) } @@ -323,28 +344,30 @@ fn ico(source: &Source, out_dir: &Path) -> Result<()> { if size == 256 { let mut buf = Vec::new(); - write_png(image.as_bytes(), &mut buf, size)?; + write_png(image.as_bytes(), &mut buf, size).context("failed to write output file")?; - frames.push(IcoFrame::with_encoded( - buf, - size, - size, - ExtendedColorType::Rgba8, - )?) + frames.push( + IcoFrame::with_encoded(buf, size, size, ExtendedColorType::Rgba8) + .context("failed to create ico frame")?, + ); } else { - frames.push(IcoFrame::as_png( - image.as_bytes(), - size, - size, - ExtendedColorType::Rgba8, - )?); + frames.push( + IcoFrame::as_png(image.as_bytes(), size, size, ExtendedColorType::Rgba8) + .context("failed to create PNG frame")?, + ); } } - let mut out_file = BufWriter::new(File::create(out_dir.join("icon.ico"))?); + let ico_path = out_dir.join("icon.ico"); + let mut out_file = + BufWriter::new(File::create(&ico_path).fs_context("failed to create output file", &ico_path)?); let encoder = IcoEncoder::new(&mut out_file); - encoder.encode_images(&frames)?; - out_file.flush()?; + encoder + .encode_images(&frames) + .context("failed to encode images")?; + out_file + .flush() + .fs_context("failed to flush output file", &ico_path)?; Ok(()) } @@ -399,7 +422,10 @@ fn android( let folder_name = format!("mipmap-{}", target.name); let out_folder = out_dir.join(&folder_name); - create_dir_all(&out_folder).context("Can't create Android mipmap output directory")?; + create_dir_all(&out_folder).fs_context( + "failed to create Android mipmap output directory", + &out_folder, + )?; fg_entries.push(PngEntry { name: format!("{}/{}", folder_name, "ic_launcher_foreground.png"), @@ -445,18 +471,29 @@ fn android( } fn create_color_file(out_dir: &Path, color: &String) -> Result<()> { let values_folder = out_dir.join("values"); - create_dir_all(&values_folder).context("Can't create Android values output directory")?; - let mut color_file = File::create(values_folder.join("ic_launcher_background.xml"))?; - color_file.write_all( - format!( - r#" - - {} -"#, - color - ) - .as_bytes(), + create_dir_all(&values_folder).fs_context( + "Can't create Android values output directory", + &values_folder, )?; + let launcher_background_xml_path = values_folder.join("ic_launcher_background.xml"); + let mut color_file = File::create(&launcher_background_xml_path).fs_context( + "failed to create Android color file", + &launcher_background_xml_path, + )?; + color_file + .write_all( + format!( + r#" + + {color} +"#, + ) + .as_bytes(), + ) + .fs_context( + "failed to write Android color file", + &launcher_background_xml_path, + )?; Ok(()) } @@ -468,7 +505,7 @@ fn android( android_out } else { let out = out_dir.join("android"); - create_dir_all(&out).context("Can't create Android output directory")?; + create_dir_all(&out).fs_context("Can't create Android output directory", &out)?; out }; let entries = android_entries(&out)?; @@ -545,9 +582,14 @@ fn android( let image = apply_round_mask(&image, entry.size, margin, radius); - let mut out_file = BufWriter::new(File::create(entry.out_path)?); - write_png(image.as_bytes(), &mut out_file, entry.size)?; - out_file.flush()?; + let mut out_file = BufWriter::new( + File::create(&entry.out_path).fs_context("failed to create output file", &entry.out_path)?, + ); + write_png(image.as_bytes(), &mut out_file, entry.size) + .context("failed to write output file")?; + out_file + .flush() + .fs_context("failed to flush output file", &entry.out_path)?; } let mut launcher_content = r#" @@ -570,10 +612,17 @@ fn android( launcher_content.push_str("\n"); let any_dpi_folder = out.join("mipmap-anydpi-v26"); - create_dir_all(&any_dpi_folder) - .context("Can't create Android mipmap-anydpi-v26 output directory")?; - let mut launcher_file = File::create(any_dpi_folder.join("ic_launcher.xml"))?; - launcher_file.write_all(launcher_content.as_bytes())?; + create_dir_all(&any_dpi_folder).fs_context( + "Can't create Android mipmap-anydpi-v26 output directory", + &any_dpi_folder, + )?; + + let launcher_xml_path = any_dpi_folder.join("ic_launcher.xml"); + let mut launcher_file = File::create(&launcher_xml_path) + .fs_context("failed to create Android launcher file", &launcher_xml_path)?; + launcher_file + .write_all(launcher_content.as_bytes()) + .fs_context("failed to write Android launcher file", &launcher_xml_path)?; Ok(()) } @@ -685,7 +734,7 @@ fn png(source: &Source, out_dir: &Path, ios_color: Rgba) -> Result<()> { ios_out } else { let out = out_dir.join("ios"); - create_dir_all(&out).context("Can't create iOS output directory")?; + create_dir_all(&out).fs_context("failed to create iOS output directory", &out)?; out }; @@ -758,13 +807,16 @@ fn resize_and_save_png( scale_percent: Option, ) -> Result<()> { let image = resize_png(source, size, bg, scale_percent)?; - let mut out_file = BufWriter::new(File::create(file_path)?); - write_png(image.as_bytes(), &mut out_file, size)?; - Ok(out_file.flush()?) + let mut out_file = + BufWriter::new(File::create(file_path).fs_context("failed to create output file", file_path)?); + write_png(image.as_bytes(), &mut out_file, size).context("failed to write output file")?; + out_file + .flush() + .fs_context("failed to save output file", file_path) } // Encode image data as png with compression. -fn write_png(image_data: &[u8], w: W, size: u32) -> Result<()> { +fn write_png(image_data: &[u8], w: W, size: u32) -> image::ImageResult<()> { let encoder = PngEncoder::new_with_quality(w, CompressionType::Best, PngFilterType::Adaptive); encoder.write_image(image_data, size, size, ExtendedColorType::Rgba8)?; Ok(()) diff --git a/crates/tauri-cli/src/info/env_system.rs b/crates/tauri-cli/src/info/env_system.rs index 797594820..0087319eb 100644 --- a/crates/tauri-cli/src/info/env_system.rs +++ b/crates/tauri-cli/src/info/env_system.rs @@ -3,6 +3,8 @@ // SPDX-License-Identifier: MIT use super::{SectionItem, Status}; +#[cfg(windows)] +use crate::error::Context; use colored::Colorize; #[cfg(windows)] use serde::Deserialize; @@ -45,7 +47,11 @@ fn build_tools_version() -> crate::Result> { "json", "-utf8", ]) - .output()?; + .output() + .map_err(|error| crate::error::Error::CommandFailed { + command: "vswhere -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -requires Microsoft.VisualStudio.Component.Windows10SDK.* -format json -utf8".to_string(), + error, + })?; let output_sdk11 = Command::new(vswhere) .args([ @@ -60,19 +66,25 @@ fn build_tools_version() -> crate::Result> { "json", "-utf8", ]) - .output()?; + .output() + .map_err(|error| crate::error::Error::CommandFailed { + command: "vswhere -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -requires Microsoft.VisualStudio.Component.Windows11SDK.* -format json -utf8".to_string(), + error, + })?; let mut instances: Vec = Vec::new(); if output_sdk10.status.success() { let stdout = String::from_utf8_lossy(&output_sdk10.stdout); - let found: Vec = serde_json::from_str(&stdout)?; + let found: Vec = + serde_json::from_str(&stdout).context("failed to parse vswhere output")?; instances.extend(found); } if output_sdk11.status.success() { let stdout = String::from_utf8_lossy(&output_sdk11.stdout); - let found: Vec = serde_json::from_str(&stdout)?; + let found: Vec = + serde_json::from_str(&stdout).context("failed to parse vswhere output")?; instances.extend(found); } @@ -97,7 +109,11 @@ fn webview2_version() -> crate::Result> { let output = Command::new(&powershell_path) .args(["-NoProfile", "-Command"]) .arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}") - .output()?; + .output() + .map_err(|error| crate::error::Error::CommandFailed { + command: "Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}".to_string(), + error, + })?; if output.status.success() { return Ok(Some( String::from_utf8_lossy(&output.stdout).replace('\n', ""), @@ -107,7 +123,11 @@ fn webview2_version() -> crate::Result> { let output = Command::new(&powershell_path) .args(["-NoProfile", "-Command"]) .arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}") - .output()?; + .output() + .map_err(|error| crate::error::Error::CommandFailed { + command: "Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}".to_string(), + error, + })?; if output.status.success() { return Ok(Some( String::from_utf8_lossy(&output.stdout).replace('\n', ""), @@ -117,7 +137,11 @@ fn webview2_version() -> crate::Result> { let output = Command::new(&powershell_path) .args(["-NoProfile", "-Command"]) .arg("Get-ItemProperty -Path 'HKCU:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}") - .output()?; + .output() + .map_err(|error| crate::error::Error::CommandFailed { + command: "Get-ItemProperty -Path 'HKCU:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}".to_string(), + error, + })?; if output.status.success() { return Ok(Some( String::from_utf8_lossy(&output.stdout).replace('\n', ""), diff --git a/crates/tauri-cli/src/info/mod.rs b/crates/tauri-cli/src/info/mod.rs index 2a774da2d..b6ea72232 100644 --- a/crates/tauri-cli/src/info/mod.rs +++ b/crates/tauri-cli/src/info/mod.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT use crate::{ + error::Context, helpers::app_paths::{resolve_frontend_dir, resolve_tauri_dir}, Result, }; @@ -37,7 +38,7 @@ pub struct VersionMetadata { fn version_metadata() -> Result { serde_json::from_str::(include_str!("../../metadata-v2.json")) - .map_err(Into::into) + .context("failed to parse version metadata") } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default)] diff --git a/crates/tauri-cli/src/info/packages_nodejs.rs b/crates/tauri-cli/src/info/packages_nodejs.rs index 73b87d8b7..2efe69b2d 100644 --- a/crates/tauri-cli/src/info/packages_nodejs.rs +++ b/crates/tauri-cli/src/info/packages_nodejs.rs @@ -8,7 +8,11 @@ use colored::Colorize; use serde::Deserialize; use std::path::PathBuf; -use crate::helpers::{cross_command, npm::PackageManager}; +use crate::error::Context; +use crate::{ + error::Error, + helpers::{cross_command, npm::PackageManager}, +}; #[derive(Deserialize)] struct YarnVersionInfo { @@ -24,10 +28,15 @@ pub fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result crate::Result crate::Result { let mut cmd = cross_command("npm"); - let output = cmd.arg("show").arg(name).arg("version").output()?; + let output = cmd + .arg("show") + .arg(name) + .arg("version") + .output() + .map_err(|error| Error::CommandFailed { + command: "npm show --version".to_string(), + error, + })?; if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); Ok(Some(stdout.replace('\n', ""))) @@ -65,7 +86,15 @@ pub fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result { let mut cmd = cross_command("pnpm"); - let output = cmd.arg("info").arg(name).arg("version").output()?; + let output = cmd + .arg("info") + .arg(name) + .arg("version") + .output() + .map_err(|error| Error::CommandFailed { + command: "pnpm info --version".to_string(), + error, + })?; if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); Ok(Some(stdout.replace('\n', ""))) diff --git a/crates/tauri-cli/src/info/plugins.rs b/crates/tauri-cli/src/info/plugins.rs index 3169324e2..8fb78fcc0 100644 --- a/crates/tauri-cli/src/info/plugins.rs +++ b/crates/tauri-cli/src/info/plugins.rs @@ -8,14 +8,16 @@ use std::{ path::{Path, PathBuf}, }; -use crate::helpers::{ - self, - cargo_manifest::{cargo_manifest_and_lock, crate_version}, - npm::PackageManager, +use crate::{ + helpers::{ + self, + cargo_manifest::{cargo_manifest_and_lock, crate_version}, + npm::PackageManager, + }, + Error, }; use super::{packages_nodejs, packages_rust, SectionItem}; -use anyhow::anyhow; #[derive(Debug)] pub struct InstalledPackage { @@ -161,5 +163,5 @@ pub fn check_mismatched_packages(frontend_dir: &Path, tauri_path: &Path) -> crat ) .collect::>() .join("\n"); - Err(anyhow!("Found version mismatched Tauri packages. Make sure the NPM and crate versions are on the same major/minor releases:\n{mismatched_text}")) + Err(Error::GenericError(format!("Found version mismatched Tauri packages. Make sure the NPM and crate versions are on the same major/minor releases:\n{mismatched_text}"))) } diff --git a/crates/tauri-cli/src/init.rs b/crates/tauri-cli/src/init.rs index 5bdf8ce1a..a72137388 100644 --- a/crates/tauri-cli/src/init.rs +++ b/crates/tauri-cli/src/init.rs @@ -17,8 +17,10 @@ use std::{ path::PathBuf, }; -use crate::Result; -use anyhow::Context; +use crate::{ + error::{Context, ErrorExt}, + Result, +}; use clap::Parser; use handlebars::{to_json, Handlebars}; use include_dir::{include_dir, Dir}; @@ -76,8 +78,10 @@ impl Options { let package_json_path = PathBuf::from(&self.directory).join("package.json"); let init_defaults = if package_json_path.exists() { - let package_json_text = read_to_string(package_json_path)?; - let package_json: crate::PackageJson = serde_json::from_str(&package_json_text)?; + let package_json_text = read_to_string(&package_json_path) + .fs_context("failed to read", package_json_path.clone())?; + let package_json: crate::PackageJson = + serde_json::from_str(&package_json_text).context("failed to parse JSON")?; let (framework, _) = infer_framework(&package_json_text); InitDefaults { app_name: package_json.product_name.or(package_json.name), @@ -187,7 +191,8 @@ pub fn command(mut options: Options) -> Result<()> { options = options.load()?; let template_target_path = PathBuf::from(&options.directory).join("src-tauri"); - let metadata = serde_json::from_str::(include_str!("../metadata-v2.json"))?; + let metadata = serde_json::from_str::(include_str!("../metadata-v2.json")) + .context("failed to parse version metadata")?; if template_target_path.exists() && !options.force { log::warn!( diff --git a/crates/tauri-cli/src/inspect.rs b/crates/tauri-cli/src/inspect.rs index d08a81835..68d040ed2 100644 --- a/crates/tauri-cli/src/inspect.rs +++ b/crates/tauri-cli/src/inspect.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use anyhow::Result; +use crate::Result; use clap::{Parser, Subcommand}; use crate::interface::{AppInterface, AppSettings, Interface}; diff --git a/crates/tauri-cli/src/interface/mod.rs b/crates/tauri-cli/src/interface/mod.rs index 16877b41f..e29377b17 100644 --- a/crates/tauri-cli/src/interface/mod.rs +++ b/crates/tauri-cli/src/interface/mod.rs @@ -11,8 +11,7 @@ use std::{ sync::Arc, }; -use crate::helpers::config::Config; -use anyhow::Context; +use crate::{error::Context, helpers::config::Config}; use tauri_bundler::bundle::{PackageType, Settings, SettingsBuilder}; pub use rust::{MobileOptions, Options, Rust as AppInterface}; @@ -20,7 +19,6 @@ pub use rust::{MobileOptions, Options, Rust as AppInterface}; pub trait DevProcess { fn kill(&self) -> std::io::Result<()>; fn try_wait(&self) -> std::io::Result>; - // TODO: #[allow(unused)] fn wait(&self) -> std::io::Result; #[allow(unused)] @@ -56,7 +54,7 @@ pub trait AppSettings { let target: String = if let Some(target) = options.target.clone() { target } else { - tauri_utils::platform::target_triple()? + tauri_utils::platform::target_triple().context("failed to get target triple")? }; let mut bins = self.get_binaries()?; @@ -81,7 +79,10 @@ pub trait AppSettings { ) } - settings_builder.build().map_err(Into::into) + settings_builder + .build() + .map_err(Box::new) + .map_err(Into::into) } } diff --git a/crates/tauri-cli/src/interface/rust.rs b/crates/tauri-cli/src/interface/rust.rs index 3c39a2209..2ccd73648 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -14,7 +14,6 @@ use std::{ time::Duration, }; -use anyhow::Context; use dunce::canonicalize; use glob::glob; use ignore::gitignore::{Gitignore, GitignoreBuilder}; @@ -30,6 +29,7 @@ use tauri_utils::config::{parse::is_configuration_file, DeepLinkProtocol, Runner use super::{AppSettings, DevProcess, ExitReason, Interface}; use crate::{ + error::{Context, Error, ErrorExt}, helpers::{ app_paths::{frontend_dir, tauri_dir}, config::{nsis_settings, reload as reload_config, wix_settings, BundleResources, Config}, @@ -140,7 +140,14 @@ impl Interface for Rust { } }) .unwrap(); - watcher.watch(tauri_dir().join("Cargo.toml"), RecursiveMode::NonRecursive)?; + watcher + .watch(tauri_dir().join("Cargo.toml"), RecursiveMode::NonRecursive) + .with_context(|| { + format!( + "failed to watch {}", + tauri_dir().join("Cargo.toml").display() + ) + })?; let (manifest, modified) = rewrite_manifest(config)?; if modified { // Wait for the modified event so we don't trigger a re-build later on @@ -411,9 +418,9 @@ fn dev_options( // Copied from https://github.com/rust-lang/cargo/blob/69255bb10de7f74511b5cef900a9d102247b6029/src/cargo/core/workspace.rs#L665 fn expand_member_path(path: &Path) -> crate::Result> { let path = path.to_str().context("path is not UTF-8 compatible")?; - let res = glob(path).with_context(|| format!("could not parse pattern `{path}`"))?; + let res = glob(path).with_context(|| format!("failed to expand glob pattern for {path}"))?; let res = res - .map(|p| p.with_context(|| format!("unable to match path to pattern `{path}`"))) + .map(|p| p.with_context(|| format!("failed to expand glob pattern for {path}"))) .collect::, _>>()?; Ok(res) } @@ -574,7 +581,7 @@ impl Rust { ); let mut p = process.lock().unwrap(); - p.kill().with_context(|| "failed to kill app process")?; + p.kill().context("failed to kill app process")?; // wait for the process to exit // note that on mobile, kill() already waits for the process to exit (duct implementation) @@ -622,18 +629,19 @@ impl MaybeWorkspace { fn resolve( self, label: &str, - get_ws_field: impl FnOnce() -> anyhow::Result, - ) -> anyhow::Result { + get_ws_field: impl FnOnce() -> crate::Result, + ) -> crate::Result { match self { MaybeWorkspace::Defined(value) => Ok(value), - MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: true }) => { - get_ws_field().context(format!( - "error inheriting `{label}` from workspace root manifest's `workspace.package.{label}`" - )) - } - MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: false }) => Err(anyhow::anyhow!( - "`workspace=false` is unsupported for `package.{label}`" - )), + MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: true }) => get_ws_field() + .with_context(|| { + format!( + "error inheriting `{label}` from workspace root manifest's `workspace.package.{label}`" + ) + }), + MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: false }) => Err( + crate::Error::GenericError("`workspace=false` is unsupported for `package.{label}`".into()), + ), } } fn _as_defined(&self) -> Option<&T> { @@ -721,8 +729,11 @@ impl CargoSettings { fn load(dir: &Path) -> crate::Result { let toml_path = dir.join("Cargo.toml"); let toml_str = std::fs::read_to_string(&toml_path) - .with_context(|| format!("Failed to read {}", toml_path.display()))?; - toml::from_str(&toml_str).with_context(|| format!("Failed to parse {}", toml_path.display())) + .fs_context("Failed to read Cargo manifest", toml_path.clone())?; + toml::from_str(&toml_str).context(format!( + "failed to parse Cargo manifest at {}", + toml_path.display() + )) } } @@ -831,11 +842,10 @@ impl AppSettings for RustAppSettings { .plugins .0 .get("updater") - .ok_or_else(|| { - anyhow::anyhow!("failed to get updater configuration: plugins > updater doesn't exist") - })? + .context("failed to get updater configuration: plugins > updater doesn't exist")? .clone(), - )?; + ) + .context("failed to parse updater plugin configuration")?; Some(UpdaterSettings { v1_compatible, pubkey: updater.pubkey, @@ -862,7 +872,8 @@ impl AppSettings for RustAppSettings { .get("deep-link") .and_then(|c| c.get("desktop").cloned()) { - let protocols: DesktopDeepLinks = serde_json::from_value(plugin_config)?; + let protocols: DesktopDeepLinks = + serde_json::from_value(plugin_config).context("failed to parse desktop deep links from Tauri configuration > plugins > deep-link > desktop")?; settings.deep_link_protocols = Some(match protocols { DesktopDeepLinks::One(p) => vec![p], DesktopDeepLinks::List(p) => p, @@ -1034,18 +1045,18 @@ impl AppSettings for RustAppSettings { impl RustAppSettings { pub fn new(config: &Config, manifest: Manifest, target: Option) -> crate::Result { let tauri_dir = tauri_dir(); - let cargo_settings = CargoSettings::load(tauri_dir).context("failed to load cargo settings")?; + let cargo_settings = CargoSettings::load(tauri_dir).context("failed to load Cargo settings")?; let cargo_package_settings = match &cargo_settings.package { Some(package_info) => package_info.clone(), None => { - return Err(anyhow::anyhow!( + return Err(crate::Error::GenericError( "No package info in the config file".to_owned(), )) } }; let ws_package_settings = CargoSettings::load(&get_workspace_dir()?) - .context("failed to load cargo settings from workspace root")? + .context("failed to load Cargo settings from workspace root")? .workspace .and_then(|v| v.package); @@ -1058,7 +1069,7 @@ impl RustAppSettings { ws_package_settings .as_ref() .and_then(|p| p.version.clone()) - .ok_or_else(|| anyhow::anyhow!("Couldn't inherit value for `version` from workspace")) + .context("Couldn't inherit value for `version` from workspace") }) .expect("Cargo project does not have a version") }); @@ -1078,9 +1089,7 @@ impl RustAppSettings { ws_package_settings .as_ref() .and_then(|v| v.description.clone()) - .ok_or_else(|| { - anyhow::anyhow!("Couldn't inherit value for `description` from workspace") - }) + .context("Couldn't inherit value for `description` from workspace") }) .unwrap() }) @@ -1091,9 +1100,7 @@ impl RustAppSettings { ws_package_settings .as_ref() .and_then(|v| v.homepage.clone()) - .ok_or_else(|| { - anyhow::anyhow!("Couldn't inherit value for `homepage` from workspace") - }) + .context("Couldn't inherit value for `homepage` from workspace") }) .unwrap() }), @@ -1103,7 +1110,7 @@ impl RustAppSettings { ws_package_settings .as_ref() .and_then(|v| v.authors.clone()) - .ok_or_else(|| anyhow::anyhow!("Couldn't inherit value for `authors` from workspace")) + .context("Couldn't inherit value for `authors` from workspace") }) .unwrap() }), @@ -1168,16 +1175,20 @@ pub(crate) fn get_cargo_metadata() -> crate::Result { let output = Command::new("cargo") .args(["metadata", "--no-deps", "--format-version", "1"]) .current_dir(tauri_dir()) - .output()?; + .output() + .map_err(|error| Error::CommandFailed { + command: "cargo metadata --no-deps --format-version 1".to_string(), + error, + })?; if !output.status.success() { - return Err(anyhow::anyhow!( - "cargo metadata command exited with a non zero exit code: {}", - String::from_utf8_lossy(&output.stderr) - )); + return Err(Error::CommandFailed { + command: "cargo metadata".to_string(), + error: std::io::Error::other(String::from_utf8_lossy(&output.stderr)), + }); } - Ok(serde_json::from_slice(&output.stdout)?) + serde_json::from_slice(&output.stdout).context("failed to parse cargo metadata") } /// Get the cargo target directory based on the provided arguments. @@ -1185,10 +1196,12 @@ pub(crate) fn get_cargo_metadata() -> crate::Result { /// Otherwise, use the target directory from cargo metadata. pub(crate) fn get_cargo_target_dir(args: &[String]) -> crate::Result { let path = if let Some(target) = get_cargo_option(args, "--target-dir") { - std::env::current_dir()?.join(target) + std::env::current_dir() + .context("failed to get current directory")? + .join(target) } else { get_cargo_metadata() - .with_context(|| "failed to run 'cargo metadata' command to get target directory")? + .context("failed to run 'cargo metadata' command to get target directory")? .target_directory }; @@ -1383,8 +1396,8 @@ fn tauri_config_to_bundle_settings( copyright: config.copyright, category: match config.category { Some(category) => Some(AppCategory::from_str(&category).map_err(|e| match e { - Some(e) => anyhow::anyhow!("invalid category, did you mean `{}`?", e), - None => anyhow::anyhow!("invalid category"), + Some(e) => Error::GenericError(format!("invalid category, did you mean `{e}`?")), + None => Error::GenericError("invalid category".to_string()), })?), None => None, }, @@ -1508,9 +1521,7 @@ fn tauri_config_to_bundle_settings( .cargo_ws_package_settings .as_ref() .and_then(|v| v.license.clone()) - .ok_or_else(|| { - anyhow::anyhow!("Couldn't inherit value for `license` from workspace") - }) + .context("Couldn't inherit value for `license` from workspace") }) .unwrap() }) diff --git a/crates/tauri-cli/src/interface/rust/cargo_config.rs b/crates/tauri-cli/src/interface/rust/cargo_config.rs index 50cfe63ac..ccb357df2 100644 --- a/crates/tauri-cli/src/interface/rust/cargo_config.rs +++ b/crates/tauri-cli/src/interface/rust/cargo_config.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use anyhow::{Context, Result}; use serde::Deserialize; use std::{ fs, @@ -11,6 +10,11 @@ use std::{ use tauri_utils::display_path; +use crate::{ + error::{Context, ErrorExt}, + Result, +}; + struct PathAncestors<'a> { current: Option<&'a Path>, } @@ -57,18 +61,12 @@ impl Config { let mut config = Self::default(); let get_config = |path: PathBuf| -> Result { - let contents = fs::read_to_string(&path).with_context(|| { - format!( - "failed to read configuration file `{}`", - display_path(&path) - ) - })?; - toml::from_str(&contents).with_context(|| { - format!( - "could not parse TOML configuration in `{}`", - display_path(&path) - ) - }) + let contents = + fs::read_to_string(&path).fs_context("failed to read configuration file", path.clone())?; + toml::from_str(&contents).context(format!( + "could not parse TOML configuration in `{}`", + display_path(&path) + )) }; for current in PathAncestors::new(path) { diff --git a/crates/tauri-cli/src/interface/rust/desktop.rs b/crates/tauri-cli/src/interface/rust/desktop.rs index 667f9cbbb..f5edb4d6e 100644 --- a/crates/tauri-cli/src/interface/rust/desktop.rs +++ b/crates/tauri-cli/src/interface/rust/desktop.rs @@ -3,9 +3,11 @@ // SPDX-License-Identifier: MIT use super::{AppSettings, DevProcess, ExitReason, Options, RustAppSettings, RustupTarget}; -use crate::CommandExt; +use crate::{ + error::{Context, ErrorExt}, + CommandExt, Error, +}; -use anyhow::Context; use shared_child::SharedChild; use std::{ fs, @@ -72,8 +74,7 @@ pub fn run_dev, ExitReason) + Send + Sync + 'static>( dev_cmd.arg("--color"); dev_cmd.arg("always"); - // TODO: double check this - dev_cmd.stdout(os_pipe::dup_stdout()?); + dev_cmd.stdout(os_pipe::dup_stdout().unwrap()); dev_cmd.stderr(Stdio::piped()); dev_cmd.arg("--"); @@ -86,16 +87,18 @@ pub fn run_dev, ExitReason) + Send + Sync + 'static>( let dev_child = match SharedChild::spawn(&mut dev_cmd) { Ok(c) => Ok(c), - Err(e) if e.kind() == ErrorKind::NotFound => Err(anyhow::anyhow!( - "`{}` command not found.{}", - runner, + Err(e) if e.kind() == ErrorKind::NotFound => crate::error::bail!( + "`{runner}` command not found.{}", if runner == "cargo" { " Please follow the Tauri setup guide: https://v2.tauri.app/start/prerequisites/" } else { "" } - )), - Err(e) => Err(e.into()), + ), + Err(e) => Err(Error::CommandFailed { + command: runner, + error: e, + }), }?; let dev_child = Arc::new(dev_child); let dev_child_stderr = dev_child.take_stderr().unwrap(); @@ -164,7 +167,8 @@ pub fn build( } if options.target == Some("universal-apple-darwin".into()) { - std::fs::create_dir_all(&out_dir).with_context(|| "failed to create project out directory")?; + std::fs::create_dir_all(&out_dir) + .fs_context("failed to create project out directory", out_dir.clone())?; let bin_name = bin_path.file_stem().unwrap(); @@ -189,9 +193,9 @@ pub fn build( let lipo_status = lipo_cmd.output_ok()?.status; if !lipo_status.success() { - return Err(anyhow::anyhow!( + crate::error::bail!( "Result of `lipo` command was unsuccessful: {lipo_status}. (Is `lipo` installed?)" - )); + ); } } else { build_production_app(options, available_targets, config_features) @@ -210,8 +214,8 @@ fn build_production_app( let runner = build_cmd.get_program().to_string_lossy().into_owned(); match build_cmd.piped() { Ok(status) if status.success() => Ok(()), - Ok(_) => Err(anyhow::anyhow!("failed to build app")), - Err(e) if e.kind() == ErrorKind::NotFound => Err(anyhow::anyhow!( + Ok(_) => crate::error::bail!("failed to build app"), + Err(e) if e.kind() == ErrorKind::NotFound => crate::error::bail!( "`{}` command not found.{}", runner, if runner == "cargo" { @@ -219,8 +223,11 @@ fn build_production_app( } else { "" } - )), - Err(e) => Err(e.into()), + ), + Err(e) => Err(Error::CommandFailed { + command: runner, + error: e, + }), } } @@ -302,7 +309,7 @@ fn validate_target( if let Some(available_targets) = available_targets { if let Some(target) = available_targets.iter().find(|t| t.name == target) { if !target.installed { - anyhow::bail!( + crate::error::bail!( "Target {target} is not installed (installed targets: {installed}). Please run `rustup target add {target}`.", target = target.name, installed = available_targets.iter().filter(|t| t.installed).map(|t| t.name.as_str()).collect::>().join(", ") @@ -310,7 +317,7 @@ fn validate_target( } } if !available_targets.iter().any(|t| t.name == target) { - anyhow::bail!("Target {target} does not exist. Please run `rustup target list` to see the available targets.", target = target); + crate::error::bail!("Target {target} does not exist. Please run `rustup target list` to see the available targets.", target = target); } } Ok(()) @@ -328,13 +335,7 @@ fn rename_app( "" }; let new_path = bin_path.with_file_name(format!("{main_binary_name}{extension}")); - fs::rename(&bin_path, &new_path).with_context(|| { - format!( - "failed to rename `{}` to `{}`", - tauri_utils::display_path(bin_path), - tauri_utils::display_path(&new_path), - ) - })?; + fs::rename(&bin_path, &new_path).fs_context("failed to rename app binary", bin_path.clone())?; Ok(new_path) } else { Ok(bin_path) diff --git a/crates/tauri-cli/src/interface/rust/installation.rs b/crates/tauri-cli/src/interface/rust/installation.rs index 11e0e7882..9a3273548 100644 --- a/crates/tauri-cli/src/interface/rust/installation.rs +++ b/crates/tauri-cli/src/interface/rust/installation.rs @@ -2,18 +2,31 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::Result; +use crate::{ + error::{Error, ErrorExt}, + Result, +}; use std::{fs::read_dir, path::PathBuf, process::Command}; pub fn installed_targets() -> Result> { let output = Command::new("rustc") .args(["--print", "sysroot"]) - .output()?; + .output() + .map_err(|error| Error::CommandFailed { + command: "rustc --print sysroot".to_string(), + error, + })?; let sysroot_path = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim().to_string()); let mut targets = Vec::new(); - for entry in read_dir(sysroot_path.join("lib").join("rustlib"))?.flatten() { + for entry in read_dir(sysroot_path.join("lib").join("rustlib")) + .fs_context( + "failed to read Rust sysroot", + sysroot_path.join("lib").join("rustlib"), + )? + .flatten() + { if entry.file_type().map(|t| t.is_dir()).unwrap_or_default() { let name = entry.file_name(); if name != "etc" && name != "src" { diff --git a/crates/tauri-cli/src/interface/rust/manifest.rs b/crates/tauri-cli/src/interface/rust/manifest.rs index f4d51a440..452aecaaa 100644 --- a/crates/tauri-cli/src/interface/rust/manifest.rs +++ b/crates/tauri-cli/src/interface/rust/manifest.rs @@ -2,19 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::helpers::{ - app_paths::tauri_dir, - config::{Config, PatternKind}, +use crate::{ + error::{Context, ErrorExt}, + helpers::{ + app_paths::tauri_dir, + config::{Config, PatternKind}, + }, }; -use anyhow::Context; use itertools::Itertools; use toml_edit::{Array, DocumentMut, InlineTable, Item, TableLike, Value}; use std::{ collections::{HashMap, HashSet}, - fs::File, - io::Write, path::Path, }; @@ -84,11 +84,11 @@ fn get_enabled_features(list: &HashMap>, feature: &str) -> V pub fn read_manifest(manifest_path: &Path) -> crate::Result<(DocumentMut, String)> { let manifest_str = std::fs::read_to_string(manifest_path) - .with_context(|| format!("Failed to read `{manifest_path:?}` file"))?; + .fs_context("failed to read Cargo.toml", manifest_path.to_path_buf())?; let manifest: DocumentMut = manifest_str .parse::() - .with_context(|| "Failed to parse Cargo.toml")?; + .context("failed to parse Cargo.toml")?; Ok((manifest, manifest_str)) } @@ -172,10 +172,10 @@ fn write_features bool>( *dep = Value::InlineTable(def); } _ => { - return Err(anyhow::anyhow!( + crate::error::bail!( "Unsupported {} dependency format on Cargo.toml", dependency_name - )) + ); } } Ok(true) @@ -313,10 +313,8 @@ pub fn rewrite_manifest(config: &Config) -> crate::Result<(Manifest, bool)> { let new_manifest_str = serialize_manifest(&manifest); if persist && original_manifest_str != new_manifest_str { - let mut manifest_file = - File::create(&manifest_path).with_context(|| "failed to open Cargo.toml for rewrite")?; - manifest_file.write_all(new_manifest_str.as_bytes())?; - manifest_file.flush()?; + std::fs::write(&manifest_path, new_manifest_str) + .fs_context("failed to rewrite Cargo manifest", manifest_path.clone())?; Ok(( Manifest { inner: manifest, diff --git a/crates/tauri-cli/src/lib.rs b/crates/tauri-cli/src/lib.rs index bab4146a9..b473f63a7 100644 --- a/crates/tauri-cli/src/lib.rs +++ b/crates/tauri-cli/src/lib.rs @@ -10,15 +10,13 @@ )] #![cfg(any(target_os = "macos", target_os = "linux", windows))] -use anyhow::Context; -pub use anyhow::Result; - mod acl; mod add; mod build; mod bundle; mod completions; mod dev; +mod error; mod helpers; mod icon; mod info; @@ -34,6 +32,7 @@ mod signer; use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum}; use env_logger::fmt::style::{AnsiColor, Style}; use env_logger::Builder; +pub use error::{Error, ErrorExt, Result}; use log::Level; use serde::{Deserialize, Serialize}; use std::io::{BufReader, Write}; @@ -48,39 +47,46 @@ use std::{ sync::{Arc, Mutex}, }; +use crate::error::Context; + /// Tauri configuration argument option. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigValue(pub(crate) serde_json::Value); impl FromStr for ConfigValue { - type Err = anyhow::Error; + type Err = Error; fn from_str(config: &str) -> std::result::Result { if config.starts_with('{') { - Ok(Self( - serde_json::from_str(config).context("invalid configuration JSON")?, - )) + Ok(Self(serde_json::from_str(config).with_context(|| { + format!("failed to parse config `{config}` as JSON") + })?)) } else { let path = PathBuf::from(config); - if path.exists() { - let raw = &read_to_string(&path) - .with_context(|| format!("invalid configuration at file {config}"))?; - match path.extension() { - Some(ext) if ext == "toml" => Ok(Self(::toml::from_str(raw)?)), - Some(ext) if ext == "json5" => Ok(Self(::json5::from_str(raw)?)), - // treat all other extensions as json - _ => Ok(Self( - // from tauri-utils/src/config/parse.rs: - // we also want to support **valid** json5 in the .json extension - // if the json5 is not valid the serde_json error for regular json will be returned. - match ::json5::from_str(raw) { - Ok(json5) => json5, - Err(_) => serde_json::from_str(raw)?, - }, - )), + let raw = + read_to_string(&path).fs_context("failed to read configuration file", path.clone())?; + match path.extension() { + Some(ext) if ext == "toml" => { + Ok(Self(::toml::from_str(&raw).with_context(|| { + format!("failed to parse config at {} as TOML", path.display()) + })?)) } - } else { - anyhow::bail!("provided configuration path does not exist") + Some(ext) if ext == "json5" => { + Ok(Self(::json5::from_str(&raw).with_context(|| { + format!("failed to parse config at {} as JSON5", path.display()) + })?)) + } + // treat all other extensions as json + _ => Ok(Self( + // from tauri-utils/src/config/parse.rs: + // we also want to support **valid** json5 in the .json extension + // if the json5 is not valid the serde_json error for regular json will be returned. + match ::json5::from_str(&raw) { + Ok(json5) => json5, + Err(_) => serde_json::from_str(&raw) + .with_context(|| format!("failed to parse config at {} as JSON", path.display()))?, + }, + )), } } } @@ -190,19 +196,7 @@ where A: Into + Clone, { if let Err(e) = try_run(args, bin_name) { - let mut message = e.to_string(); - if e.chain().count() > 1 { - message.push(':'); - } - e.chain().skip(1).for_each(|cause| { - let m = cause.to_string(); - if !message.contains(&m) { - message.push('\n'); - message.push_str(" - "); - message.push_str(&m); - } - }); - log::error!("{message}"); + log::error!("{e}"); exit(1); } } @@ -346,12 +340,19 @@ impl CommandExt for Command { fn output_ok(&mut self) -> crate::Result { let program = self.get_program().to_string_lossy().into_owned(); - log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}"))); + let args = self + .get_args() + .map(|arg| arg.to_string_lossy()) + .fold(String::new(), |acc, arg| format!("{acc} {arg}")); + let cmdline = format!("{program} {args}"); + log::debug!(action = "Running"; "Command `{cmdline}`"); self.stdout(Stdio::piped()); self.stderr(Stdio::piped()); - let mut child = self.spawn()?; + let mut child = self + .spawn() + .with_context(|| format!("failed to run command `{cmdline}`"))?; let mut stdout = child.stdout.take().map(BufReader::new).unwrap(); let stdout_lines = Arc::new(Mutex::new(Vec::new())); @@ -391,7 +392,9 @@ impl CommandExt for Command { } }); - let status = child.wait()?; + let status = child + .wait() + .with_context(|| format!("failed to run command `{cmdline}`"))?; let output = Output { status, @@ -402,7 +405,10 @@ impl CommandExt for Command { if output.status.success() { Ok(output) } else { - Err(anyhow::anyhow!("failed to run {}", program)) + crate::error::bail!( + "failed to run command `{cmdline}`: command exited with status code {}", + output.status.code().unwrap_or(-1) + ); } } } diff --git a/crates/tauri-cli/src/migrate/migrations/v1/config.rs b/crates/tauri-cli/src/migrate/migrations/v1/config.rs index e6e8d8593..2d7d1be4b 100644 --- a/crates/tauri-cli/src/migrate/migrations/v1/config.rs +++ b/crates/tauri-cli/src/migrate/migrations/v1/config.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::Result; +use crate::{error::Context, ErrorExt, Result}; use serde_json::{Map, Value}; use tauri_utils::acl::{ @@ -22,9 +22,17 @@ pub fn migrate(tauri_dir: &Path) -> Result { { let migrated = migrate_config(&mut config)?; if config_path.extension().is_some_and(|ext| ext == "toml") { - fs::write(&config_path, toml::to_string_pretty(&config)?)?; + fs::write( + &config_path, + toml::to_string_pretty(&config).context("failed to serialize config")?, + ) + .fs_context("failed to write config", config_path.clone())?; } else { - fs::write(&config_path, serde_json::to_string_pretty(&config)?)?; + fs::write( + &config_path, + serde_json::to_string_pretty(&config).context("failed to serialize config")?, + ) + .fs_context("failed to write config", config_path.clone())?; } let mut permissions: Vec = vec!["core:default"] @@ -34,7 +42,10 @@ pub fn migrate(tauri_dir: &Path) -> Result { permissions.extend(migrated.permissions.clone()); let capabilities_path = config_path.parent().unwrap().join("capabilities"); - fs::create_dir_all(&capabilities_path)?; + fs::create_dir_all(&capabilities_path).fs_context( + "failed to create capabilities directory", + capabilities_path.clone(), + )?; fs::write( capabilities_path.join("migrated.json"), serde_json::to_string_pretty(&Capability { @@ -46,7 +57,12 @@ pub fn migrate(tauri_dir: &Path) -> Result { webviews: vec![], permissions, platforms: None, - })?, + }) + .context("failed to serialize capabilities")?, + ) + .fs_context( + "failed to write capabilities", + capabilities_path.join("migrated.json"), )?; return Ok(migrated); @@ -375,7 +391,8 @@ fn process_security(security: &mut Map) -> Result<()> { let csp = if csp_value.is_null() { csp_value } else { - let mut csp: tauri_utils::config_v1::Csp = serde_json::from_value(csp_value)?; + let mut csp: tauri_utils::config_v1::Csp = + serde_json::from_value(csp_value).context("failed to deserialize CSP")?; match &mut csp { tauri_utils::config_v1::Csp::Policy(csp) => { if csp.contains("connect-src") { @@ -399,7 +416,7 @@ fn process_security(security: &mut Map) -> Result<()> { } } } - serde_json::to_value(csp)? + serde_json::to_value(csp).context("failed to serialize CSP")? }; security.insert("csp".into(), csp); @@ -423,7 +440,8 @@ fn process_allowlist( tauri_config: &mut Map, allowlist: Value, ) -> Result { - let allowlist: tauri_utils::config_v1::AllowlistConfig = serde_json::from_value(allowlist)?; + let allowlist: tauri_utils::config_v1::AllowlistConfig = + serde_json::from_value(allowlist).context("failed to deserialize allowlist")?; if allowlist.protocol.asset_scope != Default::default() { let security = tauri_config @@ -435,7 +453,8 @@ fn process_allowlist( let mut asset_protocol = Map::new(); asset_protocol.insert( "scope".into(), - serde_json::to_value(allowlist.protocol.asset_scope.clone())?, + serde_json::to_value(allowlist.protocol.asset_scope.clone()) + .context("failed to serialize asset scope")?, ); if allowlist.protocol.asset { asset_protocol.insert("enable".into(), true.into()); @@ -639,7 +658,10 @@ fn allowlist_to_permissions( fn process_cli(plugins: &mut Map, cli: Value) -> Result<()> { if let Some(cli) = cli.as_object() { - plugins.insert("cli".into(), serde_json::to_value(cli)?); + plugins.insert( + "cli".into(), + serde_json::to_value(cli).context("failed to serialize CLI")?, + ); } Ok(()) } @@ -663,7 +685,10 @@ fn process_updater( .unwrap_or_default() || updater.get("pubkey").is_some() { - plugins.insert("updater".into(), serde_json::to_value(updater)?); + plugins.insert( + "updater".into(), + serde_json::to_value(updater).context("failed to serialize updater")?, + ); migrated.plugins.insert("updater".to_string()); } } diff --git a/crates/tauri-cli/src/migrate/migrations/v1/frontend.rs b/crates/tauri-cli/src/migrate/migrations/v1/frontend.rs index 03cdaba2f..db73085ce 100644 --- a/crates/tauri-cli/src/migrate/migrations/v1/frontend.rs +++ b/crates/tauri-cli/src/migrate/migrations/v1/frontend.rs @@ -3,10 +3,10 @@ // SPDX-License-Identifier: MIT use crate::{ + error::Context, helpers::{app_paths::walk_builder, npm::PackageManager}, - Result, + Error, ErrorExt, Result, }; -use anyhow::Context; use itertools::Itertools; use magic_string::MagicString; use oxc_allocator::Allocator; @@ -101,7 +101,8 @@ pub fn migrate(frontend_dir: &Path) -> Result> { let path = entry.path(); let ext = path.extension().unwrap_or_default(); if JS_EXTENSIONS.iter().any(|e| e == &ext) { - let js_contents = std::fs::read_to_string(path)?; + let js_contents = + std::fs::read_to_string(path).fs_context("failed to read JS file", path.to_path_buf())?; let new_contents = migrate_imports( path, &js_contents, @@ -110,7 +111,7 @@ pub fn migrate(frontend_dir: &Path) -> Result> { )?; if new_contents != js_contents { fs::write(path, new_contents) - .with_context(|| format!("Error writing {}", path.display()))?; + .fs_context("failed to write JS file", path.to_path_buf())?; } } } @@ -166,7 +167,7 @@ fn migrate_imports<'a>( let allocator = Allocator::default(); let ret = Parser::new(&allocator, js_source, source_type).parse(); if !ret.errors.is_empty() { - anyhow::bail!( + crate::error::bail!( "failed to parse {} as valid Javascript/Typescript file", path.display() ) @@ -193,8 +194,12 @@ fn migrate_imports<'a>( new_module, Default::default(), ) - .map_err(|e| anyhow::anyhow!("{e}")) - .context("failed to replace import source")?; + .map_err(|e| { + Error::Context( + "failed to replace import source".to_string(), + e.to_string().into(), + ) + })?; // if module was pluginified, add to packages if let Some(plugin_name) = new_module.strip_prefix("@tauri-apps/plugin-") { @@ -279,8 +284,12 @@ fn migrate_imports<'a>( new_identifier, Default::default(), ) - .map_err(|e| anyhow::anyhow!("{e}")) - .context("failed to rename identifier")?; + .map_err(|e| { + Error::Context( + "failed to rename identifier".to_string(), + e.to_string().into(), + ) + })?; } else { // if None, we need to remove this specifier, // it will also be replaced with an import from its new plugin below @@ -297,8 +306,12 @@ fn migrate_imports<'a>( magic_js_source .remove(script_start + start as i64, script_start + end as i64) - .map_err(|e| anyhow::anyhow!("{e}")) - .context("failed to remove identifier")?; + .map_err(|e| { + Error::Context( + "failed to remove identifier".to_string(), + e.to_string().into(), + ) + })?; } } } @@ -322,8 +335,7 @@ fn migrate_imports<'a>( for import in imports_to_add { magic_js_source .append_right(script_start as u32 + start, &import) - .map_err(|e| anyhow::anyhow!("{e}")) - .context("failed to add import")?; + .map_err(|e| Error::Context("failed to add import".to_string(), e.to_string().into()))?; } } @@ -331,8 +343,9 @@ fn migrate_imports<'a>( for stmt in stmts_to_add { magic_js_source .append_right(script_start as u32 + start, stmt) - .map_err(|e| anyhow::anyhow!("{e}")) - .context("failed to add statement")?; + .map_err(|e| { + Error::Context("failed to add statement".to_string(), e.to_string().into()) + })?; } } } diff --git a/crates/tauri-cli/src/migrate/migrations/v1/manifest.rs b/crates/tauri-cli/src/migrate/migrations/v1/manifest.rs index 07a312e8c..d4d03f38d 100644 --- a/crates/tauri-cli/src/migrate/migrations/v1/manifest.rs +++ b/crates/tauri-cli/src/migrate/migrations/v1/manifest.rs @@ -3,11 +3,11 @@ // SPDX-License-Identifier: MIT use crate::{ + error::ErrorExt, interface::rust::manifest::{read_manifest, serialize_manifest}, Result, }; -use anyhow::Context; use tauri_utils::config_v1::Allowlist; use toml_edit::{DocumentMut, Entry, Item, TableLike, Value}; @@ -21,7 +21,7 @@ pub fn migrate(tauri_dir: &Path) -> Result<()> { migrate_manifest(&mut manifest)?; std::fs::write(&manifest_path, serialize_manifest(&manifest)) - .context("failed to rewrite Cargo manifest")?; + .fs_context("failed to rewrite Cargo manifest", manifest_path.clone())?; Ok(()) } diff --git a/crates/tauri-cli/src/migrate/migrations/v1/mod.rs b/crates/tauri-cli/src/migrate/migrations/v1/mod.rs index 8be16e3e2..14a9cd7c4 100644 --- a/crates/tauri-cli/src/migrate/migrations/v1/mod.rs +++ b/crates/tauri-cli/src/migrate/migrations/v1/mod.rs @@ -3,12 +3,11 @@ // SPDX-License-Identifier: MIT use crate::{ + error::Context, helpers::app_paths::{frontend_dir, tauri_dir}, Result, }; -use anyhow::Context; - mod config; mod frontend; mod manifest; diff --git a/crates/tauri-cli/src/migrate/migrations/v2_beta.rs b/crates/tauri-cli/src/migrate/migrations/v2_beta.rs index 5ea326d7a..6910ba116 100644 --- a/crates/tauri-cli/src/migrate/migrations/v2_beta.rs +++ b/crates/tauri-cli/src/migrate/migrations/v2_beta.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT use crate::{ + error::{Context, ErrorExt}, helpers::{ app_paths::{frontend_dir, tauri_dir}, npm::PackageManager, @@ -13,7 +14,6 @@ use crate::{ use std::{fs::read_to_string, path::Path}; -use anyhow::Context; use toml_edit::{DocumentMut, Item, Table, TableLike, Value}; pub fn run() -> Result<()> { @@ -28,8 +28,10 @@ pub fn run() -> Result<()> { migrate_npm_dependencies(frontend_dir)?; - std::fs::write(&manifest_path, serialize_manifest(&manifest)) - .context("failed to rewrite Cargo manifest")?; + std::fs::write(&manifest_path, serialize_manifest(&manifest)).fs_context( + "failed to rewrite Cargo manifest", + manifest_path.to_path_buf(), + )?; Ok(()) } @@ -97,14 +99,19 @@ fn migrate_permissions(tauri_dir: &Path) -> Result<()> { ]; for entry in walkdir::WalkDir::new(tauri_dir.join("capabilities")) { - let entry = entry?; + let entry = entry.map_err(std::io::Error::other).fs_context( + "failed to walk capabilities directory", + tauri_dir.join("capabilities"), + )?; let path = entry.path(); if path.extension().is_some_and(|ext| ext == "json") { - let mut capability = read_to_string(path).context("failed to read capability")?; + let mut capability = + read_to_string(path).fs_context("failed to read capability", path.to_path_buf())?; for plugin in core_plugins { capability = capability.replace(&format!("\"{plugin}:"), &format!("\"core:{plugin}:")); } - std::fs::write(path, capability).context("failed to rewrite capability")?; + std::fs::write(path, capability) + .fs_context("failed to rewrite capability", path.to_path_buf())?; } } Ok(()) diff --git a/crates/tauri-cli/src/migrate/mod.rs b/crates/tauri-cli/src/migrate/mod.rs index 629678f76..575a2516b 100644 --- a/crates/tauri-cli/src/migrate/mod.rs +++ b/crates/tauri-cli/src/migrate/mod.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT use crate::{ + error::{bail, Context, ErrorExt}, helpers::{ app_paths::tauri_dir, cargo_manifest::{crate_version, CargoLock, CargoManifest}, @@ -13,8 +14,6 @@ use crate::{ use std::{fs::read_to_string, str::FromStr}; -use anyhow::{bail, Context}; - mod migrations; pub fn command() -> Result<()> { @@ -22,17 +21,24 @@ pub fn command() -> Result<()> { let tauri_dir = tauri_dir(); - let manifest_contents = - read_to_string(tauri_dir.join("Cargo.toml")).context("failed to read Cargo manifest")?; - let manifest = toml::from_str::(&manifest_contents) - .context("failed to parse Cargo manifest")?; + let manifest_contents = read_to_string(tauri_dir.join("Cargo.toml")).fs_context( + "failed to read Cargo manifest", + tauri_dir.join("Cargo.toml"), + )?; + let manifest = toml::from_str::(&manifest_contents).with_context(|| { + format!( + "failed to parse Cargo manifest {}", + tauri_dir.join("Cargo.toml").display() + ) + })?; let workspace_dir = get_workspace_dir()?; let lock_path = workspace_dir.join("Cargo.lock"); let lock = if lock_path.exists() { - let lockfile_contents = read_to_string(lock_path).context("failed to read Cargo lockfile")?; - let lock = - toml::from_str::(&lockfile_contents).context("failed to parse Cargo lockfile")?; + let lockfile_contents = + read_to_string(&lock_path).fs_context("failed to read Cargo lockfile", &lock_path)?; + let lock = toml::from_str::(&lockfile_contents) + .with_context(|| format!("failed to parse Cargo lockfile {}", lock_path.display()))?; Some(lock) } else { None @@ -41,7 +47,8 @@ pub fn command() -> Result<()> { let tauri_version = crate_version(tauri_dir, Some(&manifest), lock.as_ref(), "tauri") .version .context("failed to get tauri version")?; - let tauri_version = semver::Version::from_str(&tauri_version)?; + let tauri_version = semver::Version::from_str(&tauri_version) + .with_context(|| format!("failed to parse tauri version {tauri_version}"))?; if tauri_version.major == 1 { migrations::v1::run().context("failed to migrate from v1")?; diff --git a/crates/tauri-cli/src/mobile/android/android_studio_script.rs b/crates/tauri-cli/src/mobile/android/android_studio_script.rs index 93edf027d..c5d447d7a 100644 --- a/crates/tauri-cli/src/mobile/android/android_studio_script.rs +++ b/crates/tauri-cli/src/mobile/android/android_studio_script.rs @@ -4,14 +4,14 @@ use super::{detect_target_ok, ensure_init, env, get_app, get_config, read_options, MobileTarget}; use crate::{ + error::{Context, ErrorExt}, helpers::config::{get as get_tauri_config, reload as reload_tauri_config}, interface::{AppInterface, Interface}, mobile::CliOptions, - Result, + Error, Result, }; use clap::{ArgAction, Parser}; -use anyhow::Context; use cargo_mobile2::{ android::{adb, target::Target}, opts::Profile, @@ -144,17 +144,23 @@ pub fn command(options: Options) -> Result<()> { log::info!("Installing target {}", target.triple()); target .install() - .context("failed to install target with rustup")?; + .map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + }) + .context("failed to install target")?; } - target.build( - &config, - &metadata, - &env, - cli_options.noise_level, - true, - profile, - )?; + target + .build( + &config, + &metadata, + &env, + cli_options.noise_level, + true, + profile, + ) + .context("failed to build Android app")?; if !validated_lib { validated_lib = true; @@ -164,17 +170,17 @@ pub fn command(options: Options) -> Result<()> { .target_dir(target.triple, profile) .join(config.so_name()); - validate_lib(&lib_path)?; + validate_lib(&lib_path).context("failed to validate library")?; } Ok(()) }, ) - .map_err(|e| anyhow::anyhow!(e.to_string()))? + .map_err(|e| Error::GenericError(e.to_string()))? } fn validate_lib(path: &Path) -> Result<()> { - let so_bytes = std::fs::read(path)?; + let so_bytes = std::fs::read(path).fs_context("failed to read library", path.to_path_buf())?; let elf = elf::ElfBytes::::minimal_parse(&so_bytes) .context("failed to parse ELF")?; let (symbol_table, string_table) = elf @@ -190,7 +196,7 @@ fn validate_lib(path: &Path) -> Result<()> { } if !symbols.contains(&"Java_app_tauri_plugin_PluginManager_handlePluginResponse") { - anyhow::bail!( + crate::error::bail!( "Library from {} does not include required runtime symbols. This means you are likely missing the tauri::mobile_entry_point macro usage, see the documentation for more information: https://v2.tauri.app/start/migrate/from-tauri-1", path.display() ); @@ -237,7 +243,7 @@ fn adb_forward_port( let device = devices.first().unwrap(); Some((device.serial_no().to_string(), device.name().to_string())) } else if devices.len() > 1 { - anyhow::bail!("Multiple Android devices are connected ({}), please disconnect devices you do not intend to use so Tauri can determine which to use", + crate::error::bail!("Multiple Android devices are connected ({}), please disconnect devices you do not intend to use so Tauri can determine which to use", devices.iter().map(|d| d.name()).collect::>().join(", ")); } else { // when building the app without running to a device, we might have an empty devices list @@ -249,7 +255,11 @@ fn adb_forward_port( // clear port forwarding for all devices for device in &devices { - let reverse_list_output = adb_reverse_list(env, device.serial_no())?; + let reverse_list_output = + adb_reverse_list(env, device.serial_no()).map_err(|error| Error::CommandFailed { + command: "adb reverse --list".to_string(), + error, + })?; // check if the device has the port forwarded if String::from_utf8_lossy(&reverse_list_output.stdout).contains(&forward) { @@ -271,11 +281,20 @@ fn adb_forward_port( log::info!("{forward} already forwarded to {target_device_name}"); } else { loop { - run_adb_reverse(env, &target_device_serial_no, &forward, &forward).with_context(|| { - format!("failed to forward port with adb, is the {target_device_name} device connected?",) + run_adb_reverse(env, &target_device_serial_no, &forward, &forward).map_err(|error| { + Error::CommandFailed { + command: format!("adb reverse {forward} {forward}"), + error, + } })?; - let reverse_list_output = adb_reverse_list(env, &target_device_serial_no)?; + let reverse_list_output = + adb_reverse_list(env, &target_device_serial_no).map_err(|error| { + Error::CommandFailed { + command: "adb reverse --list".to_string(), + error, + } + })?; // wait and retry until the port has actually been forwarded if String::from_utf8_lossy(&reverse_list_output.stdout).contains(&forward) { break; diff --git a/crates/tauri-cli/src/mobile/android/build.rs b/crates/tauri-cli/src/mobile/android/build.rs index da3f2e0c4..e5abf6ef1 100644 --- a/crates/tauri-cli/src/mobile/android/build.rs +++ b/crates/tauri-cli/src/mobile/android/build.rs @@ -8,6 +8,7 @@ use super::{ }; use crate::{ build::Options as BuildOptions, + error::Context, helpers::{ app_paths::tauri_dir, config::{get as get_tauri_config, ConfigHandle}, @@ -15,11 +16,10 @@ use crate::{ }, interface::{AppInterface, Interface, Options as InterfaceOptions}, mobile::{write_options, CliOptions}, - ConfigValue, Result, + ConfigValue, Error, Result, }; use clap::{ArgAction, Parser}; -use anyhow::Context; use cargo_mobile2::{ android::{aab, apk, config::Config as AndroidConfig, env::Env, target::Target}, opts::{NoiseLevel, Profile}, @@ -154,7 +154,7 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { }; let tauri_path = tauri_dir(); - set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?; + set_current_dir(tauri_path).context("failed to set current directory to Tauri directory")?; ensure_init( &tauri_config, @@ -175,10 +175,16 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { log::info!("Installing target {}", first_target.triple()); first_target .install() - .context("failed to install target with rustup")?; + .map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + }) + .context("failed to install target")?; } // run an initial build to initialize plugins - first_target.build(&config, &metadata, &env, noise_level, true, profile)?; + first_target + .build(&config, &metadata, &env, noise_level, true, profile) + .context("failed to build Android app")?; let open = options.open; let _handle = run_build( @@ -248,7 +254,8 @@ fn run_build( profile, get_targets_or_all(options.targets.clone().unwrap_or_default())?, options.split_per_abi, - )? + ) + .context("failed to build APK")? } else { Vec::new() }; @@ -261,7 +268,8 @@ fn run_build( profile, get_targets_or_all(options.targets.unwrap_or_default())?, options.split_per_abi, - )? + ) + .context("failed to build AAB")? } else { Vec::new() }; @@ -285,12 +293,8 @@ fn get_targets_or_all<'a>(targets: Vec) -> Result>> { .join(","); for t in targets { - let target = Target::for_name(&t).ok_or_else(|| { - anyhow::anyhow!( - "Target {} is invalid; the possible targets are {}", - t, - possible_targets - ) + let target = Target::for_name(&t).with_context(|| { + format!("Target {t} is invalid; the possible targets are {possible_targets}",) })?; outs.push(target); } diff --git a/crates/tauri-cli/src/mobile/android/dev.rs b/crates/tauri-cli/src/mobile/android/dev.rs index fcf16b204..e5a3b2124 100644 --- a/crates/tauri-cli/src/mobile/android/dev.rs +++ b/crates/tauri-cli/src/mobile/android/dev.rs @@ -8,6 +8,7 @@ use super::{ }; use crate::{ dev::Options as DevOptions, + error::{Context, ErrorExt}, helpers::{ app_paths::tauri_dir, config::{get as get_tauri_config, ConfigHandle}, @@ -18,11 +19,10 @@ use crate::{ use_network_address_for_dev_url, write_options, CliOptions, DevChild, DevHost, DevProcess, TargetDevice, }, - ConfigValue, Result, + ConfigValue, Error, Result, }; use clap::{ArgAction, Parser}; -use anyhow::Context; use cargo_mobile2::{ android::{ config::{Config as AndroidConfig, Metadata as AndroidMetadata}, @@ -145,7 +145,10 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> { if let Some(root_certificate_path) = &options.root_certificate_path { std::env::set_var( "TAURI_DEV_ROOT_CERTIFICATE", - std::fs::read_to_string(root_certificate_path).context("failed to read certificate file")?, + std::fs::read_to_string(root_certificate_path).fs_context( + "failed to read certificate file", + root_certificate_path.clone(), + )?, ); } @@ -195,7 +198,7 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> { }; let tauri_path = tauri_dir(); - set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?; + set_current_dir(tauri_path).context("failed to set current directory to Tauri directory")?; ensure_init( &tauri_config, @@ -263,23 +266,26 @@ fn run_dev( .unwrap_or_else(|| Target::all().values().next().unwrap()); if !installed_targets.contains(&target.triple().into()) { log::info!("Installing target {}", target.triple()); - target - .install() - .context("failed to install target with rustup")?; + target.install().map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + })?; } - target.build( - config, - metadata, - &env, - noise_level, - true, - if options.release_mode { - Profile::Release - } else { - Profile::Debug - }, - )?; + target + .build( + config, + metadata, + &env, + noise_level, + true, + if options.release_mode { + Profile::Release + } else { + Profile::Debug + }, + ) + .context("failed to build Android app")?; let open = options.open; interface.mobile_dev( @@ -358,5 +364,5 @@ fn run( ".MainActivity".into(), ) .map(DevChild::new) - .map_err(Into::into) + .context("failed to run Android app") } diff --git a/crates/tauri-cli/src/mobile/android/mod.rs b/crates/tauri-cli/src/mobile/android/mod.rs index 1d72e2f13..43154ea2c 100644 --- a/crates/tauri-cli/src/mobile/android/mod.rs +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -35,8 +35,9 @@ use super::{ OptionsHandle, Target as MobileTarget, MIN_DEVICE_MATCH_SCORE, }; use crate::{ + error::Context, helpers::config::{BundleResources, Config as TauriConfig}, - ConfigValue, Result, + ConfigValue, Error, ErrorExt, Result, }; mod android_studio_script; @@ -192,28 +193,33 @@ pub fn get_config( } pub fn env(non_interactive: bool) -> Result { - let env = super::env()?; - ensure_env(non_interactive)?; - cargo_mobile2::android::env::Env::from_env(env).map_err(Into::into) + let env = super::env().context("failed to setup Android environment")?; + ensure_env(non_interactive).context("failed to ensure Android environment")?; + cargo_mobile2::android::env::Env::from_env(env).context("failed to load Android environment") } fn download_cmdline_tools(extract_path: &Path) -> Result<()> { log::info!("Downloading Android command line tools..."); - let mut response = crate::helpers::http::get(CMDLINE_TOOLS_URL)?; + let mut response = crate::helpers::http::get(CMDLINE_TOOLS_URL) + .context("failed to download Android command line tools")?; let body = response .body_mut() .with_config() .limit(200 * 1024 * 1024 /* 200MB */) - .read_to_vec()?; + .read_to_vec() + .context("failed to read Android command line tools download response")?; - let mut zip = zip::ZipArchive::new(Cursor::new(body))?; + let mut zip = zip::ZipArchive::new(Cursor::new(body)) + .context("failed to create zip archive from Android command line tools download response")?; log::info!( "Extracting Android command line tools to {}", extract_path.display() ); - zip.extract(extract_path)?; + zip + .extract(extract_path) + .context("failed to extract Android command line tools")?; Ok(()) } @@ -238,7 +244,7 @@ fn ensure_java() -> Result<()> { log::info!("Using Android Studio's default Java installation: {default_java_home}"); std::env::set_var("JAVA_HOME", default_java_home); } else if which::which("java").is_err() { - anyhow::bail!("Java not found in PATH, default Android Studio Java installation not found at {default_java_home} and JAVA_HOME environment variable not set. Please install Java before proceeding"); + crate::error::bail!("Java not found in PATH, default Android Studio Java installation not found at {default_java_home} and JAVA_HOME environment variable not set. Please install Java before proceeding"); } } @@ -272,7 +278,7 @@ fn ensure_sdk(non_interactive: bool) -> Result<()> { default_android_home.display() ); } else if non_interactive { - anyhow::bail!("Android SDK not found. Make sure the SDK and NDK are installed and the ANDROID_HOME and NDK_HOME environment variables are set."); + crate::error::bail!("Android SDK not found. Make sure the SDK and NDK are installed and the ANDROID_HOME and NDK_HOME environment variables are set."); } else { log::error!( "Android SDK not found at {}", @@ -282,7 +288,7 @@ fn ensure_sdk(non_interactive: bool) -> Result<()> { let extract_path = if create_dir_all(&default_android_home).is_ok() { default_android_home.clone() } else { - std::env::current_dir()? + std::env::current_dir().context("failed to get current directory")? }; let sdk_manager_path = extract_path @@ -299,7 +305,7 @@ fn ensure_sdk(non_interactive: bool) -> Result<()> { .unwrap_or_default(); if !granted_permission_to_install { - anyhow::bail!("Skipping Android Studio command line tools installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android"); + crate::error::bail!("Skipping Android Studio command line tools installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android"); } download_cmdline_tools(&extract_path)?; @@ -313,7 +319,7 @@ fn ensure_sdk(non_interactive: bool) -> Result<()> { .unwrap_or_default(); if !granted_permission_to_install { - anyhow::bail!("Skipping Android Studio SDK installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android"); + crate::error::bail!("Skipping Android Studio SDK installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android"); } } @@ -324,10 +330,14 @@ fn ensure_sdk(non_interactive: bool) -> Result<()> { .arg("platform-tools") .arg(format!("platforms;android-{SDK_VERSION}")) .arg(format!("ndk;{NDK_VERSION}")) - .status()?; + .status() + .map_err(|error| Error::CommandFailed { + command: format!("{} --sdk_root={} --install platform-tools platforms;android-{SDK_VERSION} ndk;{NDK_VERSION}", sdk_manager_path.display(), default_android_home.display()), + error, + })?; if !status.success() { - anyhow::bail!("Failed to install Android SDK"); + crate::error::bail!("Failed to install Android SDK"); } } @@ -342,7 +352,7 @@ fn ensure_ndk(non_interactive: bool) -> Result<()> { let android_home = std::env::var_os("ANDROID_HOME") .map(PathBuf::from) .or_else(|| std::env::var_os("ANDROID_SDK_ROOT").map(PathBuf::from)) - .ok_or_else(|| anyhow::anyhow!("Failed to locate Android SDK"))?; + .context("Failed to locate Android SDK")?; let mut installed_ndks = read_dir(android_home.join("ndk")) .map(|dir| { dir @@ -357,7 +367,7 @@ fn ensure_ndk(non_interactive: bool) -> Result<()> { log::info!("Using installed NDK: {}", ndk.display()); std::env::set_var("NDK_HOME", ndk); } else if non_interactive { - anyhow::bail!("Android NDK not found. Make sure the NDK is installed and the NDK_HOME environment variable is set."); + crate::error::bail!("Android NDK not found. Make sure the NDK is installed and the NDK_HOME environment variable is set."); } else { let sdk_manager_path = android_home .join("cmdline-tools/bin/sdkmanager") @@ -373,7 +383,7 @@ fn ensure_ndk(non_interactive: bool) -> Result<()> { .unwrap_or_default(); if !granted_permission_to_install { - anyhow::bail!("Skipping Android Studio command line tools installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android"); + crate::error::bail!("Skipping Android Studio command line tools installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android"); } download_cmdline_tools(&android_home)?; @@ -387,7 +397,7 @@ fn ensure_ndk(non_interactive: bool) -> Result<()> { .unwrap_or_default(); if !granted_permission_to_install { - anyhow::bail!("Skipping Android Studio NDK installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android"); + crate::error::bail!("Skipping Android Studio NDK installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android"); } } @@ -399,10 +409,18 @@ fn ensure_ndk(non_interactive: bool) -> Result<()> { .arg(format!("--sdk_root={}", android_home.display())) .arg("--install") .arg(format!("ndk;{NDK_VERSION}")) - .status()?; + .status() + .map_err(|error| Error::CommandFailed { + command: format!( + "{} --sdk_root={} --install ndk;{NDK_VERSION}", + sdk_manager_path.display(), + android_home.display() + ), + error, + })?; if !status.success() { - anyhow::bail!("Failed to install Android NDK"); + crate::error::bail!("Failed to install Android NDK"); } let ndk_path = android_home.join("ndk").join(NDK_VERSION); @@ -422,8 +440,7 @@ fn delete_codegen_vars() { } fn adb_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result> { - let device_list = adb::device_list(env) - .map_err(|cause| anyhow::anyhow!("Failed to detect connected Android devices: {cause}"))?; + let device_list = adb::device_list(env).context("failed to detect connected Android devices")?; if !device_list.is_empty() { let device = if let Some(t) = target { let (device, score) = device_list @@ -439,7 +456,7 @@ fn adb_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result MIN_DEVICE_MATCH_SCORE { device } else { - anyhow::bail!("Could not find an Android device matching {t}") + crate::error::bail!("Could not find an Android device matching {t}") } } else if device_list.len() > 1 { let index = prompt::list( @@ -449,7 +466,7 @@ fn adb_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result(env: &'_ Env, target: Option<&str>) -> Result) -> Result MIN_DEVICE_MATCH_SCORE { device } else { - anyhow::bail!("Could not find an Android Emulator matching {t}") + crate::error::bail!("Could not find an Android Emulator matching {t}") } } else if emulator_list.len() > 1 { let index = prompt::list( @@ -493,7 +512,7 @@ fn emulator_prompt(env: &'_ Env, target: Option<&str>) -> Result) -> Result(env: &'_ Env, target: Option<&str>) -> Result> { } else { let emulator = emulator_prompt(env, target)?; log::info!("Starting emulator {}", emulator.name()); - emulator.start_detached(env)?; + emulator + .start_detached(env) + .context("failed to start emulator")?; let mut tries = 0; loop { sleep(Duration::from_secs(2)); @@ -547,11 +570,15 @@ fn inject_resources(config: &AndroidConfig, tauri_config: &TauriConfig) -> Resul .project_dir() .join("app/src/main") .join(DEFAULT_ASSET_DIR); - create_dir_all(&asset_dir)?; + create_dir_all(&asset_dir).fs_context("failed to create asset directory", asset_dir.clone())?; write( asset_dir.join("tauri.conf.json"), - serde_json::to_string(&tauri_config)?, + serde_json::to_string(&tauri_config).with_context(|| "failed to serialize tauri config")?, + ) + .fs_context( + "failed to write tauri config", + asset_dir.join("tauri.conf.json"), )?; let resources = match &tauri_config.bundle.resources { @@ -561,9 +588,9 @@ fn inject_resources(config: &AndroidConfig, tauri_config: &TauriConfig) -> Resul }; if let Some(resources) = resources { for resource in resources.iter() { - let resource = resource?; + let resource = resource.context("failed to get resource")?; let dest = asset_dir.join(resource.target()); - crate::helpers::fs::copy_file(resource.path(), dest)?; + crate::helpers::fs::copy_file(resource.path(), dest).context("failed to copy resource")?; } } @@ -572,7 +599,9 @@ fn inject_resources(config: &AndroidConfig, tauri_config: &TauriConfig) -> Resul fn configure_cargo(env: &mut Env, config: &AndroidConfig) -> Result<()> { for target in Target::all().values() { - let config = target.generate_cargo_config(config, env)?; + let config = target + .generate_cargo_config(config, env) + .context("failed to find Android tool")?; let target_var_name = target.triple.replace('-', "_").to_uppercase(); if let Some(linker) = config.linker { env.base.insert_env_var( diff --git a/crates/tauri-cli/src/mobile/android/project.rs b/crates/tauri-cli/src/mobile/android/project.rs index ee6f4df0d..570600818 100644 --- a/crates/tauri-cli/src/mobile/android/project.rs +++ b/crates/tauri-cli/src/mobile/android/project.rs @@ -2,8 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::{helpers::template, Result}; -use anyhow::Context; +use crate::{ + error::{Context, ErrorExt}, + helpers::template, + Error, Result, +}; use cargo_mobile2::{ android::{ config::{Config, Metadata}, @@ -48,9 +51,10 @@ pub fn gen( log::info!("Installing Android Rust targets..."); for target in missing_targets { log::info!("Installing target {}", target.triple()); - target - .install() - .context("failed to install target with rustup")?; + target.install().map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + })?; } } } @@ -137,34 +141,17 @@ pub fn gen( let source_src = config.app().root_dir().join(source); let source_file = source_src .file_name() - .ok_or_else(|| anyhow::anyhow!("asset source {} is invalid", source_src.display()))?; - fs::copy(&source_src, source_dest.join(source_file)).map_err(|cause| { - anyhow::anyhow!( - "failed to copy {} to {}: {}", - source_src.display(), - source_dest.display(), - cause - ) - })?; + .with_context(|| format!("asset source {} is invalid", source_src.display()))?; + fs::copy(&source_src, source_dest.join(source_file)) + .fs_context("failed to copy asset", source_src)?; } let dest = prefix_path(dest, "app/src/main/"); - fs::create_dir_all(&dest).map_err(|cause| { - anyhow::anyhow!( - "failed to create directory at {}: {}", - dest.display(), - cause - ) - })?; + fs::create_dir_all(&dest).fs_context("failed to create directory", dest.clone())?; let asset_dir = dest.join(DEFAULT_ASSET_DIR); if !asset_dir.is_dir() { - fs::create_dir_all(&asset_dir).map_err(|cause| { - anyhow::anyhow!( - "failed to create asset dir {path}: {cause}", - path = asset_dir.display() - ) - })?; + fs::create_dir_all(&asset_dir).fs_context("failed to create asset dir", asset_dir)?; } Ok(()) diff --git a/crates/tauri-cli/src/mobile/init.rs b/crates/tauri-cli/src/mobile/init.rs index 9d9356132..4ffa328ad 100644 --- a/crates/tauri-cli/src/mobile/init.rs +++ b/crates/tauri-cli/src/mobile/init.rs @@ -38,8 +38,7 @@ pub fn command( reinstall_deps, skip_targets_install, config, - ) - .map_err(|e| anyhow::anyhow!("{:#}", e))?; + )?; Ok(()) } @@ -311,7 +310,7 @@ fn escape_kotlin_keyword( out.write(&escaped_result).map_err(Into::into) } -fn app_root(ctx: &Context) -> Result<&str, RenderError> { +fn app_root(ctx: &Context) -> std::result::Result<&str, RenderError> { let app_root = ctx .data() .get("app") diff --git a/crates/tauri-cli/src/mobile/ios/build.rs b/crates/tauri-cli/src/mobile/ios/build.rs index 27f06a608..712e7d0a8 100644 --- a/crates/tauri-cli/src/mobile/ios/build.rs +++ b/crates/tauri-cli/src/mobile/ios/build.rs @@ -9,6 +9,7 @@ use super::{ }; use crate::{ build::Options as BuildOptions, + error::{Context, ErrorExt}, helpers::{ app_paths::tauri_dir, config::{get as get_tauri_config, ConfigHandle}, @@ -16,11 +17,10 @@ use crate::{ }, interface::{AppInterface, Interface, Options as InterfaceOptions}, mobile::{ios::ensure_ios_runtime_installed, write_options, CliOptions}, - ConfigValue, Result, + ConfigValue, Error, Result, }; use clap::{ArgAction, Parser, ValueEnum}; -use anyhow::Context; use cargo_mobile2::{ apple::{ config::Config as AppleConfig, @@ -126,7 +126,7 @@ impl std::fmt::Display for ExportMethod { impl std::str::FromStr for ExportMethod { type Err = &'static str; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> std::result::Result { match s { "app-store-connect" => Ok(Self::AppStoreConnect), "release-testing" => Ok(Self::ReleaseTesting), @@ -195,7 +195,7 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { }; let tauri_path = tauri_dir(); - set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?; + set_current_dir(tauri_path).context("failed to set current directory")?; ensure_init( &tauri_config, @@ -221,9 +221,12 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { tauri_path.join("Info.ios.plist").into(), plist::Value::Dictionary(plist).into(), ])?; - merged_info_plist.to_file_xml(&info_plist_path)?; + merged_info_plist + .to_file_xml(&info_plist_path) + .map_err(std::io::Error::other) + .fs_context("failed to save merged Info.plist file", info_plist_path)?; - let mut env = env()?; + let mut env = env().context("failed to load iOS environment")?; if !options.open { ensure_ios_runtime_installed()?; @@ -240,10 +243,10 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { let minor = iter.next().context(format!( "failed to parse Xcode version `{xcode_version}` as semver" ))?; - let major = major.parse::().context(format!( + let major = major.parse::().ok().context(format!( "failed to parse Xcode version `{xcode_version}` as semver: major is not a number" ))?; - let minor = minor.parse::().context(format!( + let minor = minor.parse::().ok().context(format!( "failed to parse Xcode version `{xcode_version}` as semver: minor is not a number" ))?; @@ -268,20 +271,29 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { options.debug, )?; if pbxproj.has_changes() { - pbxproj.save()?; + pbxproj + .save() + .fs_context("failed to save pbxproj file", pbxproj.path)?; } // merge export options and write to temp file let _export_options_tmp = if !export_options_plist.is_empty() { let export_options_plist_path = config.project_dir().join("ExportOptions.plist"); - let export_options = tempfile::NamedTempFile::new()?; + let export_options = + tempfile::NamedTempFile::new().context("failed to create temporary file")?; let merged_plist = merge_plist(vec![ export_options.path().to_owned().into(), export_options_plist_path.clone().into(), plist::Value::from(export_options_plist).into(), ])?; - merged_plist.to_file_xml(export_options.path())?; + merged_plist + .to_file_xml(export_options.path()) + .map_err(std::io::Error::other) + .fs_context( + "failed to save export options plist file", + export_options.path().to_path_buf(), + )?; config.set_export_options_plist_path(export_options.path()); @@ -373,26 +385,31 @@ fn run_build( .skip_codesign(); } - target.build(None, config, env, noise_level, profile, build_config)?; + target + .build(None, config, env, noise_level, profile, build_config) + .context("failed to build iOS app")?; let mut archive_config = ArchiveConfig::new(); if skip_signing { archive_config = archive_config.skip_codesign(); } - target.archive( - config, - env, - noise_level, - profile, - Some(app_version), - archive_config, - )?; + target + .archive( + config, + env, + noise_level, + profile, + Some(app_version), + archive_config, + ) + .context("failed to archive iOS app")?; let out_dir = config.export_dir().join(target.arch); if target.sdk == "iphonesimulator" { - fs::create_dir_all(&out_dir)?; + fs::create_dir_all(&out_dir) + .fs_context("failed to create Xcode output directory", out_dir.clone())?; let app_path = config .archive_dir() @@ -403,7 +420,7 @@ fn run_build( .with_extension("app"); let path = out_dir.join(app_path.file_name().unwrap()); - fs::rename(&app_path, &path)?; + fs::rename(&app_path, &path).fs_context("failed to rename app", app_path)?; out_files.push(path); } else { // if we skipped code signing, we do not have the entitlements applied to our exported IPA @@ -421,12 +438,15 @@ fn run_build( validity_days: 365, password: password.clone(), }, - )?; - let tmp_dir = tempfile::tempdir()?; + ) + .map_err(Box::new)?; + let tmp_dir = tempfile::tempdir().context("failed to create temporary directory")?; let cert_path = tmp_dir.path().join("cert.p12"); - std::fs::write(&cert_path, certificate)?; + std::fs::write(&cert_path, certificate) + .fs_context("failed to write certificate", cert_path.clone())?; let self_signed_cert_keychain = - tauri_macos_sign::Keychain::with_certificate_file(&cert_path, &password.into())?; + tauri_macos_sign::Keychain::with_certificate_file(&cert_path, &password.into()) + .map_err(Box::new)?; let app_dir = config .export_dir() @@ -434,16 +454,18 @@ fn run_build( .join("Products/Applications") .join(format!("{}.app", config.app().stylized_name())); - self_signed_cert_keychain.sign( - &app_dir.join(config.app().stylized_name()), - Some( - &config - .project_dir() - .join(config.scheme()) - .join(format!("{}.entitlements", config.scheme())), - ), - false, - )?; + self_signed_cert_keychain + .sign( + &app_dir.join(config.app().stylized_name()), + Some( + &config + .project_dir() + .join(config.scheme()) + .join(format!("{}.entitlements", config.scheme())), + ), + false, + ) + .map_err(Box::new)?; } let mut export_config = ExportConfig::new().allow_provisioning_updates(); @@ -451,12 +473,15 @@ fn run_build( export_config = export_config.authentication_credentials(credentials.clone()); } - target.export(config, env, noise_level, export_config)?; + target + .export(config, env, noise_level, export_config) + .context("failed to export iOS app")?; if let Ok(ipa_path) = config.ipa_path() { - fs::create_dir_all(&out_dir)?; + fs::create_dir_all(&out_dir) + .fs_context("failed to create Xcode output directory", out_dir.clone())?; let path = out_dir.join(ipa_path.file_name().unwrap()); - fs::rename(&ipa_path, &path)?; + fs::rename(&ipa_path, &path).fs_context("failed to rename IPA", ipa_path)?; out_files.push(path); } } @@ -464,7 +489,7 @@ fn run_build( Ok(()) }, ) - .map_err(|e: TargetInvalid| anyhow::anyhow!(e.to_string()))??; + .map_err(|e: TargetInvalid| Error::GenericError(e.to_string()))??; log_finished(out_files, "iOS Bundle"); @@ -485,7 +510,7 @@ fn auth_credentials_from_env() -> Result Ok(None), - _ => anyhow::bail!( + _ => crate::error::bail!( "APPLE_API_KEY, APPLE_API_ISSUER and APPLE_API_KEY_PATH must be provided for code signing" ), } diff --git a/crates/tauri-cli/src/mobile/ios/dev.rs b/crates/tauri-cli/src/mobile/ios/dev.rs index 5b48e7101..406a72a0b 100644 --- a/crates/tauri-cli/src/mobile/ios/dev.rs +++ b/crates/tauri-cli/src/mobile/ios/dev.rs @@ -8,6 +8,7 @@ use super::{ }; use crate::{ dev::Options as DevOptions, + error::{Context, ErrorExt}, helpers::{ app_paths::tauri_dir, config::{get as get_tauri_config, ConfigHandle}, @@ -22,7 +23,6 @@ use crate::{ }; use clap::{ArgAction, Parser}; -use anyhow::Context; use cargo_mobile2::{ apple::{ config::Config as AppleConfig, @@ -150,11 +150,14 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> { if let Some(root_certificate_path) = &options.root_certificate_path { std::env::set_var( "TAURI_DEV_ROOT_CERTIFICATE", - std::fs::read_to_string(root_certificate_path).context("failed to read certificate file")?, + std::fs::read_to_string(root_certificate_path).fs_context( + "failed to read root certificate file", + root_certificate_path.clone(), + )?, ); } - let env = env()?; + let env = env().context("failed to load iOS environment")?; let device = if options.open { None } else { @@ -200,7 +203,7 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> { }; let tauri_path = tauri_dir(); - set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?; + set_current_dir(tauri_path).context("failed to set current directory to Tauri directory")?; ensure_init( &tauri_config, @@ -219,7 +222,10 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> { tauri_path.join("Info.plist").into(), tauri_path.join("Info.ios.plist").into(), ])?; - merged_info_plist.to_file_xml(&info_plist_path)?; + merged_info_plist + .to_file_xml(&info_plist_path) + .map_err(std::io::Error::other) + .fs_context("failed to save merged Info.plist file", info_plist_path)?; let mut pbxproj = load_pbxproj(&config)?; @@ -237,7 +243,9 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> { !options.release_mode, )?; if pbxproj.has_changes() { - pbxproj.save()?; + pbxproj + .save() + .fs_context("failed to save pbxproj file", pbxproj.path)?; } run_dev( @@ -325,7 +333,7 @@ fn run_dev( } Err(e) => { crate::dev::kill_before_dev_process(); - Err(e.into()) + crate::error::bail!("failed to run iOS app: {}", e) } } } else { diff --git a/crates/tauri-cli/src/mobile/ios/mod.rs b/crates/tauri-cli/src/mobile/ios/mod.rs index ea55d9862..64181b865 100644 --- a/crates/tauri-cli/src/mobile/ios/mod.rs +++ b/crates/tauri-cli/src/mobile/ios/mod.rs @@ -28,12 +28,13 @@ use super::{ OptionsHandle, Target as MobileTarget, MIN_DEVICE_MATCH_SCORE, }; use crate::{ + error::{Context, ErrorExt}, helpers::{ app_paths::tauri_dir, config::{BundleResources, Config as TauriConfig, ConfigHandle}, pbxproj, strip_semver_prerelease_tag, }, - ConfigValue, Result, + ConfigValue, Error, Result, }; use std::{ @@ -223,7 +224,7 @@ pub fn get_config( } 1 => None, _ => { - log::warn!("You must set the code signing certificate development team ID on the `bundle > iOS > developmentTeam` config value or the `{APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME}` environment variable. Available certificates: {}", teams.iter().map(|t| format!("{} (ID: {})", t.name, t.id)).collect::>().join(", ")); + log::warn!("You must set the code signing certificate development team ID on the `bundle > iOS > developmentTeam` config value or the `{APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME}` environment variable. Available certificates: {}", teams.iter().map(|t| format!("{} (ID: {})", t.name, t.id)).collect::>().join(", ")); None } } @@ -234,7 +235,8 @@ pub fn get_config( ios_version: Some(tauri_config.bundle.ios.minimum_system_version.clone()), ..Default::default() }; - let config = AppleConfig::from_raw(app.clone(), Some(raw))?; + let config = AppleConfig::from_raw(app.clone(), Some(raw)) + .context("failed to create Apple configuration")?; let tauri_dir = tauri_dir(); @@ -287,8 +289,9 @@ pub fn get_config( } fn connected_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result> { - let device_list = device::list_devices(env) - .map_err(|cause| anyhow::anyhow!("Failed to detect connected iOS devices: {cause}"))?; + let device_list = device::list_devices(env).map_err(|cause| { + Error::GenericError(format!("Failed to detect connected iOS devices: {cause}")) + })?; if !device_list.is_empty() { let device = if let Some(t) = target { let (device, score) = device_list @@ -304,7 +307,7 @@ fn connected_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result MIN_DEVICE_MATCH_SCORE { device } else { - anyhow::bail!("Could not find an iOS device matching {t}") + crate::error::bail!("Could not find an iOS device matching {t}") } } else { let index = if device_list.len() > 1 { @@ -315,7 +318,7 @@ fn connected_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result(env: &'_ Env, target: Option<&str>) -> Result) -> Result { let simulator_list = device::list_simulators(env).map_err(|cause| { - anyhow::anyhow!("Failed to detect connected iOS Simulator devices: {cause}") + Error::GenericError(format!( + "Failed to detect connected iOS Simulator devices: {cause}" + )) })?; if !simulator_list.is_empty() { let device = if let Some(t) = target { @@ -362,7 +367,7 @@ fn simulator_prompt(env: &'_ Env, target: Option<&str>) -> Result MIN_DEVICE_MATCH_SCORE { device } else { - anyhow::bail!("Could not find an iOS Simulator matching {t}") + crate::error::bail!("Could not find an iOS Simulator matching {t}") } } else if simulator_list.len() > 1 { let index = prompt::list( @@ -372,7 +377,7 @@ fn simulator_prompt(env: &'_ Env, target: Option<&str>) -> Result) -> Result(env: &'_ Env, target: Option<&str>) -> Result> { } else { let simulator = simulator_prompt(env, target)?; log::info!("Starting simulator {}", simulator.name()); - simulator.start_detached(env)?; + simulator + .start_detached(env) + .context("failed to start simulator")?; Ok(simulator.into()) } } fn ensure_ios_runtime_installed() -> Result<()> { - let installed_platforms_json = - duct::cmd("xcrun", ["simctl", "list", "runtimes", "--json"]).read()?; + let installed_platforms_json = duct::cmd("xcrun", ["simctl", "list", "runtimes", "--json"]) + .read() + .map_err(|e| Error::CommandFailed { + command: "xcrun simctl list runtimes --json".to_string(), + error: e, + })?; let installed_platforms: InstalledRuntimesList = serde_json::from_str(&installed_platforms_json).unwrap_or_default(); if !installed_platforms @@ -427,9 +442,13 @@ fn ensure_ios_runtime_installed() -> Result<()> { duct::cmd("xcodebuild", ["-downloadPlatform", "iOS"]) .stdout_file(os_pipe::dup_stdout().unwrap()) .stderr_file(os_pipe::dup_stderr().unwrap()) - .run()?; + .run() + .map_err(|e| Error::CommandFailed { + command: "xcodebuild -downloadPlatform iOS".to_string(), + error: e, + })?; } else { - anyhow::bail!("iOS platform not installed"); + crate::error::bail!("iOS platform not installed"); } } Ok(()) @@ -451,7 +470,7 @@ fn open_and_wait(config: &AppleConfig, env: &Env) -> ! { fn inject_resources(config: &AppleConfig, tauri_config: &TauriConfig) -> Result<()> { let asset_dir = config.project_dir().join(DEFAULT_ASSET_DIR); - create_dir_all(&asset_dir)?; + create_dir_all(&asset_dir).fs_context("failed to create asset directory", asset_dir.clone())?; let resources = match &tauri_config.bundle.resources { Some(BundleResources::List(paths)) => Some(ResourcePaths::new(paths.as_slice(), true)), @@ -460,7 +479,7 @@ fn inject_resources(config: &AppleConfig, tauri_config: &TauriConfig) -> Result< }; if let Some(resources) = resources { for resource in resources.iter() { - let resource = resource?; + let resource = resource.context("failed to get resource")?; let dest = asset_dir.join(resource.target()); crate::helpers::fs::copy_file(resource.path(), dest)?; } @@ -490,7 +509,7 @@ fn merge_plist(src: Vec) -> Result { for plist_kind in src { let plist = match plist_kind { - PlistKind::Path(p) => plist::Value::from_file(p), + PlistKind::Path(p) => plist::Value::from_file(p).context("failed to read plist file"), PlistKind::Plist(v) => Ok(v), }; if let Ok(src_plist) = plist { @@ -515,7 +534,9 @@ pub fn signing_from_env() -> Result<( ) { (Some(certificate), Some(certificate_password)) => { log::info!("Reading iOS certificates from "); - tauri_macos_sign::Keychain::with_certificate(&certificate, &certificate_password).map(Some)? + tauri_macos_sign::Keychain::with_certificate(&certificate, &certificate_password) + .map(Some) + .map_err(Box::new)? } (Some(_), None) => { log::warn!("The IOS_CERTIFICATE environment variable is set but not IOS_CERTIFICATE_PASSWORD. Ignoring the certificate..."); @@ -525,7 +546,9 @@ pub fn signing_from_env() -> Result<( }; let provisioning_profile = if let Some(provisioning_profile) = var_os("IOS_MOBILE_PROVISION") { - tauri_macos_sign::ProvisioningProfile::from_base64(&provisioning_profile).map(Some)? + tauri_macos_sign::ProvisioningProfile::from_base64(&provisioning_profile) + .map(Some) + .map_err(Box::new)? } else { if keychain.is_some() { log::warn!("You have provided an iOS certificate via environment variables but the IOS_MOBILE_PROVISION environment variable is not set. This will fail when signing unless the profile is set in your Xcode project."); diff --git a/crates/tauri-cli/src/mobile/ios/project.rs b/crates/tauri-cli/src/mobile/ios/project.rs index 9b4efec3e..d9cd80981 100644 --- a/crates/tauri-cli/src/mobile/ios/project.rs +++ b/crates/tauri-cli/src/mobile/ios/project.rs @@ -3,15 +3,15 @@ // SPDX-License-Identifier: MIT use crate::{ + error::Context, helpers::{config::Config as TauriConfig, template}, mobile::ios::LIB_OUTPUT_FILE_NAME, - Result, + Error, ErrorExt, Result, }; -use anyhow::Context; use cargo_mobile2::{ apple::{ config::{Config, Metadata}, - deps, rust_version_check, + deps, target::Target, }, config::app::DEFAULT_ASSET_DIR, @@ -53,17 +53,20 @@ pub fn gen( log::info!("Installing iOS Rust targets..."); for target in missing_targets { log::info!("Installing target {}", target.triple()); - target - .install() - .context("failed to install target with rustup")?; + target.install().map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + })?; } } } - rust_version_check(wrapper)?; - - deps::install_all(wrapper, non_interactive, true, reinstall_deps) - .with_context(|| "failed to install Apple dependencies")?; + deps::install_all(wrapper, non_interactive, true, reinstall_deps).map_err(|error| { + Error::CommandFailed { + command: "pod install".to_string(), + error: std::io::Error::other(error), + } + })?; let dest = config.project_dir(); let rel_prefix = util::relativize_path(config.app().root_dir(), &dest); @@ -174,9 +177,14 @@ pub fn gen( .with_context(|| "failed to process template")?; if let Some(template_path) = tauri_config.bundle.ios.template.as_ref() { - let template = std::fs::read_to_string(template_path) - .context("failed to read custom Xcode project template")?; - let mut output_file = std::fs::File::create(dest.join("project.yml"))?; + let template = std::fs::read_to_string(template_path).fs_context( + "failed to read custom Xcode project template", + template_path.to_path_buf(), + )?; + let mut output_file = std::fs::File::create(dest.join("project.yml")).fs_context( + "failed to create project.yml file", + dest.join("project.yml"), + )?; handlebars .render_template_to_write(&template, map.inner(), &mut output_file) .expect("Failed to render template"); @@ -189,12 +197,7 @@ pub fn gen( // Create all required project directories if they don't already exist for dir in &dirs_to_create { - std::fs::create_dir_all(dir).map_err(|cause| { - anyhow::anyhow!( - "failed to create directory at {path}: {cause}", - path = dir.display() - ) - })?; + std::fs::create_dir_all(dir).fs_context("failed to create directory", dir.to_path_buf())?; } // Note that Xcode doesn't always reload the project nicely; reopening is @@ -211,7 +214,10 @@ pub fn gen( .stdout_file(os_pipe::dup_stdout().unwrap()) .stderr_file(os_pipe::dup_stderr().unwrap()) .run() - .with_context(|| "failed to run `xcodegen`")?; + .map_err(|error| Error::CommandFailed { + command: "xcodegen".to_string(), + error, + })?; if !ios_pods.is_empty() || !macos_pods.is_empty() { duct::cmd( @@ -224,7 +230,10 @@ pub fn gen( .stdout_file(os_pipe::dup_stdout().unwrap()) .stderr_file(os_pipe::dup_stderr().unwrap()) .run() - .with_context(|| "failed to run `pod install`")?; + .map_err(|error| Error::CommandFailed { + command: "pod install".to_string(), + error, + })?; } Ok(()) } diff --git a/crates/tauri-cli/src/mobile/ios/xcode_script.rs b/crates/tauri-cli/src/mobile/ios/xcode_script.rs index 0217aaa80..c35e55b3d 100644 --- a/crates/tauri-cli/src/mobile/ios/xcode_script.rs +++ b/crates/tauri-cli/src/mobile/ios/xcode_script.rs @@ -4,13 +4,13 @@ use super::{ensure_init, env, get_app, get_config, read_options, MobileTarget}; use crate::{ + error::{Context, ErrorExt}, helpers::config::{get as get_tauri_config, reload as reload_tauri_config}, interface::{AppInterface, Interface, Options as InterfaceOptions}, mobile::ios::LIB_OUTPUT_FILE_NAME, - Result, + Error, Result, }; -use anyhow::Context; use cargo_mobile2::{apple::target::Target, opts::Profile, target::TargetTrait}; use clap::{ArgAction, Parser}; use object::{Object, ObjectSymbol}; @@ -78,7 +78,15 @@ pub fn command(options: Options) -> Result<()> { || var("npm_config_user_agent") .is_ok_and(|agent| agent.starts_with("bun/1.0") || agent.starts_with("bun/1.1")) { - set_current_dir(current_dir()?.parent().unwrap().parent().unwrap()).unwrap(); + set_current_dir( + current_dir() + .context("failed to resolve current directory")? + .parent() + .unwrap() + .parent() + .unwrap(), + ) + .unwrap(); } crate::helpers::app_paths::resolve(); @@ -142,20 +150,22 @@ pub fn command(options: Options) -> Result<()> { )?; } - let env = env()?.explicit_env_vars(cli_options.vars); + let env = env() + .context("failed to load iOS environment")? + .explicit_env_vars(cli_options.vars); if !options.sdk_root.is_dir() { - return Err(anyhow::anyhow!( + crate::error::bail!( "SDK root provided by Xcode was invalid. {} doesn't exist or isn't a directory", options.sdk_root.display(), - )); + ); } let include_dir = options.sdk_root.join("usr/include"); if !include_dir.is_dir() { - return Err(anyhow::anyhow!( + crate::error::bail!( "Include dir was invalid. {} doesn't exist or isn't a directory", include_dir.display() - )); + ); } // Host flags that are used by build scripts @@ -164,10 +174,7 @@ pub fn command(options: Options) -> Result<()> { .sdk_root .join("../../../../MacOSX.platform/Developer/SDKs/MacOSX.sdk"); if !macos_sdk_root.is_dir() { - return Err(anyhow::anyhow!( - "Invalid SDK root {}", - macos_sdk_root.display() - )); + crate::error::bail!("Invalid SDK root {}", macos_sdk_root.display()); } format!("-isysroot {}", macos_sdk_root.display()) }; @@ -224,10 +231,7 @@ pub fn command(options: Options) -> Result<()> { "arm64" if simulator => ("aarch64_apple_ios_sim", "aarch64-apple-ios-sim"), "x86_64" => ("x86_64_apple_ios", "x86_64-apple-ios"), _ => { - return Err(anyhow::anyhow!( - "Arch specified by Xcode was invalid. {} isn't a known arch", - arch - )) + crate::error::bail!("Arch specified by Xcode was invalid. {arch} isn't a known arch") } }; @@ -252,30 +256,28 @@ pub fn command(options: Options) -> Result<()> { } else { &arch }) - .ok_or_else(|| { - anyhow::anyhow!( - "Arch specified by Xcode was invalid. {} isn't a known arch", - arch - ) - })? + .with_context(|| format!("Arch specified by Xcode was invalid. {arch} isn't a known arch"))? }; if !installed_targets.contains(&rust_triple.into()) { log::info!("Installing target {}", target.triple()); - target - .install() - .context("failed to install target with rustup")?; + target.install().map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + })?; } - target.compile_lib( - &config, - &metadata, - cli_options.noise_level, - true, - profile, - &env, - target_env, - )?; + target + .compile_lib( + &config, + &metadata, + cli_options.noise_level, + true, + profile, + &env, + target_env, + ) + .context("failed to compile iOS app")?; let out_dir = interface.app_settings().out_dir(&InterfaceOptions { debug: matches!(profile, Profile::Debug), @@ -285,23 +287,25 @@ pub fn command(options: Options) -> Result<()> { let lib_path = out_dir.join(format!("lib{}.a", config.app().lib_name())); if !lib_path.exists() { - return Err(anyhow::anyhow!("Library not found at {}. Make sure your Cargo.toml file has a [lib] block with `crate-type = [\"staticlib\", \"cdylib\", \"lib\"]`", lib_path.display())); + crate::error::bail!("Library not found at {}. Make sure your Cargo.toml file has a [lib] block with `crate-type = [\"staticlib\", \"cdylib\", \"lib\"]`", lib_path.display()); } validate_lib(&lib_path)?; let project_dir = config.project_dir(); let externals_lib_dir = project_dir.join(format!("Externals/{arch}/{}", profile.as_str())); - std::fs::create_dir_all(&externals_lib_dir)?; + std::fs::create_dir_all(&externals_lib_dir).fs_context( + "failed to create externals lib directory", + externals_lib_dir.clone(), + )?; // backwards compatible lib output file name let uses_new_lib_output_file_name = { - let pbxproj_contents = read_to_string( - project_dir - .join(format!("{}.xcodeproj", config.app().name())) - .join("project.pbxproj"), - ) - .context("missing project.pbxproj file in the Xcode project")?; + let pbxproj_path = project_dir + .join(format!("{}.xcodeproj", config.app().name())) + .join("project.pbxproj"); + let pbxproj_contents = read_to_string(&pbxproj_path) + .fs_context("failed to read project.pbxproj file", pbxproj_path)?; pbxproj_contents.contains(LIB_OUTPUT_FILE_NAME) }; @@ -312,22 +316,31 @@ pub fn command(options: Options) -> Result<()> { format!("lib{}.a", config.app().lib_name()) }; - std::fs::copy(lib_path, externals_lib_dir.join(lib_output_file_name))?; + std::fs::copy(&lib_path, externals_lib_dir.join(lib_output_file_name)).fs_context( + "failed to copy mobile lib file to Externals directory", + lib_path.to_path_buf(), + )?; } Ok(()) } fn validate_lib(path: &Path) -> Result<()> { - let mut archive = ar::Archive::new(std::fs::File::open(path)?); + let mut archive = ar::Archive::new( + std::fs::File::open(path).fs_context("failed to open mobile lib file", path.to_path_buf())?, + ); // Iterate over all entries in the archive: while let Some(entry) = archive.next_entry() { let Ok(mut entry) = entry else { continue; }; let mut obj_bytes = Vec::new(); - entry.read_to_end(&mut obj_bytes)?; + entry + .read_to_end(&mut obj_bytes) + .fs_context("failed to read mobile lib entry", path.to_path_buf())?; - let file = object::File::parse(&*obj_bytes)?; + let file = object::File::parse(&*obj_bytes) + .map_err(std::io::Error::other) + .fs_context("failed to parse mobile lib entry", path.to_path_buf())?; for symbol in file.symbols() { let Ok(name) = symbol.name() else { continue; @@ -338,7 +351,7 @@ fn validate_lib(path: &Path) -> Result<()> { } } - anyhow::bail!( + crate::error::bail!( "Library from {} does not include required runtime symbols. This means you are likely missing the tauri::mobile_entry_point macro usage, see the documentation for more information: https://v2.tauri.app/start/migrate/from-tauri-1", path.display() ) diff --git a/crates/tauri-cli/src/mobile/mod.rs b/crates/tauri-cli/src/mobile/mod.rs index 64504f547..ac0025fe6 100644 --- a/crates/tauri-cli/src/mobile/mod.rs +++ b/crates/tauri-cli/src/mobile/mod.rs @@ -3,15 +3,14 @@ // SPDX-License-Identifier: MIT use crate::{ + error::{Context, ErrorExt}, helpers::{ app_paths::tauri_dir, config::{reload as reload_config, Config as TauriConfig, ConfigHandle, ConfigMetadata}, }, interface::{AppInterface, AppSettings, DevProcess, Interface, Options as InterfaceOptions}, - ConfigValue, + ConfigValue, Error, Result, }; -use anyhow::Context; -use anyhow::{bail, Result}; use heck::ToSnekCase; use jsonrpsee::core::client::{Client, ClientBuilder, ClientT}; use jsonrpsee::server::{RpcModule, ServerBuilder, ServerHandle}; @@ -284,12 +283,14 @@ fn use_network_address_for_dev_url( "If your frontend is not listening on that address, try configuring your development server to use the `TAURI_DEV_HOST` environment variable or 0.0.0.0 as host" ); - *url = url::Url::parse(&format!( + let url_str = format!( "{}://{}{}", url.scheme(), SocketAddr::new(ip, url.port_or_known_default().unwrap()), url.path() - ))?; + ); + *url = + url::Url::parse(&url_str).with_context(|| format!("failed to parse URL: {url_str}"))?; dev_options .config @@ -357,7 +358,7 @@ fn env_vars() -> HashMap { vars } -fn env() -> Result { +fn env() -> std::result::Result { let env = Env::new()?.explicit_env_vars(env_vars()); Ok(env) } @@ -372,12 +373,17 @@ pub fn write_options( options.vars.extend(env_vars()); let runtime = Runtime::new().unwrap(); - let r: anyhow::Result<(ServerHandle, SocketAddr)> = runtime.block_on(async move { - let server = ServerBuilder::default().build("127.0.0.1:0").await?; - let addr = server.local_addr()?; + let r: crate::Result<(ServerHandle, SocketAddr)> = runtime.block_on(async move { + let server = ServerBuilder::default() + .build("127.0.0.1:0") + .await + .context("failed to build WebSocket server")?; + let addr = server.local_addr().context("failed to get local address")?; let mut module = RpcModule::new(()); - module.register_method("options", move |_, _, _| Some(options.clone()))?; + module + .register_method("options", move |_, _, _| Some(options.clone())) + .context("failed to register options method")?; let handle = server.start(module); @@ -385,15 +391,15 @@ pub fn write_options( }); let (handle, addr) = r?; - write( - temp_dir().join(format!( - "{}-server-addr", - config - .original_identifier() - .context("app configuration is missing an identifier")? - )), - addr.to_string(), - )?; + let server_addr_path = temp_dir().join(format!( + "{}-server-addr", + config + .original_identifier() + .context("app configuration is missing an identifier")? + )); + + write(&server_addr_path, addr.to_string()) + .fs_context("failed to write server address file", server_addr_path)?; Ok(OptionsHandle(runtime, handle)) } @@ -420,10 +426,14 @@ fn read_options(config: &ConfigMetadata) -> CliOptions { .parse() .unwrap(), ) - .await?; + .await + .context("failed to build WebSocket client")?; let client: Client = ClientBuilder::default().build_with_tokio(tx, rx); - let options: CliOptions = client.request("options", rpc_params![]).await?; - Ok::(options) + let options: CliOptions = client + .request("options", rpc_params![]) + .await + .context("failed to request options")?; + Ok::(options) }) .expect("failed to read CLI options"); @@ -485,7 +495,7 @@ fn ensure_init( target: Target, ) -> Result<()> { if !project_dir.exists() { - bail!( + crate::error::bail!( "{} project directory {} doesn't exist. Please run `tauri {} init` and try again.", target.ide_name(), project_dir.display(), @@ -518,7 +528,12 @@ fn ensure_init( .join(format!("{}.xcodeproj", app.name())) .join("project.pbxproj"), ) - .context("missing project.yml file in the Xcode project directory")?; + .fs_context( + "missing project.pbxproj file in the Xcode project directory", + project_dir + .join(format!("{}.xcodeproj", app.name())) + .join("project.pbxproj"), + )?; if !(pbxproj_contents.contains(ios::LIB_OUTPUT_FILE_NAME) || pbxproj_contents.contains(&format!("lib{}.a", app.lib_name()))) @@ -531,7 +546,7 @@ fn ensure_init( if !project_outdated_reasons.is_empty() { let reason = project_outdated_reasons.join(" and "); - bail!( + crate::error::bail!( "{} project directory is outdated because {reason}. Please run `tauri {} init` and try again.", target.ide_name(), target.command_name(), @@ -552,15 +567,15 @@ fn ensure_gradlew(project_dir: &std::path::Path) -> Result<()> { if !is_executable { permissions.set_mode(permissions.mode() | 0o111); std::fs::set_permissions(&gradlew_path, permissions) - .context("failed to mark gradlew as executable")?; + .fs_context("failed to mark gradlew as executable", gradlew_path.clone())?; } std::fs::write( &gradlew_path, std::fs::read_to_string(&gradlew_path) - .context("failed to read gradlew")? + .fs_context("failed to read gradlew", gradlew_path.clone())? .replace("\r\n", "\n"), ) - .context("failed to replace gradlew CRLF with LF")?; + .fs_context("failed to replace gradlew CRLF with LF", gradlew_path)?; } Ok(()) diff --git a/crates/tauri-cli/src/plugin/android.rs b/crates/tauri-cli/src/plugin/android.rs index 51322a57c..1fc91046b 100644 --- a/crates/tauri-cli/src/plugin/android.rs +++ b/crates/tauri-cli/src/plugin/android.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT use crate::{ + error::Context, helpers::{prompts, template}, Result, }; @@ -50,13 +51,15 @@ pub fn command(cli: Cli) -> Result<()> { match cli.command { Commands::Init(options) => { let plugin_name = match options.plugin_name { - None => super::infer_plugin_name(std::env::current_dir()?)?, + None => super::infer_plugin_name( + std::env::current_dir().context("failed to get current directory")?, + )?, Some(name) => name, }; let out_dir = PathBuf::from(options.out_dir); if out_dir.join("android").exists() { - return Err(anyhow::anyhow!("android folder already exists")); + crate::error::bail!("Android folder already exists"); } let plugin_id = prompts::input( diff --git a/crates/tauri-cli/src/plugin/init.rs b/crates/tauri-cli/src/plugin/init.rs index 39219fe48..baed52a70 100644 --- a/crates/tauri-cli/src/plugin/init.rs +++ b/crates/tauri-cli/src/plugin/init.rs @@ -3,13 +3,12 @@ // SPDX-License-Identifier: MIT use super::PluginIosFramework; -use crate::helpers::prompts; use crate::Result; use crate::{ - helpers::{resolve_tauri_path, template}, + error::{Context, ErrorExt}, + helpers::{prompts, resolve_tauri_path, template}, VersionMetadata, }; -use anyhow::Context; use clap::Parser; use handlebars::{to_json, Handlebars}; use heck::{ToKebabCase, ToPascalCase, ToSnakeCase}; @@ -90,7 +89,14 @@ pub fn command(mut options: Options) -> Result<()> { let template_target_path = PathBuf::from(options.directory); let metadata = crates_metadata()?; - if std::fs::read_dir(&template_target_path)?.count() > 0 { + if std::fs::read_dir(&template_target_path) + .fs_context( + "failed to read target directory", + template_target_path.clone(), + )? + .count() + > 0 + { log::warn!("Plugin dir ({:?}) not empty.", template_target_path); } else { let (tauri_dep, tauri_example_dep, tauri_build_dep, tauri_plugin_dep) = @@ -247,15 +253,19 @@ pub fn command(mut options: Options) -> Result<()> { } let permissions_dir = template_target_path.join("permissions"); - std::fs::create_dir(&permissions_dir) - .with_context(|| "failed to create `permissions` directory")?; + std::fs::create_dir(&permissions_dir).fs_context( + "failed to create `permissions` directory", + permissions_dir.clone(), + )?; let default_permissions = r#"[default] description = "Default permissions for the plugin" permissions = ["allow-ping"] "#; - std::fs::write(permissions_dir.join("default.toml"), default_permissions) - .with_context(|| "failed to write `permissions/default.toml`")?; + std::fs::write(permissions_dir.join("default.toml"), default_permissions).fs_context( + "failed to write default permissions file", + permissions_dir.join("default.toml"), + )?; Ok(()) } @@ -275,7 +285,7 @@ pub fn plugin_name_data(data: &mut BTreeMap<&'static str, serde_json::Value>, pl pub fn crates_metadata() -> Result { serde_json::from_str::(include_str!("../../metadata-v2.json")) - .map_err(Into::into) + .context("failed to parse Tauri version metadata") } pub fn generate_android_out_file( diff --git a/crates/tauri-cli/src/plugin/ios.rs b/crates/tauri-cli/src/plugin/ios.rs index 88621954c..421b2fe26 100644 --- a/crates/tauri-cli/src/plugin/ios.rs +++ b/crates/tauri-cli/src/plugin/ios.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT use super::PluginIosFramework; -use crate::{helpers::template, Result}; +use crate::{error::Context, helpers::template, Result}; use clap::{Parser, Subcommand}; use handlebars::Handlebars; @@ -53,13 +53,15 @@ pub fn command(cli: Cli) -> Result<()> { match cli.command { Commands::Init(options) => { let plugin_name = match options.plugin_name { - None => super::infer_plugin_name(std::env::current_dir()?)?, + None => super::infer_plugin_name( + std::env::current_dir().context("failed to get current directory")?, + )?, Some(name) => name, }; let out_dir = PathBuf::from(options.out_dir); if out_dir.join("ios").exists() { - return Err(anyhow::anyhow!("ios folder already exists")); + crate::error::bail!("iOS folder already exists"); } let handlebars = Handlebars::new(); diff --git a/crates/tauri-cli/src/plugin/mod.rs b/crates/tauri-cli/src/plugin/mod.rs index a1c5c7d9a..6237f897b 100644 --- a/crates/tauri-cli/src/plugin/mod.rs +++ b/crates/tauri-cli/src/plugin/mod.rs @@ -6,7 +6,10 @@ use std::{fmt::Display, path::Path}; use clap::{Parser, Subcommand, ValueEnum}; -use crate::Result; +use crate::{ + error::{Context, ErrorExt}, + Result, +}; mod android; mod init; @@ -67,8 +70,10 @@ fn infer_plugin_name>(directory: P) -> Result { let dir = directory.as_ref(); let cargo_toml_path = dir.join("Cargo.toml"); let name = if cargo_toml_path.exists() { - let contents = std::fs::read_to_string(cargo_toml_path)?; - let cargo_toml: toml::Value = toml::from_str(&contents)?; + let contents = std::fs::read_to_string(&cargo_toml_path) + .fs_context("failed to read Cargo manifest", cargo_toml_path)?; + let cargo_toml: toml::Value = + toml::from_str(&contents).context("failed to parse Cargo.toml")?; cargo_toml .get("package") .and_then(|v| v.get("name")) diff --git a/crates/tauri-cli/src/plugin/new.rs b/crates/tauri-cli/src/plugin/new.rs index 3e000d43a..328c165c3 100644 --- a/crates/tauri-cli/src/plugin/new.rs +++ b/crates/tauri-cli/src/plugin/new.rs @@ -3,7 +3,10 @@ // SPDX-License-Identifier: MIT use super::PluginIosFramework; -use crate::Result; +use crate::{ + error::{Context, ErrorExt}, + Result, +}; use clap::Parser; use std::path::PathBuf; @@ -70,12 +73,14 @@ impl From for super::init::Options { } pub fn command(mut options: Options) -> Result<()> { - let cwd = std::env::current_dir()?; + let cwd = std::env::current_dir().context("failed to get current directory")?; if let Some(dir) = &options.directory { - std::fs::create_dir_all(cwd.join(dir))?; + std::fs::create_dir_all(cwd.join(dir)) + .fs_context("failed to create crate directory", cwd.join(dir))?; } else { let target = cwd.join(format!("tauri-plugin-{}", options.plugin_name)); - std::fs::create_dir_all(&target)?; + std::fs::create_dir_all(&target) + .fs_context("failed to create crate directory", target.clone())?; options.directory.replace(target.display().to_string()); } diff --git a/crates/tauri-cli/src/signer/sign.rs b/crates/tauri-cli/src/signer/sign.rs index 303cc2e37..44eee5d57 100644 --- a/crates/tauri-cli/src/signer/sign.rs +++ b/crates/tauri-cli/src/signer/sign.rs @@ -5,10 +5,10 @@ use std::path::{Path, PathBuf}; use crate::{ + error::Context, helpers::updater_signature::{secret_key, sign_file}, Result, }; -use anyhow::Context; use base64::Engine; use clap::Parser; use tauri_utils::display_path; @@ -48,9 +48,7 @@ pub fn command(mut options: Options) -> Result<()> { let private_key = if let Some(pk) = options.private_key { pk } else { - return Err(anyhow::anyhow!( - "Key generation aborted: Unable to find the private key".to_string(), - )); + crate::error::bail!("Key generation aborted: Unable to find the private key"); }; if options.password.is_none() { diff --git a/crates/tauri-macos-sign/Cargo.toml b/crates/tauri-macos-sign/Cargo.toml index bcd7ea760..37e787cd4 100644 --- a/crates/tauri-macos-sign/Cargo.toml +++ b/crates/tauri-macos-sign/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" rust-version = "1.77.2" [dependencies] -anyhow = "1" +thiserror = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tempfile = "3" diff --git a/crates/tauri-macos-sign/src/certificate.rs b/crates/tauri-macos-sign/src/certificate.rs index 97afa2199..11d2cf336 100644 --- a/crates/tauri-macos-sign/src/certificate.rs +++ b/crates/tauri-macos-sign/src/certificate.rs @@ -2,12 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use anyhow::{Context, Result}; use apple_codesign::create_self_signed_code_signing_certificate; use x509_certificate::{EcdsaCurve, KeyAlgorithm}; pub use apple_codesign::CertificateProfile; +use crate::{Error, Result}; + /// Self signed certificate options. pub struct SelfSignedCertificateRequest { /// Which key type to use @@ -49,16 +50,21 @@ pub fn generate_self_signed(request: SelfSignedCertificateRequest) -> Result Result { - let tmp_dir = tempfile::tempdir()?; + let tmp_dir = tempfile::tempdir().map_err(Error::TempDir)?; let cert_path = tmp_dir.path().join("cert.p12"); super::decode_base64(certificate_encoded, &cert_path)?; Self::with_certificate_file(&cert_path, certificate_password) } pub fn with_certificate_file(cert_path: &Path, certificate_password: &OsString) -> Result { - let home_dir = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("failed to resolve home dir"))?; + let home_dir = dirs::home_dir().ok_or(Error::ResolveHomeDir)?; let keychain_path = home_dir.join("Library").join("Keychains").join(format!( "{}.keychain-db", Alphanumeric.sample_string(&mut rand::rng(), 16) @@ -73,7 +72,11 @@ impl Keychain { let keychain_list_output = Command::new("security") .args(["list-keychain", "-d", "user"]) - .output()?; + .output() + .map_err(|e| Error::CommandFailed { + command: "security list-keychain -d user".to_string(), + error: e, + })?; assert_command( Command::new("security") @@ -81,7 +84,11 @@ impl Keychain { .arg(&keychain_path) .piped(), "failed to create keychain", - )?; + ) + .map_err(|error| Error::CommandFailed { + command: "security create-Keychain".to_string(), + error, + })?; assert_command( Command::new("security") @@ -89,7 +96,11 @@ impl Keychain { .arg(&keychain_path) .piped(), "failed to set unlock keychain", - )?; + ) + .map_err(|error| Error::CommandFailed { + command: "security unlock-keychain".to_string(), + error, + })?; assert_command( Command::new("security") @@ -109,7 +120,11 @@ impl Keychain { .arg(&keychain_path) .piped(), "failed to import keychain certificate", - )?; + ) + .map_err(|error| Error::CommandFailed { + command: "security import".to_string(), + error, + })?; assert_command( Command::new("security") @@ -117,7 +132,11 @@ impl Keychain { .arg(&keychain_path) .piped(), "failed to set keychain settings", - )?; + ) + .map_err(|error| Error::CommandFailed { + command: "security set-keychain-settings".to_string(), + error, + })?; assert_command( Command::new("security") @@ -132,7 +151,11 @@ impl Keychain { .arg(&keychain_path) .piped(), "failed to set keychain settings", - )?; + ) + .map_err(|error| Error::CommandFailed { + command: "security set-key-partition-list".to_string(), + error, + })?; let current_keychains = String::from_utf8_lossy(&keychain_list_output.stdout) .split('\n') @@ -151,11 +174,15 @@ impl Keychain { .arg(&keychain_path) .piped(), "failed to list keychain", - )?; + ) + .map_err(|error| Error::CommandFailed { + command: "security list-keychain".to_string(), + error, + })?; let signing_identity = identity::list(&keychain_path) .map(|l| l.first().cloned())? - .ok_or_else(|| anyhow::anyhow!("failed to resolve signing identity"))?; + .ok_or(Error::ResolveSigningIdentity)?; Ok(Self { path: Some(keychain_path), @@ -211,7 +238,12 @@ impl Keychain { codesign.arg(path); - assert_command(codesign.piped(), "failed to sign app")?; + assert_command(codesign.piped(), "failed to sign app").map_err(|error| { + Error::CommandFailed { + command: "codesign".to_string(), + error, + } + })?; Ok(()) } diff --git a/crates/tauri-macos-sign/src/keychain/identity.rs b/crates/tauri-macos-sign/src/keychain/identity.rs index 185e50f92..a39c48423 100644 --- a/crates/tauri-macos-sign/src/keychain/identity.rs +++ b/crates/tauri-macos-sign/src/keychain/identity.rs @@ -2,14 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use anyhow::Context; use once_cell_regex::regex; use std::{collections::BTreeSet, path::Path, process::Command}; use x509_certificate::certificate::X509Certificate; -use crate::Result; +use crate::{Error, Result}; -fn get_pem_list(keychain_path: &Path, name_substr: &str) -> std::io::Result { +fn get_pem_list(keychain_path: &Path, name_substr: &str) -> Result { Command::new("security") .arg("find-certificate") .args(["-p", "-a"]) @@ -19,6 +18,10 @@ fn get_pem_list(keychain_path: &Path, name_substr: &str) -> std::io::Result Result { let common_name = cert .subject_common_name() - .ok_or_else(|| anyhow::anyhow!("skipping cert, missing common name"))?; + .ok_or(Error::CertificateMissingCommonName)?; let organization = cert .subject_name() @@ -62,7 +65,9 @@ impl Team { .iter_organizational_unit() .next() .and_then(|v| v.to_string().ok()) - .ok_or_else(|| anyhow::anyhow!("skipping cert {common_name}: missing Organization Unit"))?; + .ok_or_else(|| Error::CertificateMissingOrganizationUnit { + common_name: common_name.clone(), + })?; Ok(Self { name, @@ -89,10 +94,9 @@ pub fn list(keychain_path: &Path) -> Result> { "iOS App Development:", "Mac Development:", ] { - let pem_list_out = - get_pem_list(keychain_path, cert_prefix).context("Failed to call `security` command")?; + let pem_list_out = get_pem_list(keychain_path, cert_prefix)?; let cert_list = X509Certificate::from_pem_multiple(pem_list_out.stdout) - .context("Failed to parse X509 cert")?; + .map_err(|error| Error::X509Certificate { error })?; certs.extend(cert_list.into_iter().map(|cert| (cert_prefix, cert))); } certs @@ -102,7 +106,7 @@ pub fn list(keychain_path: &Path) -> Result> { .into_iter() .flat_map(|(cert_prefix, cert)| { Team::from_x509(cert_prefix, cert).map_err(|err| { - eprintln!("{err}"); + log::error!("{err}"); err }) }) diff --git a/crates/tauri-macos-sign/src/lib.rs b/crates/tauri-macos-sign/src/lib.rs index 183cb16cc..64a9c3d57 100644 --- a/crates/tauri-macos-sign/src/lib.rs +++ b/crates/tauri-macos-sign/src/lib.rs @@ -8,7 +8,6 @@ use std::{ process::{Command, ExitStatus}, }; -use anyhow::{Context, Result}; use serde::Deserialize; pub mod certificate; @@ -18,6 +17,61 @@ mod provisioning_profile; pub use keychain::{Keychain, Team}; pub use provisioning_profile::ProvisioningProfile; +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("failed to create temp directory: {0}")] + TempDir(std::io::Error), + #[error("failed to resolve home dir")] + ResolveHomeDir, + #[error("failed to resolve signing identity")] + ResolveSigningIdentity, + #[error("failed to decode provisioning profile")] + FailedToDecodeProvisioningProfile, + #[error("could not find provisioning profile UUID")] + FailedToFindProvisioningProfileUuid, + #[error("{context} {path}: {error}")] + Plist { + context: &'static str, + path: PathBuf, + error: plist::Error, + }, + #[error("failed to upload app to Apple's notarization servers: {error}")] + FailedToUploadApp { error: std::io::Error }, + #[error("failed to notarize app: {0}")] + Notarize(String), + #[error("failed to parse notarytool output as JSON: {output}")] + ParseNotarytoolOutput { output: String }, + #[error("failed to run command {command}: {error}")] + CommandFailed { + command: String, + error: std::io::Error, + }, + #[error("{context} {path}: {error}")] + Fs { + context: &'static str, + path: PathBuf, + error: std::io::Error, + }, + #[error("failed to parse X509 certificate: {error}")] + X509Certificate { + error: x509_certificate::X509CertificateError, + }, + #[error("failed to create PFX from self signed certificate")] + FailedToCreatePFX, + #[error("failed to create self signed certificate: {error}")] + FailedToCreateSelfSignedCertificate { + error: Box, + }, + #[error("failed to encode DER: {error}")] + FailedToEncodeDER { error: std::io::Error }, + #[error("certificate missing common name")] + CertificateMissingCommonName, + #[error("certificate missing organization unit for common name {common_name}")] + CertificateMissingOrganizationUnit { common_name: String }, +} + +pub type Result = std::result::Result; + trait CommandExt { // The `pipe` function sets the stdout and stderr to properly // show the command output in the Node.js wrapper. @@ -88,7 +142,7 @@ fn notarize_inner( .file_stem() .expect("failed to get bundle filename"); - let tmp_dir = tempfile::tempdir()?; + let tmp_dir = tempfile::tempdir().map_err(Error::TempDir)?; let zip_path = tmp_dir .path() .join(format!("{}.zip", bundle_stem.to_string_lossy())); @@ -110,7 +164,11 @@ fn notarize_inner( assert_command( Command::new("ditto").args(zip_args).piped(), "failed to zip app with ditto", - )?; + ) + .map_err(|error| Error::CommandFailed { + command: "ditto".to_string(), + error, + })?; // sign the zip file keychain.sign(&zip_path, None, false)?; @@ -135,13 +193,12 @@ fn notarize_inner( .args(notarize_args) .notarytool_args(auth, tmp_dir.path())? .output() - .context("failed to upload app to Apple's notarization servers.")?; + .map_err(|error| Error::FailedToUploadApp { error })?; if !output.status.success() { - return Err( - anyhow::anyhow!("failed to notarize app") - .context(String::from_utf8_lossy(&output.stderr).into_owned()), - ); + return Err(Error::Notarize( + String::from_utf8_lossy(&output.stderr).into_owned(), + )); } let output_str = String::from_utf8_lossy(&output.stdout); @@ -176,17 +233,17 @@ fn notarize_inner( .notarytool_args(auth, tmp_dir.path())? .output() { - Err(anyhow::anyhow!( + Err(Error::Notarize(format!( "{log_message}\nLog:\n{}", String::from_utf8_lossy(&output.stdout) - )) + ))) } else { - Err(anyhow::anyhow!("{log_message}")) + Err(Error::Notarize(log_message.to_string())) } } else { - Err(anyhow::anyhow!( - "failed to parse notarytool output as JSON: `{output_str}`" - )) + Err(Error::ParseNotarytoolOutput { + output: output_str.into_owned(), + }) } } @@ -204,7 +261,10 @@ fn staple_app(mut app_bundle_path: PathBuf) -> Result<()> { .args(vec!["stapler", "staple", "-v", filename]) .current_dir(app_bundle_path) .output() - .context("failed to staple app.")?; + .map_err(|error| Error::CommandFailed { + command: "xcrun stapler staple".to_string(), + error, + })?; Ok(()) } @@ -245,7 +305,11 @@ impl NotarytoolCmdExt for Command { let key_path = match key { ApiKey::Raw(k) => { let key_path = temp_dir.join("AuthKey.p8"); - std::fs::write(&key_path, k)?; + std::fs::write(&key_path, k).map_err(|error| Error::Fs { + context: "failed to write notarization API key to temp file", + path: key_path.clone(), + error, + })?; key_path } ApiKey::Path(p) => p.to_owned(), @@ -266,7 +330,7 @@ impl NotarytoolCmdExt for Command { } fn decode_base64(base64: &OsStr, out_path: &Path) -> Result<()> { - let tmp_dir = tempfile::tempdir()?; + let tmp_dir = tempfile::tempdir().map_err(Error::TempDir)?; let src_path = tmp_dir.path().join("src"); let base64 = base64 @@ -277,7 +341,11 @@ fn decode_base64(base64: &OsStr, out_path: &Path) -> Result<()> { // as base64 contain whitespace decoding may be broken // https://github.com/marshallpierce/rust-base64/issues/105 // we'll use builtin base64 command from the OS - std::fs::write(&src_path, base64)?; + std::fs::write(&src_path, base64).map_err(|error| Error::Fs { + context: "failed to write base64 to temp file", + path: src_path.clone(), + error, + })?; assert_command( std::process::Command::new("base64") @@ -288,13 +356,17 @@ fn decode_base64(base64: &OsStr, out_path: &Path) -> Result<()> { .arg(out_path) .piped(), "failed to decode certificate", - )?; + ) + .map_err(|error| Error::CommandFailed { + command: "base64 --decode".to_string(), + error, + })?; Ok(()) } fn assert_command( - response: Result, + response: std::result::Result, error_message: &str, ) -> std::io::Result<()> { let status = diff --git a/crates/tauri-macos-sign/src/provisioning_profile.rs b/crates/tauri-macos-sign/src/provisioning_profile.rs index e9be82a9a..9e251a743 100644 --- a/crates/tauri-macos-sign/src/provisioning_profile.rs +++ b/crates/tauri-macos-sign/src/provisioning_profile.rs @@ -4,7 +4,7 @@ use std::{ffi::OsStr, path::PathBuf, process::Command}; -use anyhow::{Context, Result}; +use crate::{Error, Result}; use rand::distr::{Alphanumeric, SampleString}; pub struct ProvisioningProfile { @@ -13,12 +13,16 @@ pub struct ProvisioningProfile { impl ProvisioningProfile { pub fn from_base64(base64: &OsStr) -> Result { - let home_dir = dirs::home_dir().unwrap(); + let home_dir = dirs::home_dir().ok_or(Error::ResolveHomeDir)?; let provisioning_profiles_folder = home_dir .join("Library") .join("MobileDevice") .join("Provisioning Profiles"); - std::fs::create_dir_all(&provisioning_profiles_folder).unwrap(); + std::fs::create_dir_all(&provisioning_profiles_folder).map_err(|error| Error::Fs { + context: "failed to create provisioning profiles folder", + path: provisioning_profiles_folder.clone(), + error, + })?; let provisioning_profile_path = provisioning_profiles_folder.join(format!( "{}.mobileprovision", @@ -35,18 +39,26 @@ impl ProvisioningProfile { let output = Command::new("security") .args(["cms", "-D", "-i"]) .arg(&self.path) - .output()?; + .output() + .map_err(|error| Error::CommandFailed { + command: "security cms -D -i".to_string(), + error, + })?; if !output.status.success() { - return Err(anyhow::anyhow!("failed to decode provisioning profile")); + return Err(Error::FailedToDecodeProvisioningProfile); } - let plist = plist::from_bytes::(&output.stdout) - .context("failed to decode provisioning profile as plist")?; + let plist = + plist::from_bytes::(&output.stdout).map_err(|error| Error::Plist { + context: "failed to parse provisioning profile as plist", + path: self.path.clone(), + error, + })?; plist .get("UUID") .and_then(|v| v.as_string().map(ToString::to_string)) - .ok_or_else(|| anyhow::anyhow!("could not find provisioning profile UUID")) + .ok_or(Error::FailedToFindProvisioningProfileUuid) } }