From c0eaffb05d96f25a3a7bcde901ef4d2640ab28b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 20:51:32 +1200 Subject: [PATCH 01/17] Refactor for rich errors, split user abort and genuine error --- Cargo.lock | 117 +++++++++++++++++++++++++++++++--- Cargo.toml | 3 +- src/bins.rs | 8 +-- src/drivers.rs | 98 ++++++++++++---------------- src/errors.rs | 77 ++++++++++++++++++++++ src/fetchers.rs | 6 +- src/fetchers/gh_crate_meta.rs | 10 +-- src/fetchers/quickinstall.rs | 19 ++++-- src/helpers.rs | 53 +++++++-------- src/lib.rs | 7 +- src/main.rs | 80 +++++++++++++++-------- 11 files changed, 340 insertions(+), 138 deletions(-) create mode 100644 src/errors.rs diff --git a/Cargo.lock b/Cargo.lock index feb71914..c67c8f1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,12 +26,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "anyhow" -version = "1.0.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" - [[package]] name = "async-trait" version = "0.1.53" @@ -147,7 +141,6 @@ dependencies = [ name = "cargo-binstall" version = "0.7.0" dependencies = [ - "anyhow", "async-trait", "cargo_metadata", "cargo_toml", @@ -156,6 +149,7 @@ dependencies = [ "env_logger", "flate2", "log", + "miette", "reqwest", "semver", "serde", @@ -165,6 +159,7 @@ dependencies = [ "strum_macros", "tar", "tempfile", + "thiserror", "tinytemplate", "tokio", "url", @@ -262,7 +257,7 @@ dependencies = [ "atty", "bitflags", "strsim", - "textwrap", + "textwrap 0.11.0", "unicode-width", "vec_map", ] @@ -676,6 +671,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + [[package]] name = "itoa" version = "1.0.2" @@ -770,6 +771,36 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "miette" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c90329e44f9208b55f45711f9558cec15d7ef8295cc65ecd6d4188ae8edc58c" +dependencies = [ + "atty", + "miette-derive", + "once_cell", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap 0.15.0", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b5bc45b761bcf1b5e6e6c4128cd93b84c218721a8d9b894aa0aff4ed180174c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "mime" version = "0.3.16" @@ -857,6 +888,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" +[[package]] +name = "owo-colors" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b" + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -1161,6 +1198,12 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + [[package]] name = "socket2" version = "0.4.4" @@ -1226,6 +1269,34 @@ dependencies = [ "syn", ] +[[package]] +name = "supports-color" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872ced36b91d47bae8a214a683fe54e7078875b399dfa251df346c9b547d1f9" +dependencies = [ + "atty", + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590b34f7c5f01ecc9d78dba4b3f445f31df750a67621cf31626f3b7441ce6406" +dependencies = [ + "atty", +] + +[[package]] +name = "supports-unicode" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8b945e45b417b125a8ec51f1b7df2f8df7920367700d1f98aedd21e5735f8b2" +dependencies = [ + "atty", +] + [[package]] name = "syn" version = "1.0.95" @@ -1271,6 +1342,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -1280,6 +1361,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.31" @@ -1463,6 +1555,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +[[package]] +name = "unicode-linebreak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +dependencies = [ + "regex", +] + [[package]] name = "unicode-normalization" version = "0.1.19" diff --git a/Cargo.toml b/Cargo.toml index f2009264..60265956 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ pkg-fmt = "zip" pkg-fmt = "zip" [dependencies] -anyhow = "1.0.57" async-trait = "0.1.52" cargo_metadata = "0.14.2" cargo_toml = "0.11.4" @@ -27,6 +26,7 @@ crates_io_api = { version = "0.8.0", default-features = false, features = ["rust dirs = "4.0.0" flate2 = { version = "1.0.24", features = ["zlib-ng"], default-features = false } log = "0.4.14" +miette = { version = "4.7.1", features = ["fancy-no-backtrace"] } reqwest = { version = "0.11.10", features = [ "rustls-tls" ], default-features = false } semver = "1.0.7" serde = { version = "1.0.136", features = [ "derive" ] } @@ -36,6 +36,7 @@ strum = "0.24.0" strum_macros = "0.24.0" tar = "0.4.38" tempfile = "3.3.0" +thiserror = "1.0.31" tinytemplate = "1.2.1" # This crate uses features rt-multi-thread and macros in `#[tokio::main]` and diff --git a/src/bins.rs b/src/bins.rs index 3000df5e..76dfc9fc 100644 --- a/src/bins.rs +++ b/src/bins.rs @@ -4,7 +4,7 @@ use cargo_toml::Product; use log::debug; use serde::Serialize; -use crate::{PkgFmt, PkgMeta, Template}; +use crate::{BinstallError, PkgFmt, PkgMeta, Template}; pub struct BinFile { pub base_name: String, @@ -14,7 +14,7 @@ pub struct BinFile { } impl BinFile { - pub fn from_product(data: &Data, product: &Product) -> Result { + pub fn from_product(data: &Data, product: &Product) -> Result { let base_name = product.name.clone().unwrap(); let binary_ext = if data.target.contains("windows") { @@ -77,7 +77,7 @@ impl BinFile { ) } - pub fn install_bin(&self) -> Result<(), anyhow::Error> { + pub fn install_bin(&self) -> Result<(), BinstallError> { // TODO: check if file already exists debug!( "Copy file from '{}' to '{}'", @@ -96,7 +96,7 @@ impl BinFile { Ok(()) } - pub fn install_link(&self) -> Result<(), anyhow::Error> { + pub fn install_link(&self) -> Result<(), BinstallError> { // Remove existing symlink // TODO: check if existing symlink is correct if self.link.exists() { diff --git a/src/drivers.rs b/src/drivers.rs index 38568db8..7b39bd10 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -1,25 +1,26 @@ +use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use std::time::Duration; -use anyhow::{anyhow, Context}; +use crates_io_api::AsyncClient; use log::debug; use semver::{Version, VersionReq}; -use crates_io_api::AsyncClient; - -use crate::helpers::*; -use crate::PkgFmt; +use crate::{helpers::*, BinstallError, PkgFmt}; fn find_version<'a, V: Iterator>( requirement: &str, version_iter: V, -) -> Result { +) -> Result { // Parse version requirement - let version_req = VersionReq::parse(requirement)?; + let version_req = VersionReq::parse(requirement).map_err(|err| BinstallError::VersionReq { + req: requirement.into(), + err, + })?; // Filter for matching versions - let mut filtered: Vec<_> = version_iter - .filter(|v| { + let filtered: BTreeSet<_> = version_iter + .filter_map(|v| { // Remove leading `v` for git tags let ver_str = match v.strip_prefix("s") { Some(v) => v, @@ -27,36 +28,26 @@ fn find_version<'a, V: Iterator>( }; // Parse out version - let ver = match Version::parse(ver_str) { - Ok(sv) => sv, - Err(_) => return false, - }; - + let ver = Version::parse(ver_str).ok()?; debug!("Version: {:?}", ver); // Filter by version match - version_req.matches(&ver) + if version_req.matches(&ver) { + Some(ver) + } else { + None + } }) .collect(); - // Sort by highest matching version - filtered.sort_by(|a, b| { - let a = Version::parse(a).unwrap(); - let b = Version::parse(b).unwrap(); - - b.partial_cmp(&a).unwrap() - }); - debug!("Filtered: {:?}", filtered); // Return highest version - match filtered.get(0) { - Some(v) => Ok(v.to_string()), - None => Err(anyhow!( - "No matching version for requirement: '{}'", - version_req - )), - } + filtered + .iter() + .max() + .cloned() + .ok_or_else(|| BinstallError::VersionMismatch { req: version_req }) } /// Fetch a crate by name and version from crates.io @@ -64,7 +55,7 @@ pub async fn fetch_crate_cratesio( name: &str, version_req: &str, temp_dir: &Path, -) -> Result { +) -> Result { // Fetch / update index debug!("Looking up crate information"); @@ -72,23 +63,18 @@ pub async fn fetch_crate_cratesio( let api_client = AsyncClient::new( "cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100), - )?; + ) + .expect("bug: invalid user agent"); // Fetch online crate information - let crate_info = api_client - .get_crate(name.as_ref()) - .await - .context("Error fetching crate information"); - - let base_info = match crate_info { - Ok(i) => i, - Err(_) => { - return Err(anyhow::anyhow!( - "Error fetching information for crate {}", - name - )); - } - }; + let base_info = + api_client + .get_crate(name.as_ref()) + .await + .map_err(|err| BinstallError::CratesIoApi { + crate_name: name.into(), + err, + })?; // Locate matching version let version_iter = @@ -99,16 +85,14 @@ pub async fn fetch_crate_cratesio( let version_name = find_version(version_req, version_iter)?; // Fetch information for the filtered version - let version = match base_info.versions.iter().find(|v| v.num == version_name) { - Some(v) => v, - None => { - return Err(anyhow::anyhow!( - "No information found for crate: '{}' version: '{}'", - name, - version_name - )); - } - }; + let version = base_info + .versions + .iter() + .find(|v| v.num == version_name.to_string()) + .ok_or_else(|| BinstallError::VersionUnavailable { + crate_name: name.into(), + v: version_name.clone(), + })?; debug!("Found information for crate version: '{}'", version.num); @@ -136,6 +120,6 @@ pub async fn fetch_crate_gh_releases( _name: &str, _version: Option<&str>, _temp_dir: &Path, -) -> Result { +) -> Result { unimplemented!(); } diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 00000000..97705be4 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,77 @@ +use miette::Diagnostic; +use thiserror::Error; + +#[derive(Error, Diagnostic, Debug)] +pub enum BinstallError { + #[error("installation cancelled by user")] + #[diagnostic(code(binstall::user_abort))] + UserAbort, + + #[error(transparent)] + #[diagnostic(code(binstall::io))] + Io(#[from] std::io::Error), + + #[error(transparent)] + #[diagnostic(code(binstall::url_parse))] + UrlParse(#[from] url::ParseError), + + #[error(transparent)] + #[diagnostic(code(binstall::reqwest))] + Reqwest(#[from] reqwest::Error), + + #[error(transparent)] + #[diagnostic(code(binstall::template))] + Template(#[from] tinytemplate::error::Error), + + #[error(transparent)] + #[diagnostic(code(binstall::unzip))] + Unzip(#[from] zip::result::ZipError), + + #[error(transparent)] + #[diagnostic(code(binstall::cargo_manifest))] + CargoManifest(#[from] cargo_toml::Error), + + #[error("crates.io api error fetching crate information for '{crate_name}': {err}")] + #[diagnostic(code(binstall::crates_io_api))] + CratesIoApi { + crate_name: String, + #[source] + err: crates_io_api::Error, + }, + + #[error("version string '{v}' is not semver: {err}")] + #[diagnostic(code(binstall::version::parse))] + VersionParse { + v: String, + #[source] + err: semver::Error, + }, + + #[error("version requirement '{req}' is not semver: {err}")] + #[diagnostic(code(binstall::version::requirement))] + VersionReq { + req: String, + #[source] + err: semver::Error, + }, + + #[error("no version matching requirement '{req}'")] + #[diagnostic(code(binstall::version::mismatch))] + VersionMismatch { req: semver::VersionReq }, + + #[error("no crate information available for '{crate_name}' version '{v}'")] + #[diagnostic(code(binstall::version::unavailable))] + VersionUnavailable { + crate_name: String, + v: semver::Version, + }, + + #[error("could not {method} {url}: {err}")] + #[diagnostic(code(binstall::http))] + Http { + method: reqwest::Method, + url: url::Url, + #[source] + err: reqwest::Error, + }, +} diff --git a/src/fetchers.rs b/src/fetchers.rs index e4474f12..44fdf57e 100644 --- a/src/fetchers.rs +++ b/src/fetchers.rs @@ -4,7 +4,7 @@ pub use gh_crate_meta::*; pub use log::debug; pub use quickinstall::*; -use crate::{PkgFmt, PkgMeta}; +use crate::{BinstallError, PkgFmt, PkgMeta}; mod gh_crate_meta; mod quickinstall; @@ -17,10 +17,10 @@ pub trait Fetcher { Self: Sized; /// Fetch a package - async fn fetch(&self, dst: &Path) -> Result<(), anyhow::Error>; + async fn fetch(&self, dst: &Path) -> Result<(), BinstallError>; /// Check if a package is available for download - async fn check(&self) -> Result; + async fn check(&self) -> Result; /// Return the package format fn pkg_fmt(&self) -> PkgFmt; diff --git a/src/fetchers/gh_crate_meta.rs b/src/fetchers/gh_crate_meta.rs index d4e1150b..c0f4035a 100644 --- a/src/fetchers/gh_crate_meta.rs +++ b/src/fetchers/gh_crate_meta.rs @@ -6,14 +6,14 @@ use serde::Serialize; use url::Url; use super::Data; -use crate::{download, remote_exists, PkgFmt, Template}; +use crate::{download, remote_exists, BinstallError, PkgFmt, Template}; pub struct GhCrateMeta { data: Data, } impl GhCrateMeta { - fn url(&self) -> Result { + fn url(&self) -> Result { let ctx = Context::from_data(&self.data); debug!("Using context: {:?}", ctx); Ok(ctx.render_url(&self.data.meta.pkg_url)?) @@ -26,13 +26,13 @@ impl super::Fetcher for GhCrateMeta { Box::new(Self { data: data.clone() }) } - async fn check(&self) -> Result { + async fn check(&self) -> Result { let url = self.url()?; info!("Checking for package at: '{url}'"); remote_exists(url.as_str(), Method::HEAD).await } - async fn fetch(&self, dst: &Path) -> Result<(), anyhow::Error> { + async fn fetch(&self, dst: &Path) -> Result<(), BinstallError> { let url = self.url()?; info!("Downloading package from: '{url}'"); download(url.as_str(), dst).await @@ -101,7 +101,7 @@ impl<'c> Context<'c> { } } - pub(self) fn render_url(&self, template: &str) -> Result { + pub(self) fn render_url(&self, template: &str) -> Result { Ok(Url::parse(&self.render(template)?)?) } } diff --git a/src/fetchers/quickinstall.rs b/src/fetchers/quickinstall.rs index b04a8ec2..61871e5c 100644 --- a/src/fetchers/quickinstall.rs +++ b/src/fetchers/quickinstall.rs @@ -2,9 +2,10 @@ use std::path::Path; use log::info; use reqwest::Method; +use url::Url; use super::Data; -use crate::{download, remote_exists, PkgFmt}; +use crate::{download, remote_exists, BinstallError, PkgFmt}; const BASE_URL: &str = "https://github.com/alsuren/cargo-quickinstall/releases/download"; const STATS_URL: &str = "https://warehouse-clerk-tmp.vercel.app/api/crate"; @@ -25,14 +26,14 @@ impl super::Fetcher for QuickInstall { }) } - async fn check(&self) -> Result { + async fn check(&self) -> Result { let url = self.package_url(); self.report().await?; info!("Checking for package at: '{url}'"); remote_exists(&url, Method::HEAD).await } - async fn fetch(&self, dst: &Path) -> Result<(), anyhow::Error> { + async fn fetch(&self, dst: &Path) -> Result<(), BinstallError> { let url = self.package_url(); info!("Downloading package from: '{url}'"); download(&url, &dst).await @@ -68,14 +69,20 @@ impl QuickInstall { ) } - pub async fn report(&self) -> Result<(), anyhow::Error> { + pub async fn report(&self) -> Result<(), BinstallError> { info!("Sending installation report to quickinstall (anonymous)"); + let url = Url::parse(&self.stats_url())?; reqwest::Client::builder() .user_agent(USER_AGENT) .build()? - .request(Method::HEAD, &self.stats_url()) + .request(Method::HEAD, url.clone()) .send() - .await?; + .await + .map_err(|err| BinstallError::Http { + method: Method::HEAD, + url, + err, + })?; Ok(()) } } diff --git a/src/helpers.rs b/src/helpers.rs index 64647f18..327f9240 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,24 +1,24 @@ use std::path::{Path, PathBuf}; -use log::{debug, error, info}; +use log::{debug, info}; use cargo_toml::Manifest; use flate2::read::GzDecoder; +use reqwest::Method; use serde::Serialize; use tar::Archive; use tinytemplate::TinyTemplate; +use url::Url; use xz2::read::XzDecoder; use zip::read::ZipArchive; use zstd::stream::Decoder as ZstdDecoder; -use crate::Meta; - -use super::PkgFmt; +use crate::{BinstallError, Meta, PkgFmt}; /// Load binstall metadata from the crate `Cargo.toml` at the provided path pub fn load_manifest_path>( manifest_path: P, -) -> Result, anyhow::Error> { +) -> Result, BinstallError> { debug!("Reading manifest: {}", manifest_path.as_ref().display()); // Load and parse manifest (this checks file system for binary output names) @@ -28,21 +28,24 @@ pub fn load_manifest_path>( Ok(manifest) } -pub async fn remote_exists(url: &str, method: reqwest::Method) -> Result { +pub async fn remote_exists(url: &str, method: reqwest::Method) -> Result { let req = reqwest::Client::new().request(method, url).send().await?; Ok(req.status().is_success()) } /// Download a file from the provided URL to the provided path -pub async fn download>(url: &str, path: P) -> Result<(), anyhow::Error> { +pub async fn download>(url: &str, path: P) -> Result<(), BinstallError> { + let url = Url::parse(url)?; debug!("Downloading from: '{}'", url); - let resp = reqwest::get(url).await?; - - if !resp.status().is_success() { - error!("Download error: {}", resp.status()); - return Err(anyhow::anyhow!(resp.status())); - } + let resp = reqwest::get(url.clone()) + .await + .and_then(|r| r.error_for_status()) + .map_err(|err| BinstallError::Http { + method: Method::GET, + url, + err, + })?; let bytes = resp.bytes().await?; @@ -60,7 +63,7 @@ pub fn extract, P: AsRef>( source: S, fmt: PkgFmt, path: P, -) -> Result<(), anyhow::Error> { +) -> Result<(), BinstallError> { match fmt { PkgFmt::Tar => { // Extract to install dir @@ -188,23 +191,23 @@ pub fn get_install_path>(install_path: Option

) -> Option Result { - info!("Do you wish to continue? yes/no"); +pub fn confirm() -> Result<(), BinstallError> { + loop { + info!("Do you wish to continue? yes/no"); - let mut input = String::new(); - std::io::stdin().read_line(&mut input)?; + let mut input = String::new(); + std::io::stdin().read_line(&mut input).unwrap(); - match input.as_str().trim() { - "yes" => Ok(true), - "no" => Ok(false), - _ => Err(anyhow::anyhow!( - "Valid options are 'yes', 'no', please try again" - )), + match input.as_str().trim() { + "yes" | "y" | "YES" | "Y" => break Ok(()), + "no" | "n" | "NO" | "N" => break Err(BinstallError::UserAbort), + _ => continue, + } } } pub trait Template: Serialize { - fn render(&self, template: &str) -> Result + fn render(&self, template: &str) -> Result where Self: Sized, { diff --git a/src/lib.rs b/src/lib.rs index dc7e4569..06f84c45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,11 +3,14 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString, EnumVariantNames}; +pub mod drivers; +pub use drivers::*; + pub mod helpers; pub use helpers::*; -pub mod drivers; -pub use drivers::*; +mod errors; +pub use errors::BinstallError; pub mod bins; pub mod fetchers; diff --git a/src/main.rs b/src/main.rs index f0a0c5e6..514ef287 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,12 @@ -use std::{path::PathBuf, str::FromStr}; +use std::{path::PathBuf, str::FromStr, time::Instant}; use cargo_toml::{Package, Product}; use log::{debug, error, info, warn, LevelFilter}; +use miette::{miette, IntoDiagnostic, Result, WrapErr}; use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode}; use structopt::StructOpt; use tempfile::TempDir; -use tokio::process::Command; +use tokio::{process::Command, runtime::Runtime}; use cargo_binstall::{ bins, @@ -74,8 +75,31 @@ struct Options { pkg_url: Option, } -#[tokio::main] -async fn main() -> Result<(), anyhow::Error> { +fn main() -> Result<()> { + let start = Instant::now(); + + let rt = Runtime::new().unwrap(); + let result = rt.block_on(entry()); + drop(rt); + + let done = start.elapsed(); + + if let Err(err) = result { + debug!("run time: {done:?}"); + + if let Some(BinstallError::UserAbort) = err.downcast_ref::() { + warn!("Installation cancelled"); + Ok(()) + } else { + Err(err) + } + } else { + info!("Installation complete! [{done:?}]"); + Ok(()) + } +} + +async fn entry() -> Result<()> { // Filter extraneous arg when invoked by cargo // `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"] // `cargo binstall --help` gives ["/home/ryan/.cargo/bin/cargo-binstall", "binstall", "--help"] @@ -106,7 +130,9 @@ async fn main() -> Result<(), anyhow::Error> { .unwrap(); // Create a temporary directory for downloads etc. - let temp_dir = TempDir::new()?; + let temp_dir = TempDir::new() + .map_err(BinstallError::from) + .wrap_err("Creating a temporary directory failed.")?; info!("Installing package: '{}'", opts.name); @@ -129,9 +155,8 @@ async fn main() -> Result<(), anyhow::Error> { o=opts.version, p=package.version ); - if opts.no_confirm || opts.dry_run || !confirm()? { - warn!("Installation cancelled"); - return Ok(()); + if !opts.no_confirm && !opts.dry_run { + confirm()?; } } @@ -156,7 +181,7 @@ async fn main() -> Result<(), anyhow::Error> { // Compute install directory let install_path = get_install_path(opts.install_path.as_deref()).ok_or_else(|| { error!("No viable install path found of specified, try `--install-path`"); - anyhow::anyhow!("No install path found or specified") + miette!("No install path found or specified") })?; debug!("Using install path: {}", install_path.display()); @@ -211,16 +236,15 @@ async fn install_from_package( package: Package, pkg_path: PathBuf, temp_dir: TempDir, -) -> Result<(), anyhow::Error> { +) -> Result<()> { // Prompt user for third-party source if fetcher.is_third_party() { warn!( "The package will be downloaded from third-party source {}", fetcher.source_name() ); - if !opts.no_confirm && !opts.dry_run && !confirm()? { - warn!("Installation cancelled"); - return Ok(()); + if !opts.no_confirm && !opts.dry_run { + confirm()?; } } else { info!( @@ -274,7 +298,7 @@ async fn install_from_package( if binaries.is_empty() { error!("No binaries specified (or inferred from file system)"); - return Err(anyhow::anyhow!( + return Err(miette!( "No binaries specified (or inferred from file system)" )); } @@ -295,7 +319,7 @@ async fn install_from_package( let bin_files = binaries .iter() .map(|p| bins::BinFile::from_product(&bin_data, p)) - .collect::, anyhow::Error>>()?; + .collect::, BinstallError>>()?; // Prompt user for confirmation info!("This will install the following binaries:"); @@ -315,9 +339,8 @@ async fn install_from_package( return Ok(()); } - if !opts.no_confirm && !confirm()? { - warn!("Installation cancelled"); - return Ok(()); + if !opts.no_confirm { + confirm()?; } info!("Installing binaries..."); @@ -332,8 +355,6 @@ async fn install_from_package( } } - info!("Installation complete!"); - if opts.no_cleanup { let _ = temp_dir.into_path(); } else { @@ -345,12 +366,11 @@ async fn install_from_package( Ok(()) } -async fn install_from_source(opts: Options, package: Package) -> Result<(), anyhow::Error> { +async fn install_from_source(opts: Options, package: Package) -> Result<()> { // Prompt user for source install warn!("The package will be installed from source (with cargo)",); - if !opts.no_confirm && !opts.dry_run && !confirm()? { - warn!("Installation cancelled"); - return Ok(()); + if !opts.no_confirm && !opts.dry_run { + confirm()?; } if opts.dry_run { @@ -371,16 +391,22 @@ async fn install_from_source(opts: Options, package: Package) -> Result<() .arg(package.version) .arg("--target") .arg(opts.target) - .spawn()?; + .spawn() + .into_diagnostic() + .wrap_err("Spawning cargo install failed.")?; debug!("Spawned command pid={:?}", child.id()); - let status = child.wait().await?; + let status = child + .wait() + .await + .into_diagnostic() + .wrap_err("Running cargo install failed.")?; if status.success() { info!("Cargo finished successfully"); Ok(()) } else { error!("Cargo errored! {:?}", status); - Err(anyhow::anyhow!("Cargo install error")) + Err(miette!("Cargo install error")) } } } From f9e69503b05439eb7557a5ac94db376ac82cfc71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 21:15:52 +1200 Subject: [PATCH 02/17] Document the library error types --- src/errors.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index 97705be4..b369a537 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -2,35 +2,54 @@ use miette::Diagnostic; use thiserror::Error; #[derive(Error, Diagnostic, Debug)] +#[diagnostic(url(docsrs))] pub enum BinstallError { + /// The installation was cancelled by a user at a confirmation prompt. #[error("installation cancelled by user")] #[diagnostic(code(binstall::user_abort))] UserAbort, + /// A generic I/O error. #[error(transparent)] #[diagnostic(code(binstall::io))] Io(#[from] std::io::Error), + /// A URL is invalid. + /// + /// This may be the result of a template in a Cargo manifest. #[error(transparent)] #[diagnostic(code(binstall::url_parse))] UrlParse(#[from] url::ParseError), + /// A generic error from our HTTP client, reqwest. + /// + /// Errors resulting from HTTP fetches are handled with [`BinstallError::Http`] instead. #[error(transparent)] #[diagnostic(code(binstall::reqwest))] Reqwest(#[from] reqwest::Error), + /// A rendering error in a template. #[error(transparent)] #[diagnostic(code(binstall::template))] Template(#[from] tinytemplate::error::Error), + /// An error while unzipping a file. #[error(transparent)] #[diagnostic(code(binstall::unzip))] Unzip(#[from] zip::result::ZipError), + /// A parsing or validation error in a cargo manifest. + /// + /// This should be rare, as manifests are generally fetched from crates.io, which does its own + /// validation upstream. The most common failure will therefore be for direct repository access + /// and with the `--manifest-path` option. #[error(transparent)] #[diagnostic(code(binstall::cargo_manifest))] CargoManifest(#[from] cargo_toml::Error), + /// An error interacting with the crates.io API. + /// + /// This could either be a "not found" or a server/transport error. #[error("crates.io api error fetching crate information for '{crate_name}': {err}")] #[diagnostic(code(binstall::crates_io_api))] CratesIoApi { @@ -39,6 +58,10 @@ pub enum BinstallError { err: crates_io_api::Error, }, + /// A version is not valid semver. + /// + /// Note that we use the [`semver`] crate, which parses Cargo version syntax; this may be + /// somewhat stricter or very slightly different from other semver implementations. #[error("version string '{v}' is not semver: {err}")] #[diagnostic(code(binstall::version::parse))] VersionParse { @@ -47,6 +70,12 @@ pub enum BinstallError { err: semver::Error, }, + /// A version requirement is not valid. + /// + /// This is usually provided via the `--version` option. + /// + /// Note that we use the [`semver`] crate, which parses Cargo version requirement syntax; they + /// may be slightly different from other semver requirements expressions implementations. #[error("version requirement '{req}' is not semver: {err}")] #[diagnostic(code(binstall::version::requirement))] VersionReq { @@ -55,10 +84,17 @@ pub enum BinstallError { err: semver::Error, }, + /// No available version matches the requirements. + /// + /// This may be the case when using the `--version` option. + /// + /// Note that using `--version 1.2.3` is interpreted as the requirement `^1.2.3` as per + /// Cargo.toml rules. If you want the exact version 1.2.3, use `--version '=1.2.3'`. #[error("no version matching requirement '{req}'")] #[diagnostic(code(binstall::version::mismatch))] VersionMismatch { req: semver::VersionReq }, + /// The crates.io API doesn't have manifest metadata for the given version. #[error("no crate information available for '{crate_name}' version '{v}'")] #[diagnostic(code(binstall::version::unavailable))] VersionUnavailable { @@ -66,6 +102,10 @@ pub enum BinstallError { v: semver::Version, }, + /// An HTTP request failed. + /// + /// This includes both connection/transport failures and when the HTTP status of the response + /// is not as expected. #[error("could not {method} {url}: {err}")] #[diagnostic(code(binstall::http))] Http { From f56ed6fc4c3a87c2fe4499854fec77e606506e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 22:00:24 +1200 Subject: [PATCH 03/17] Add per-error exit codes --- src/errors.rs | 133 ++++++++++++++++++++++++++++++++++++-------------- src/main.rs | 24 ++++++--- 2 files changed, 113 insertions(+), 44 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index b369a537..bdf2841f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,55 +1,77 @@ use miette::Diagnostic; use thiserror::Error; +/// Errors emitted by the library portion of cargo-binstall. #[derive(Error, Diagnostic, Debug)] #[diagnostic(url(docsrs))] +#[non_exhaustive] pub enum BinstallError { /// The installation was cancelled by a user at a confirmation prompt. + /// + /// - Exit code: 32 #[error("installation cancelled by user")] #[diagnostic(code(binstall::user_abort))] UserAbort, - /// A generic I/O error. - #[error(transparent)] - #[diagnostic(code(binstall::io))] - Io(#[from] std::io::Error), - /// A URL is invalid. /// /// This may be the result of a template in a Cargo manifest. + /// + /// - Exit code: 65 #[error(transparent)] #[diagnostic(code(binstall::url_parse))] UrlParse(#[from] url::ParseError), - /// A generic error from our HTTP client, reqwest. - /// - /// Errors resulting from HTTP fetches are handled with [`BinstallError::Http`] instead. - #[error(transparent)] - #[diagnostic(code(binstall::reqwest))] - Reqwest(#[from] reqwest::Error), - - /// A rendering error in a template. - #[error(transparent)] - #[diagnostic(code(binstall::template))] - Template(#[from] tinytemplate::error::Error), - /// An error while unzipping a file. + /// + /// - Exit code: 66 #[error(transparent)] #[diagnostic(code(binstall::unzip))] Unzip(#[from] zip::result::ZipError), - /// A parsing or validation error in a cargo manifest. + /// A rendering error in a template. /// - /// This should be rare, as manifests are generally fetched from crates.io, which does its own - /// validation upstream. The most common failure will therefore be for direct repository access - /// and with the `--manifest-path` option. + /// - Exit code: 67 #[error(transparent)] - #[diagnostic(code(binstall::cargo_manifest))] - CargoManifest(#[from] cargo_toml::Error), + #[diagnostic(code(binstall::template))] + Template(#[from] tinytemplate::error::Error), + + /// A generic error from our HTTP client, reqwest. + /// + /// Errors resulting from HTTP fetches are handled with [`BinstallError::Http`] instead. + /// + /// - Exit code: 68 + #[error(transparent)] + #[diagnostic(code(binstall::reqwest))] + Reqwest(#[from] reqwest::Error), + + /// An HTTP request failed. + /// + /// This includes both connection/transport failures and when the HTTP status of the response + /// is not as expected. + /// + /// - Exit code: 69 + #[error("could not {method} {url}: {err}")] + #[diagnostic(code(binstall::http))] + Http { + method: reqwest::Method, + url: url::Url, + #[source] + err: reqwest::Error, + }, + + /// A generic I/O error. + /// + /// - Exit code: 74 + #[error(transparent)] + #[diagnostic(code(binstall::io))] + Io(#[from] std::io::Error), /// An error interacting with the crates.io API. /// /// This could either be a "not found" or a server/transport error. + /// + /// - Exit code: 76 #[error("crates.io api error fetching crate information for '{crate_name}': {err}")] #[diagnostic(code(binstall::crates_io_api))] CratesIoApi { @@ -58,10 +80,23 @@ pub enum BinstallError { err: crates_io_api::Error, }, + /// A parsing or validation error in a cargo manifest. + /// + /// This should be rare, as manifests are generally fetched from crates.io, which does its own + /// validation upstream. The most common failure will therefore be for direct repository access + /// and with the `--manifest-path` option. + /// + /// - Exit code: 78 + #[error(transparent)] + #[diagnostic(code(binstall::cargo_manifest))] + CargoManifest(#[from] cargo_toml::Error), + /// A version is not valid semver. /// /// Note that we use the [`semver`] crate, which parses Cargo version syntax; this may be /// somewhat stricter or very slightly different from other semver implementations. + /// + /// - Exit code: 80 #[error("version string '{v}' is not semver: {err}")] #[diagnostic(code(binstall::version::parse))] VersionParse { @@ -76,6 +111,8 @@ pub enum BinstallError { /// /// Note that we use the [`semver`] crate, which parses Cargo version requirement syntax; they /// may be slightly different from other semver requirements expressions implementations. + /// + /// - Exit code: 81 #[error("version requirement '{req}' is not semver: {err}")] #[diagnostic(code(binstall::version::requirement))] VersionReq { @@ -90,28 +127,52 @@ pub enum BinstallError { /// /// Note that using `--version 1.2.3` is interpreted as the requirement `^1.2.3` as per /// Cargo.toml rules. If you want the exact version 1.2.3, use `--version '=1.2.3'`. + /// + /// - Exit code: 82 #[error("no version matching requirement '{req}'")] #[diagnostic(code(binstall::version::mismatch))] VersionMismatch { req: semver::VersionReq }, /// The crates.io API doesn't have manifest metadata for the given version. + /// + /// - Exit code: 83 #[error("no crate information available for '{crate_name}' version '{v}'")] #[diagnostic(code(binstall::version::unavailable))] VersionUnavailable { crate_name: String, v: semver::Version, }, - - /// An HTTP request failed. - /// - /// This includes both connection/transport failures and when the HTTP status of the response - /// is not as expected. - #[error("could not {method} {url}: {err}")] - #[diagnostic(code(binstall::http))] - Http { - method: reqwest::Method, - url: url::Url, - #[source] - err: reqwest::Error, - }, +} + +impl BinstallError { + /// The recommended exit code for this error. + /// + /// This will never output: + /// - 0 (success) + /// - 1 and 2 (catchall and shell) + /// - 16 (binstall errors not handled here) + /// - 64 (generic error) + pub fn exit_code(&self) -> u8 { + use BinstallError::*; + let code = match self { + UserAbort => 32, + UrlParse(_) => 65, + Unzip(_) => 66, + Template(_) => 67, + Reqwest(_) => 68, + Http { .. } => 69, + Io(_) => 74, + CratesIoApi { .. } => 76, + CargoManifest { .. } => 78, + VersionParse { .. } => 80, + VersionReq { .. } => 81, + VersionMismatch { .. } => 82, + VersionUnavailable { .. } => 83, + }; + + // reserved codes + debug_assert!(code != 64 && code != 16 && code != 1 && code != 2 && code != 0); + + code + } } diff --git a/src/main.rs b/src/main.rs index 514ef287..1b842388 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, str::FromStr, time::Instant}; +use std::{path::PathBuf, process::exit, str::FromStr, time::Instant}; use cargo_toml::{Package, Product}; use log::{debug, error, info, warn, LevelFilter}; @@ -75,7 +75,7 @@ struct Options { pkg_url: Option, } -fn main() -> Result<()> { +fn main() -> ! { let start = Instant::now(); let rt = Runtime::new().unwrap(); @@ -87,15 +87,23 @@ fn main() -> Result<()> { if let Err(err) = result { debug!("run time: {done:?}"); - if let Some(BinstallError::UserAbort) = err.downcast_ref::() { - warn!("Installation cancelled"); - Ok(()) - } else { - Err(err) + match err.downcast::() { + Ok(liberr @ BinstallError::UserAbort) => { + warn!("Installation cancelled"); + exit(liberr.exit_code() as _); + } + Ok(liberr) => { + eprintln!("{liberr:?}"); + exit(liberr.exit_code() as _); + } + Err(binerr) => { + eprintln!("{binerr:?}"); + exit(16); + } } } else { info!("Installation complete! [{done:?}]"); - Ok(()) + exit(0); } } From bd35c473a934d9040b88dfbddfb99c8f573dce41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 22:15:20 +1200 Subject: [PATCH 04/17] Use new Termination trait --- src/errors.rs | 21 ++++++++++++++++++--- src/main.rs | 51 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index bdf2841f..544272dc 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,6 @@ +use std::process::{ExitCode, Termination}; + +use log::warn; use miette::Diagnostic; use thiserror::Error; @@ -152,9 +155,9 @@ impl BinstallError { /// - 1 and 2 (catchall and shell) /// - 16 (binstall errors not handled here) /// - 64 (generic error) - pub fn exit_code(&self) -> u8 { + pub fn exit_code(&self) -> ExitCode { use BinstallError::*; - let code = match self { + let code: u8 = match self { UserAbort => 32, UrlParse(_) => 65, Unzip(_) => 66, @@ -173,6 +176,18 @@ impl BinstallError { // reserved codes debug_assert!(code != 64 && code != 16 && code != 1 && code != 2 && code != 0); - code + code.into() + } +} + +impl Termination for BinstallError { + fn report(self) -> ExitCode { + if let BinstallError::UserAbort = self { + warn!("Installation cancelled"); + } else { + eprintln!("{self:?}"); + } + + self.exit_code() } } diff --git a/src/main.rs b/src/main.rs index 1b842388..2fe56c1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,9 @@ -use std::{path::PathBuf, process::exit, str::FromStr, time::Instant}; +use std::{ + path::PathBuf, + process::{ExitCode, Termination}, + str::FromStr, + time::{Duration, Instant}, +}; use cargo_toml::{Package, Product}; use log::{debug, error, info, warn, LevelFilter}; @@ -75,7 +80,29 @@ struct Options { pkg_url: Option, } -fn main() -> ! { +enum MainExit { + Success(Duration), + Error(BinstallError), + Report(miette::Report), +} + +impl Termination for MainExit { + fn report(self) -> ExitCode { + match self { + Self::Success(spent) => { + info!("Installation complete! [{spent:?}]"); + ExitCode::SUCCESS + } + Self::Error(err) => err.report(), + Self::Report(err) => { + eprintln!("{err:?}"); + ExitCode::from(16) + } + } + } +} + +fn main() -> MainExit { let start = Instant::now(); let rt = Runtime::new().unwrap(); @@ -83,27 +110,15 @@ fn main() -> ! { drop(rt); let done = start.elapsed(); + debug!("run time: {done:?}"); if let Err(err) = result { - debug!("run time: {done:?}"); - match err.downcast::() { - Ok(liberr @ BinstallError::UserAbort) => { - warn!("Installation cancelled"); - exit(liberr.exit_code() as _); - } - Ok(liberr) => { - eprintln!("{liberr:?}"); - exit(liberr.exit_code() as _); - } - Err(binerr) => { - eprintln!("{binerr:?}"); - exit(16); - } + Ok(liberr) => MainExit::Error(liberr), + Err(binerr) => MainExit::Report(binerr), } } else { - info!("Installation complete! [{done:?}]"); - exit(0); + MainExit::Success(done) } } From 02c8c0af00f7cde2b466e639b2182228beb48d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 22:17:27 +1200 Subject: [PATCH 05/17] Avoid bare url --- src/helpers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers.rs b/src/helpers.rs index 327f9240..1f90ac69 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -153,7 +153,7 @@ pub fn extract, P: AsRef>( } /// Fetch install path from environment -/// roughly follows https://doc.rust-lang.org/cargo/commands/cargo-install.html#description +/// roughly follows pub fn get_install_path>(install_path: Option

) -> Option { // Command line override first first if let Some(p) = install_path { From 84ebc0039e82ec63a8fb2536ad4999cf8fd1f72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 23:01:12 +1200 Subject: [PATCH 06/17] Pretty-print errors --- src/errors.rs | 7 ++++--- src/main.rs | 23 +++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 544272dc..e6e4ab94 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,7 +1,7 @@ use std::process::{ExitCode, Termination}; use log::warn; -use miette::Diagnostic; +use miette::{Report, Diagnostic}; use thiserror::Error; /// Errors emitted by the library portion of cargo-binstall. @@ -182,12 +182,13 @@ impl BinstallError { impl Termination for BinstallError { fn report(self) -> ExitCode { + let code = self.exit_code(); if let BinstallError::UserAbort = self { warn!("Installation cancelled"); } else { - eprintln!("{self:?}"); + eprintln!("{:?}", Report::new(self)); } - self.exit_code() + code } } diff --git a/src/main.rs b/src/main.rs index 2fe56c1b..fa70206d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,11 +90,15 @@ impl Termination for MainExit { fn report(self) -> ExitCode { match self { Self::Success(spent) => { - info!("Installation complete! [{spent:?}]"); + info!("Installation completed in {spent:?}"); ExitCode::SUCCESS } - Self::Error(err) => err.report(), + Self::Error(err) => { + error!("Fatal error:"); + err.report() + } Self::Report(err) => { + error!("Fatal error:"); eprintln!("{err:?}"); ExitCode::from(16) } @@ -112,14 +116,13 @@ fn main() -> MainExit { let done = start.elapsed(); debug!("run time: {done:?}"); - if let Err(err) = result { - match err.downcast::() { - Ok(liberr) => MainExit::Error(liberr), - Err(binerr) => MainExit::Report(binerr), - } - } else { - MainExit::Success(done) - } + result + .map(|_| MainExit::Success(done)) + .unwrap_or_else(|err| { + err.downcast::() + .map(MainExit::Error) + .unwrap_or_else(MainExit::Report) + }) } async fn entry() -> Result<()> { From 33f4c76826cbd9b9f4094effb78943fb52da5574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 23:01:45 +1200 Subject: [PATCH 07/17] Add help text for when a crate is not found --- src/errors.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index e6e4ab94..16f7233d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,7 +1,7 @@ use std::process::{ExitCode, Termination}; use log::warn; -use miette::{Report, Diagnostic}; +use miette::{Diagnostic, Report}; use thiserror::Error; /// Errors emitted by the library portion of cargo-binstall. @@ -76,7 +76,10 @@ pub enum BinstallError { /// /// - Exit code: 76 #[error("crates.io api error fetching crate information for '{crate_name}': {err}")] - #[diagnostic(code(binstall::crates_io_api))] + #[diagnostic( + code(binstall::crates_io_api), + help("Check that the crate name you provided is correct.\nYou can also search for a matching crate at: https://lib.rs/search?q={crate_name}") + )] CratesIoApi { crate_name: String, #[source] From d21c4a08752709e59f4fb55cd920f52b3960feff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 23:05:35 +1200 Subject: [PATCH 08/17] Add link to error code descriptions --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 57b337df..6cf74ca3 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ If your package already uses this approach, you shouldn't need to set anything. ### Examples -For example, the default configuration (as shown above) for a crate called `radio-sx128x` (version: `v0.14.1-alpha.5` on x86_64 linux) would be interpolated to: +For example, the default configuration (as shown above) for a crate called `radio-sx128x` (version: `v0.14.1-alpha.5` on x86\_64 linux) would be interpolated to: - A download URL of `https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/rust-radio-sx128x-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz` - Containing a single binary file `rust-radio-sx128x-x86_64-unknown-linux-gnu-v0.14.1-alpha.5/rust-radio-x86_64-unknown-linux-gnu` @@ -199,6 +199,8 @@ Which provides a binary path of: `sx128x-util-x86_64-unknown-linux-gnu[.exe]`. I - Yes and also no? We're not (yet? #1) doing anything to verify the CI binaries are produced by the right person / organisation. However, we're pulling data from crates.io and the cargo manifest, both of which are _already_ trusted entities, and this is functionally a replacement for `curl ... | bash` or `wget`-ing the same files, so, things can be improved but it's also sorta moot +- What do the error codes mean? + - You can find a full description of errors including exit codes here: --- From 7f0c818313131a7a7502fadf8ee51e6314f542af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 23:16:55 +1200 Subject: [PATCH 09/17] =?UTF-8?q?Don=E2=80=99t=20print=20"Fatal=20error"?= =?UTF-8?q?=20for=20UserAbort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/errors.rs | 3 ++- src/main.rs | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 16f7233d..cb3a2bd1 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,6 +1,6 @@ use std::process::{ExitCode, Termination}; -use log::warn; +use log::{error, warn}; use miette::{Diagnostic, Report}; use thiserror::Error; @@ -189,6 +189,7 @@ impl Termination for BinstallError { if let BinstallError::UserAbort = self { warn!("Installation cancelled"); } else { + error!("Fatal error:"); eprintln!("{:?}", Report::new(self)); } diff --git a/src/main.rs b/src/main.rs index fa70206d..5af8b851 100644 --- a/src/main.rs +++ b/src/main.rs @@ -93,10 +93,7 @@ impl Termination for MainExit { info!("Installation completed in {spent:?}"); ExitCode::SUCCESS } - Self::Error(err) => { - error!("Fatal error:"); - err.report() - } + Self::Error(err) => err.report(), Self::Report(err) => { error!("Fatal error:"); eprintln!("{err:?}"); From 52c0213d98a9f262c2dbfbc42eec4e5b96b3a37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 23:17:57 +1200 Subject: [PATCH 10/17] Show prompt for confirm --- src/helpers.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/helpers.rs b/src/helpers.rs index 1f90ac69..6c412b6d 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,3 +1,4 @@ +use std::io::{stderr, stdin, Write}; use std::path::{Path, PathBuf}; use log::{debug, info}; @@ -194,9 +195,11 @@ pub fn get_install_path>(install_path: Option

) -> Option Result<(), BinstallError> { loop { info!("Do you wish to continue? yes/no"); + eprint!("? "); + stderr().flush().ok(); let mut input = String::new(); - std::io::stdin().read_line(&mut input).unwrap(); + stdin().read_line(&mut input).unwrap(); match input.as_str().trim() { "yes" | "y" | "YES" | "Y" => break Ok(()), From c83c184983e3b0517fa10d733960cd4cfadbca2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 23:18:22 +1200 Subject: [PATCH 11/17] Default to no if nothing is entered on confirm --- src/helpers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers.rs b/src/helpers.rs index 6c412b6d..d6be62de 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -194,7 +194,7 @@ pub fn get_install_path>(install_path: Option

) -> Option Result<(), BinstallError> { loop { - info!("Do you wish to continue? yes/no"); + info!("Do you wish to continue? yes/[no]"); eprint!("? "); stderr().flush().ok(); @@ -203,7 +203,7 @@ pub fn confirm() -> Result<(), BinstallError> { match input.as_str().trim() { "yes" | "y" | "YES" | "Y" => break Ok(()), - "no" | "n" | "NO" | "N" => break Err(BinstallError::UserAbort), + "no" | "n" | "NO" | "N" | "" => break Err(BinstallError::UserAbort), _ => continue, } } From dee45f4b81de8b092d8ae75be32845d6789347f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 23:18:45 +1200 Subject: [PATCH 12/17] Correct HTTP error for remote_exists() helper --- src/helpers.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/helpers.rs b/src/helpers.rs index d6be62de..3501532c 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -29,8 +29,13 @@ pub fn load_manifest_path>( Ok(manifest) } -pub async fn remote_exists(url: &str, method: reqwest::Method) -> Result { - let req = reqwest::Client::new().request(method, url).send().await?; +pub async fn remote_exists(url: &str, method: Method) -> Result { + let url = Url::parse(url)?; + let req = reqwest::Client::new() + .request(method.clone(), url.clone()) + .send() + .await + .map_err(|err| BinstallError::Http { method, url, err })?; Ok(req.status().is_success()) } From 1cac046ffd2916d9498e7fe26597e3946fba3210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 23:19:56 +1200 Subject: [PATCH 13/17] Formatting --- src/helpers.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/helpers.rs b/src/helpers.rs index 3501532c..c1ffcec0 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,5 +1,8 @@ -use std::io::{stderr, stdin, Write}; -use std::path::{Path, PathBuf}; +use std::{ + fs, + io::{stderr, stdin, Write}, + path::{Path, PathBuf}, +}; use log::{debug, info}; @@ -58,8 +61,8 @@ pub async fn download>(url: &str, path: P) -> Result<(), Binstall let path = path.as_ref(); debug!("Download OK, writing to file: '{}'", path.display()); - std::fs::create_dir_all(path.parent().unwrap())?; - std::fs::write(&path, bytes)?; + fs::create_dir_all(path.parent().unwrap())?; + fs::write(&path, bytes)?; Ok(()) } @@ -79,7 +82,7 @@ pub fn extract, P: AsRef>( path.as_ref() ); - let dat = std::fs::File::open(source)?; + let dat = fs::File::open(source)?; let mut tar = Archive::new(dat); tar.unpack(path)?; @@ -92,7 +95,7 @@ pub fn extract, P: AsRef>( path.as_ref() ); - let dat = std::fs::File::open(source)?; + let dat = fs::File::open(source)?; let tar = GzDecoder::new(dat); let mut tgz = Archive::new(tar); @@ -106,7 +109,7 @@ pub fn extract, P: AsRef>( path.as_ref() ); - let dat = std::fs::File::open(source)?; + let dat = fs::File::open(source)?; let tar = XzDecoder::new(dat); let mut txz = Archive::new(tar); @@ -139,7 +142,7 @@ pub fn extract, P: AsRef>( path.as_ref() ); - let dat = std::fs::File::open(source)?; + let dat = fs::File::open(source)?; let mut zip = ZipArchive::new(dat)?; zip.extract(path)?; @@ -151,7 +154,7 @@ pub fn extract, P: AsRef>( path.as_ref() ); // Copy to install dir - std::fs::copy(source, path)?; + fs::copy(source, path)?; } }; From aa4339e07df3123ab30bd2fd57900e0725a90f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 23:24:40 +1200 Subject: [PATCH 14/17] Expose crate::errors module --- README.md | 2 +- src/lib.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6cf74ca3..ce95831d 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ Which provides a binary path of: `sx128x-util-x86_64-unknown-linux-gnu[.exe]`. I However, we're pulling data from crates.io and the cargo manifest, both of which are _already_ trusted entities, and this is functionally a replacement for `curl ... | bash` or `wget`-ing the same files, so, things can be improved but it's also sorta moot - What do the error codes mean? - - You can find a full description of errors including exit codes here: + - You can find a full description of errors including exit codes here: --- diff --git a/src/lib.rs b/src/lib.rs index 06f84c45..1fa840b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,12 +6,12 @@ use strum_macros::{Display, EnumString, EnumVariantNames}; pub mod drivers; pub use drivers::*; +pub mod errors; +pub use errors::*; + pub mod helpers; pub use helpers::*; -mod errors; -pub use errors::BinstallError; - pub mod bins; pub mod fetchers; From 529781a9a9ddb260a374268f2a59ffcd55669849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 31 May 2022 23:51:01 +1200 Subject: [PATCH 15/17] Format and link version mismatch warning --- src/errors.rs | 45 +++++++++++++++++++++++++++++++++------------ src/main.rs | 10 +++++++--- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index cb3a2bd1..6e3dc263 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -13,7 +13,7 @@ pub enum BinstallError { /// /// - Exit code: 32 #[error("installation cancelled by user")] - #[diagnostic(code(binstall::user_abort))] + #[diagnostic(severity(info), code(binstall::user_abort))] UserAbort, /// A URL is invalid. @@ -22,21 +22,21 @@ pub enum BinstallError { /// /// - Exit code: 65 #[error(transparent)] - #[diagnostic(code(binstall::url_parse))] + #[diagnostic(severity(error), code(binstall::url_parse))] UrlParse(#[from] url::ParseError), /// An error while unzipping a file. /// /// - Exit code: 66 #[error(transparent)] - #[diagnostic(code(binstall::unzip))] + #[diagnostic(severity(error), code(binstall::unzip))] Unzip(#[from] zip::result::ZipError), /// A rendering error in a template. /// /// - Exit code: 67 #[error(transparent)] - #[diagnostic(code(binstall::template))] + #[diagnostic(severity(error), code(binstall::template))] Template(#[from] tinytemplate::error::Error), /// A generic error from our HTTP client, reqwest. @@ -45,7 +45,7 @@ pub enum BinstallError { /// /// - Exit code: 68 #[error(transparent)] - #[diagnostic(code(binstall::reqwest))] + #[diagnostic(severity(error), code(binstall::reqwest))] Reqwest(#[from] reqwest::Error), /// An HTTP request failed. @@ -55,7 +55,7 @@ pub enum BinstallError { /// /// - Exit code: 69 #[error("could not {method} {url}: {err}")] - #[diagnostic(code(binstall::http))] + #[diagnostic(severity(error), code(binstall::http))] Http { method: reqwest::Method, url: url::Url, @@ -67,7 +67,7 @@ pub enum BinstallError { /// /// - Exit code: 74 #[error(transparent)] - #[diagnostic(code(binstall::io))] + #[diagnostic(severity(error), code(binstall::io))] Io(#[from] std::io::Error), /// An error interacting with the crates.io API. @@ -77,6 +77,7 @@ pub enum BinstallError { /// - Exit code: 76 #[error("crates.io api error fetching crate information for '{crate_name}': {err}")] #[diagnostic( + severity(error), code(binstall::crates_io_api), help("Check that the crate name you provided is correct.\nYou can also search for a matching crate at: https://lib.rs/search?q={crate_name}") )] @@ -94,7 +95,11 @@ pub enum BinstallError { /// /// - Exit code: 78 #[error(transparent)] - #[diagnostic(code(binstall::cargo_manifest))] + #[diagnostic( + severity(error), + code(binstall::cargo_manifest), + help("If you used --manifest-path, check the Cargo.toml syntax.") + )] CargoManifest(#[from] cargo_toml::Error), /// A version is not valid semver. @@ -104,7 +109,7 @@ pub enum BinstallError { /// /// - Exit code: 80 #[error("version string '{v}' is not semver: {err}")] - #[diagnostic(code(binstall::version::parse))] + #[diagnostic(severity(error), code(binstall::version::parse))] VersionParse { v: String, #[source] @@ -120,7 +125,7 @@ pub enum BinstallError { /// /// - Exit code: 81 #[error("version requirement '{req}' is not semver: {err}")] - #[diagnostic(code(binstall::version::requirement))] + #[diagnostic(severity(error), code(binstall::version::requirement))] VersionReq { req: String, #[source] @@ -136,18 +141,33 @@ pub enum BinstallError { /// /// - Exit code: 82 #[error("no version matching requirement '{req}'")] - #[diagnostic(code(binstall::version::mismatch))] + #[diagnostic(severity(error), code(binstall::version::mismatch))] VersionMismatch { req: semver::VersionReq }, /// The crates.io API doesn't have manifest metadata for the given version. /// /// - Exit code: 83 #[error("no crate information available for '{crate_name}' version '{v}'")] - #[diagnostic(code(binstall::version::unavailable))] + #[diagnostic(severity(error), code(binstall::version::unavailable))] VersionUnavailable { crate_name: String, v: semver::Version, }, + + /// Warning: The resolved version may not be what was meant. + /// + /// This occurs when using the `--version` option with a bare version, like `--version 1.2.3`. + /// That is parsed as the semver requirement `^1.2.3`, but the user may have expected that to + /// be an exact version (which should be specified with `--version '=1.2.3'`. + /// + /// - Exit code: none (runtime warning only) + #[error("version semantic mismatch: {ver} <> {req}")] + #[diagnostic( + severity(warning), + code(binstall::version::warning), + help("You specified `--version {req}` but the package resolved that to '{ver}'.\nUse `--version '={req}'` if you want an exact match.") + )] + VersionWarning { ver: String, req: String }, } impl BinstallError { @@ -174,6 +194,7 @@ impl BinstallError { VersionReq { .. } => 81, VersionMismatch { .. } => 82, VersionUnavailable { .. } => 83, + VersionWarning { .. } => unimplemented!("BUG: warnings do not terminate"), }; // reserved codes diff --git a/src/main.rs b/src/main.rs index 5af8b851..000ae381 100644 --- a/src/main.rs +++ b/src/main.rs @@ -173,9 +173,13 @@ async fn entry() -> Result<()> { let is_plain_version = semver::Version::from_str(&opts.version).is_ok(); if is_plain_version && package.version != opts.version { - warn!( - "You specified `--version {o}` but the package resolved that to '{p}', use `={o}` if you want an exact match", - o=opts.version, p=package.version + warn!("Warning!"); + eprintln!( + "{:?}", + miette::Report::new(BinstallError::VersionWarning { + ver: package.version.clone(), + req: opts.version.clone() + }) ); if !opts.no_confirm && !opts.dry_run { From 77b331f97cbde4bd47f4dc132aeb9a54152557d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Wed, 1 Jun 2022 00:05:26 +1200 Subject: [PATCH 16/17] Add error codes to documentation --- src/errors.rs | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 6e3dc263..070c91ce 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -11,7 +11,8 @@ use thiserror::Error; pub enum BinstallError { /// The installation was cancelled by a user at a confirmation prompt. /// - /// - Exit code: 32 + /// - Code: `binstall::user_abort` + /// - Exit: 32 #[error("installation cancelled by user")] #[diagnostic(severity(info), code(binstall::user_abort))] UserAbort, @@ -20,21 +21,24 @@ pub enum BinstallError { /// /// This may be the result of a template in a Cargo manifest. /// - /// - Exit code: 65 + /// - Code: `binstall::url_parse` + /// - Exit: 65 #[error(transparent)] #[diagnostic(severity(error), code(binstall::url_parse))] UrlParse(#[from] url::ParseError), /// An error while unzipping a file. /// - /// - Exit code: 66 + /// - Code: `binstall::unzip` + /// - Exit: 66 #[error(transparent)] #[diagnostic(severity(error), code(binstall::unzip))] Unzip(#[from] zip::result::ZipError), /// A rendering error in a template. /// - /// - Exit code: 67 + /// - Code: `binstall::template` + /// - Exit: 67 #[error(transparent)] #[diagnostic(severity(error), code(binstall::template))] Template(#[from] tinytemplate::error::Error), @@ -43,7 +47,8 @@ pub enum BinstallError { /// /// Errors resulting from HTTP fetches are handled with [`BinstallError::Http`] instead. /// - /// - Exit code: 68 + /// - Code: `binstall::reqwest` + /// - Exit: 68 #[error(transparent)] #[diagnostic(severity(error), code(binstall::reqwest))] Reqwest(#[from] reqwest::Error), @@ -53,7 +58,8 @@ pub enum BinstallError { /// This includes both connection/transport failures and when the HTTP status of the response /// is not as expected. /// - /// - Exit code: 69 + /// - Code: `binstall::http` + /// - Exit: 69 #[error("could not {method} {url}: {err}")] #[diagnostic(severity(error), code(binstall::http))] Http { @@ -65,7 +71,8 @@ pub enum BinstallError { /// A generic I/O error. /// - /// - Exit code: 74 + /// - Code: `binstall::io` + /// - Exit: 74 #[error(transparent)] #[diagnostic(severity(error), code(binstall::io))] Io(#[from] std::io::Error), @@ -74,7 +81,8 @@ pub enum BinstallError { /// /// This could either be a "not found" or a server/transport error. /// - /// - Exit code: 76 + /// - Code: `binstall::crates_io_api` + /// - Exit: 76 #[error("crates.io api error fetching crate information for '{crate_name}': {err}")] #[diagnostic( severity(error), @@ -93,7 +101,8 @@ pub enum BinstallError { /// validation upstream. The most common failure will therefore be for direct repository access /// and with the `--manifest-path` option. /// - /// - Exit code: 78 + /// - Code: `binstall::cargo_manifest` + /// - Exit: 78 #[error(transparent)] #[diagnostic( severity(error), @@ -107,7 +116,8 @@ pub enum BinstallError { /// Note that we use the [`semver`] crate, which parses Cargo version syntax; this may be /// somewhat stricter or very slightly different from other semver implementations. /// - /// - Exit code: 80 + /// - Code: `binstall::version::parse` + /// - Exit: 80 #[error("version string '{v}' is not semver: {err}")] #[diagnostic(severity(error), code(binstall::version::parse))] VersionParse { @@ -123,7 +133,8 @@ pub enum BinstallError { /// Note that we use the [`semver`] crate, which parses Cargo version requirement syntax; they /// may be slightly different from other semver requirements expressions implementations. /// - /// - Exit code: 81 + /// - Code: `binstall::version::requirement` + /// - Exit: 81 #[error("version requirement '{req}' is not semver: {err}")] #[diagnostic(severity(error), code(binstall::version::requirement))] VersionReq { @@ -139,14 +150,16 @@ pub enum BinstallError { /// Note that using `--version 1.2.3` is interpreted as the requirement `^1.2.3` as per /// Cargo.toml rules. If you want the exact version 1.2.3, use `--version '=1.2.3'`. /// - /// - Exit code: 82 + /// - Code: `binstall::version::mismatch` + /// - Exit: 82 #[error("no version matching requirement '{req}'")] #[diagnostic(severity(error), code(binstall::version::mismatch))] VersionMismatch { req: semver::VersionReq }, /// The crates.io API doesn't have manifest metadata for the given version. /// - /// - Exit code: 83 + /// - Code: `binstall::version::unavailable` + /// - Exit: 83 #[error("no crate information available for '{crate_name}' version '{v}'")] #[diagnostic(severity(error), code(binstall::version::unavailable))] VersionUnavailable { @@ -160,7 +173,8 @@ pub enum BinstallError { /// That is parsed as the semver requirement `^1.2.3`, but the user may have expected that to /// be an exact version (which should be specified with `--version '=1.2.3'`. /// - /// - Exit code: none (runtime warning only) + /// - Code: `binstall::version::warning` + /// - Exit: none (runtime warning only) #[error("version semantic mismatch: {ver} <> {req}")] #[diagnostic( severity(warning), From 73b3ac1adb2c245c30b0d0884114c25f1eb47cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Wed, 1 Jun 2022 00:30:28 +1200 Subject: [PATCH 17/17] Drop tokio macros --- Cargo.lock | 12 ------------ Cargo.toml | 5 ++++- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c67c8f1d..b430ff06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1450,21 +1450,9 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "socket2", - "tokio-macros", "winapi", ] -[[package]] -name = "tokio-macros" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tokio-rustls" version = "0.23.4" diff --git a/Cargo.toml b/Cargo.toml index 60265956..dc1cee79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,9 +41,11 @@ tinytemplate = "1.2.1" # This crate uses features rt-multi-thread and macros in `#[tokio::main]` and # uses feature process to create process. -tokio = { version = "1.18.0", features = [ "rt-multi-thread", "macros", "process" ], default-features = false } +tokio = { version = "1.18.0", features = [ "rt-multi-thread", "process" ], default-features = false } + url = "2.2.2" xz2 = "0.1.6" + # Disable all features of zip except for features of compression algorithms: # Disable features include: # - aes-crypto: Enables decryption of files which were encrypted with AES, absolutely zero use for @@ -51,6 +53,7 @@ xz2 = "0.1.6" # - time: Enables features using the [time](https://github.com/time-rs/time) crate, # which is not used by this crate. zip = { version = "0.6.2", default-features = false, features = [ "deflate", "bzip2", "zstd" ] } + # zstd is also depended by zip. # Since zip 0.6.2 depends on zstd 0.10.0, we also have to use 0.10.0 here, # otherwise there will be a link conflict.