Refactor for rich errors, split user abort and genuine error

This commit is contained in:
Félix Saparelli 2022-05-31 20:51:32 +12:00
parent 3c38a2f0eb
commit c0eaffb05d
No known key found for this signature in database
GPG key ID: B948C4BAE44FC474
11 changed files with 340 additions and 138 deletions

117
Cargo.lock generated
View file

@ -26,12 +26,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "anyhow"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.53" version = "0.1.53"
@ -147,7 +141,6 @@ dependencies = [
name = "cargo-binstall" name = "cargo-binstall"
version = "0.7.0" version = "0.7.0"
dependencies = [ dependencies = [
"anyhow",
"async-trait", "async-trait",
"cargo_metadata", "cargo_metadata",
"cargo_toml", "cargo_toml",
@ -156,6 +149,7 @@ dependencies = [
"env_logger", "env_logger",
"flate2", "flate2",
"log", "log",
"miette",
"reqwest", "reqwest",
"semver", "semver",
"serde", "serde",
@ -165,6 +159,7 @@ dependencies = [
"strum_macros", "strum_macros",
"tar", "tar",
"tempfile", "tempfile",
"thiserror",
"tinytemplate", "tinytemplate",
"tokio", "tokio",
"url", "url",
@ -262,7 +257,7 @@ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
"strsim", "strsim",
"textwrap", "textwrap 0.11.0",
"unicode-width", "unicode-width",
"vec_map", "vec_map",
] ]
@ -676,6 +671,12 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
[[package]]
name = "is_ci"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.2" version = "1.0.2"
@ -770,6 +771,36 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 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]] [[package]]
name = "mime" name = "mime"
version = "0.3.16" version = "0.3.16"
@ -857,6 +888,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
[[package]]
name = "owo-colors"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b"
[[package]] [[package]]
name = "peeking_take_while" name = "peeking_take_while"
version = "0.1.2" version = "0.1.2"
@ -1161,6 +1198,12 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
[[package]]
name = "smawk"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.4.4" version = "0.4.4"
@ -1226,6 +1269,34 @@ dependencies = [
"syn", "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]] [[package]]
name = "syn" name = "syn"
version = "1.0.95" version = "1.0.95"
@ -1271,6 +1342,16 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.11.0" version = "0.11.0"
@ -1280,6 +1361,17 @@ dependencies = [
"unicode-width", "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]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.31" version = "1.0.31"
@ -1463,6 +1555,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" 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]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
version = "0.1.19" version = "0.1.19"

View file

@ -19,7 +19,6 @@ pkg-fmt = "zip"
pkg-fmt = "zip" pkg-fmt = "zip"
[dependencies] [dependencies]
anyhow = "1.0.57"
async-trait = "0.1.52" async-trait = "0.1.52"
cargo_metadata = "0.14.2" cargo_metadata = "0.14.2"
cargo_toml = "0.11.4" 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" dirs = "4.0.0"
flate2 = { version = "1.0.24", features = ["zlib-ng"], default-features = false } flate2 = { version = "1.0.24", features = ["zlib-ng"], default-features = false }
log = "0.4.14" log = "0.4.14"
miette = { version = "4.7.1", features = ["fancy-no-backtrace"] }
reqwest = { version = "0.11.10", features = [ "rustls-tls" ], default-features = false } reqwest = { version = "0.11.10", features = [ "rustls-tls" ], default-features = false }
semver = "1.0.7" semver = "1.0.7"
serde = { version = "1.0.136", features = [ "derive" ] } serde = { version = "1.0.136", features = [ "derive" ] }
@ -36,6 +36,7 @@ strum = "0.24.0"
strum_macros = "0.24.0" strum_macros = "0.24.0"
tar = "0.4.38" tar = "0.4.38"
tempfile = "3.3.0" tempfile = "3.3.0"
thiserror = "1.0.31"
tinytemplate = "1.2.1" tinytemplate = "1.2.1"
# This crate uses features rt-multi-thread and macros in `#[tokio::main]` and # This crate uses features rt-multi-thread and macros in `#[tokio::main]` and

View file

@ -4,7 +4,7 @@ use cargo_toml::Product;
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use crate::{PkgFmt, PkgMeta, Template}; use crate::{BinstallError, PkgFmt, PkgMeta, Template};
pub struct BinFile { pub struct BinFile {
pub base_name: String, pub base_name: String,
@ -14,7 +14,7 @@ pub struct BinFile {
} }
impl BinFile { impl BinFile {
pub fn from_product(data: &Data, product: &Product) -> Result<Self, anyhow::Error> { pub fn from_product(data: &Data, product: &Product) -> Result<Self, BinstallError> {
let base_name = product.name.clone().unwrap(); let base_name = product.name.clone().unwrap();
let binary_ext = if data.target.contains("windows") { 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 // TODO: check if file already exists
debug!( debug!(
"Copy file from '{}' to '{}'", "Copy file from '{}' to '{}'",
@ -96,7 +96,7 @@ impl BinFile {
Ok(()) Ok(())
} }
pub fn install_link(&self) -> Result<(), anyhow::Error> { pub fn install_link(&self) -> Result<(), BinstallError> {
// Remove existing symlink // Remove existing symlink
// TODO: check if existing symlink is correct // TODO: check if existing symlink is correct
if self.link.exists() { if self.link.exists() {

View file

@ -1,25 +1,26 @@
use std::collections::BTreeSet;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Context}; use crates_io_api::AsyncClient;
use log::debug; use log::debug;
use semver::{Version, VersionReq}; use semver::{Version, VersionReq};
use crates_io_api::AsyncClient; use crate::{helpers::*, BinstallError, PkgFmt};
use crate::helpers::*;
use crate::PkgFmt;
fn find_version<'a, V: Iterator<Item = &'a String>>( fn find_version<'a, V: Iterator<Item = &'a String>>(
requirement: &str, requirement: &str,
version_iter: V, version_iter: V,
) -> Result<String, anyhow::Error> { ) -> Result<Version, BinstallError> {
// Parse version requirement // 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 // Filter for matching versions
let mut filtered: Vec<_> = version_iter let filtered: BTreeSet<_> = version_iter
.filter(|v| { .filter_map(|v| {
// Remove leading `v` for git tags // Remove leading `v` for git tags
let ver_str = match v.strip_prefix("s") { let ver_str = match v.strip_prefix("s") {
Some(v) => v, Some(v) => v,
@ -27,36 +28,26 @@ fn find_version<'a, V: Iterator<Item = &'a String>>(
}; };
// Parse out version // Parse out version
let ver = match Version::parse(ver_str) { let ver = Version::parse(ver_str).ok()?;
Ok(sv) => sv,
Err(_) => return false,
};
debug!("Version: {:?}", ver); debug!("Version: {:?}", ver);
// Filter by version match // Filter by version match
version_req.matches(&ver) if version_req.matches(&ver) {
Some(ver)
} else {
None
}
}) })
.collect(); .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); debug!("Filtered: {:?}", filtered);
// Return highest version // Return highest version
match filtered.get(0) { filtered
Some(v) => Ok(v.to_string()), .iter()
None => Err(anyhow!( .max()
"No matching version for requirement: '{}'", .cloned()
version_req .ok_or_else(|| BinstallError::VersionMismatch { req: version_req })
)),
}
} }
/// Fetch a crate by name and version from crates.io /// Fetch a crate by name and version from crates.io
@ -64,7 +55,7 @@ pub async fn fetch_crate_cratesio(
name: &str, name: &str,
version_req: &str, version_req: &str,
temp_dir: &Path, temp_dir: &Path,
) -> Result<PathBuf, anyhow::Error> { ) -> Result<PathBuf, BinstallError> {
// Fetch / update index // Fetch / update index
debug!("Looking up crate information"); debug!("Looking up crate information");
@ -72,23 +63,18 @@ pub async fn fetch_crate_cratesio(
let api_client = AsyncClient::new( let api_client = AsyncClient::new(
"cargo-binstall (https://github.com/ryankurte/cargo-binstall)", "cargo-binstall (https://github.com/ryankurte/cargo-binstall)",
Duration::from_millis(100), Duration::from_millis(100),
)?; )
.expect("bug: invalid user agent");
// Fetch online crate information // Fetch online crate information
let crate_info = api_client let base_info =
.get_crate(name.as_ref()) api_client
.await .get_crate(name.as_ref())
.context("Error fetching crate information"); .await
.map_err(|err| BinstallError::CratesIoApi {
let base_info = match crate_info { crate_name: name.into(),
Ok(i) => i, err,
Err(_) => { })?;
return Err(anyhow::anyhow!(
"Error fetching information for crate {}",
name
));
}
};
// Locate matching version // Locate matching version
let version_iter = let version_iter =
@ -99,16 +85,14 @@ pub async fn fetch_crate_cratesio(
let version_name = find_version(version_req, version_iter)?; let version_name = find_version(version_req, version_iter)?;
// Fetch information for the filtered version // Fetch information for the filtered version
let version = match base_info.versions.iter().find(|v| v.num == version_name) { let version = base_info
Some(v) => v, .versions
None => { .iter()
return Err(anyhow::anyhow!( .find(|v| v.num == version_name.to_string())
"No information found for crate: '{}' version: '{}'", .ok_or_else(|| BinstallError::VersionUnavailable {
name, crate_name: name.into(),
version_name v: version_name.clone(),
)); })?;
}
};
debug!("Found information for crate version: '{}'", version.num); debug!("Found information for crate version: '{}'", version.num);
@ -136,6 +120,6 @@ pub async fn fetch_crate_gh_releases(
_name: &str, _name: &str,
_version: Option<&str>, _version: Option<&str>,
_temp_dir: &Path, _temp_dir: &Path,
) -> Result<PathBuf, anyhow::Error> { ) -> Result<PathBuf, BinstallError> {
unimplemented!(); unimplemented!();
} }

77
src/errors.rs Normal file
View file

@ -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,
},
}

View file

@ -4,7 +4,7 @@ pub use gh_crate_meta::*;
pub use log::debug; pub use log::debug;
pub use quickinstall::*; pub use quickinstall::*;
use crate::{PkgFmt, PkgMeta}; use crate::{BinstallError, PkgFmt, PkgMeta};
mod gh_crate_meta; mod gh_crate_meta;
mod quickinstall; mod quickinstall;
@ -17,10 +17,10 @@ pub trait Fetcher {
Self: Sized; Self: Sized;
/// Fetch a package /// 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 /// Check if a package is available for download
async fn check(&self) -> Result<bool, anyhow::Error>; async fn check(&self) -> Result<bool, BinstallError>;
/// Return the package format /// Return the package format
fn pkg_fmt(&self) -> PkgFmt; fn pkg_fmt(&self) -> PkgFmt;

View file

@ -6,14 +6,14 @@ use serde::Serialize;
use url::Url; use url::Url;
use super::Data; use super::Data;
use crate::{download, remote_exists, PkgFmt, Template}; use crate::{download, remote_exists, BinstallError, PkgFmt, Template};
pub struct GhCrateMeta { pub struct GhCrateMeta {
data: Data, data: Data,
} }
impl GhCrateMeta { impl GhCrateMeta {
fn url(&self) -> Result<Url, anyhow::Error> { fn url(&self) -> Result<Url, BinstallError> {
let ctx = Context::from_data(&self.data); let ctx = Context::from_data(&self.data);
debug!("Using context: {:?}", ctx); debug!("Using context: {:?}", ctx);
Ok(ctx.render_url(&self.data.meta.pkg_url)?) Ok(ctx.render_url(&self.data.meta.pkg_url)?)
@ -26,13 +26,13 @@ impl super::Fetcher for GhCrateMeta {
Box::new(Self { data: data.clone() }) Box::new(Self { data: data.clone() })
} }
async fn check(&self) -> Result<bool, anyhow::Error> { async fn check(&self) -> Result<bool, BinstallError> {
let url = self.url()?; let url = self.url()?;
info!("Checking for package at: '{url}'"); info!("Checking for package at: '{url}'");
remote_exists(url.as_str(), Method::HEAD).await 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()?; let url = self.url()?;
info!("Downloading package from: '{url}'"); info!("Downloading package from: '{url}'");
download(url.as_str(), dst).await download(url.as_str(), dst).await
@ -101,7 +101,7 @@ impl<'c> Context<'c> {
} }
} }
pub(self) fn render_url(&self, template: &str) -> Result<Url, anyhow::Error> { pub(self) fn render_url(&self, template: &str) -> Result<Url, BinstallError> {
Ok(Url::parse(&self.render(template)?)?) Ok(Url::parse(&self.render(template)?)?)
} }
} }

View file

@ -2,9 +2,10 @@ use std::path::Path;
use log::info; use log::info;
use reqwest::Method; use reqwest::Method;
use url::Url;
use super::Data; 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 BASE_URL: &str = "https://github.com/alsuren/cargo-quickinstall/releases/download";
const STATS_URL: &str = "https://warehouse-clerk-tmp.vercel.app/api/crate"; 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<bool, anyhow::Error> { async fn check(&self) -> Result<bool, BinstallError> {
let url = self.package_url(); let url = self.package_url();
self.report().await?; self.report().await?;
info!("Checking for package at: '{url}'"); info!("Checking for package at: '{url}'");
remote_exists(&url, Method::HEAD).await 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(); let url = self.package_url();
info!("Downloading package from: '{url}'"); info!("Downloading package from: '{url}'");
download(&url, &dst).await 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)"); info!("Sending installation report to quickinstall (anonymous)");
let url = Url::parse(&self.stats_url())?;
reqwest::Client::builder() reqwest::Client::builder()
.user_agent(USER_AGENT) .user_agent(USER_AGENT)
.build()? .build()?
.request(Method::HEAD, &self.stats_url()) .request(Method::HEAD, url.clone())
.send() .send()
.await?; .await
.map_err(|err| BinstallError::Http {
method: Method::HEAD,
url,
err,
})?;
Ok(()) Ok(())
} }
} }

View file

@ -1,24 +1,24 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use log::{debug, error, info}; use log::{debug, info};
use cargo_toml::Manifest; use cargo_toml::Manifest;
use flate2::read::GzDecoder; use flate2::read::GzDecoder;
use reqwest::Method;
use serde::Serialize; use serde::Serialize;
use tar::Archive; use tar::Archive;
use tinytemplate::TinyTemplate; use tinytemplate::TinyTemplate;
use url::Url;
use xz2::read::XzDecoder; use xz2::read::XzDecoder;
use zip::read::ZipArchive; use zip::read::ZipArchive;
use zstd::stream::Decoder as ZstdDecoder; use zstd::stream::Decoder as ZstdDecoder;
use crate::Meta; use crate::{BinstallError, Meta, PkgFmt};
use super::PkgFmt;
/// Load binstall metadata from the crate `Cargo.toml` at the provided path /// Load binstall metadata from the crate `Cargo.toml` at the provided path
pub fn load_manifest_path<P: AsRef<Path>>( pub fn load_manifest_path<P: AsRef<Path>>(
manifest_path: P, manifest_path: P,
) -> Result<Manifest<Meta>, anyhow::Error> { ) -> Result<Manifest<Meta>, BinstallError> {
debug!("Reading manifest: {}", manifest_path.as_ref().display()); debug!("Reading manifest: {}", manifest_path.as_ref().display());
// Load and parse manifest (this checks file system for binary output names) // Load and parse manifest (this checks file system for binary output names)
@ -28,21 +28,24 @@ pub fn load_manifest_path<P: AsRef<Path>>(
Ok(manifest) Ok(manifest)
} }
pub async fn remote_exists(url: &str, method: reqwest::Method) -> Result<bool, anyhow::Error> { pub async fn remote_exists(url: &str, method: reqwest::Method) -> Result<bool, BinstallError> {
let req = reqwest::Client::new().request(method, url).send().await?; let req = reqwest::Client::new().request(method, url).send().await?;
Ok(req.status().is_success()) Ok(req.status().is_success())
} }
/// Download a file from the provided URL to the provided path /// Download a file from the provided URL to the provided path
pub async fn download<P: AsRef<Path>>(url: &str, path: P) -> Result<(), anyhow::Error> { pub async fn download<P: AsRef<Path>>(url: &str, path: P) -> Result<(), BinstallError> {
let url = Url::parse(url)?;
debug!("Downloading from: '{}'", url); debug!("Downloading from: '{}'", url);
let resp = reqwest::get(url).await?; let resp = reqwest::get(url.clone())
.await
if !resp.status().is_success() { .and_then(|r| r.error_for_status())
error!("Download error: {}", resp.status()); .map_err(|err| BinstallError::Http {
return Err(anyhow::anyhow!(resp.status())); method: Method::GET,
} url,
err,
})?;
let bytes = resp.bytes().await?; let bytes = resp.bytes().await?;
@ -60,7 +63,7 @@ pub fn extract<S: AsRef<Path>, P: AsRef<Path>>(
source: S, source: S,
fmt: PkgFmt, fmt: PkgFmt,
path: P, path: P,
) -> Result<(), anyhow::Error> { ) -> Result<(), BinstallError> {
match fmt { match fmt {
PkgFmt::Tar => { PkgFmt::Tar => {
// Extract to install dir // Extract to install dir
@ -188,23 +191,23 @@ pub fn get_install_path<P: AsRef<Path>>(install_path: Option<P>) -> Option<PathB
None None
} }
pub fn confirm() -> Result<bool, anyhow::Error> { pub fn confirm() -> Result<(), BinstallError> {
info!("Do you wish to continue? yes/no"); loop {
info!("Do you wish to continue? yes/no");
let mut input = String::new(); let mut input = String::new();
std::io::stdin().read_line(&mut input)?; std::io::stdin().read_line(&mut input).unwrap();
match input.as_str().trim() { match input.as_str().trim() {
"yes" => Ok(true), "yes" | "y" | "YES" | "Y" => break Ok(()),
"no" => Ok(false), "no" | "n" | "NO" | "N" => break Err(BinstallError::UserAbort),
_ => Err(anyhow::anyhow!( _ => continue,
"Valid options are 'yes', 'no', please try again" }
)),
} }
} }
pub trait Template: Serialize { pub trait Template: Serialize {
fn render(&self, template: &str) -> Result<String, anyhow::Error> fn render(&self, template: &str) -> Result<String, BinstallError>
where where
Self: Sized, Self: Sized,
{ {

View file

@ -3,11 +3,14 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum_macros::{Display, EnumString, EnumVariantNames}; use strum_macros::{Display, EnumString, EnumVariantNames};
pub mod drivers;
pub use drivers::*;
pub mod helpers; pub mod helpers;
pub use helpers::*; pub use helpers::*;
pub mod drivers; mod errors;
pub use drivers::*; pub use errors::BinstallError;
pub mod bins; pub mod bins;
pub mod fetchers; pub mod fetchers;

View file

@ -1,11 +1,12 @@
use std::{path::PathBuf, str::FromStr}; use std::{path::PathBuf, str::FromStr, time::Instant};
use cargo_toml::{Package, Product}; use cargo_toml::{Package, Product};
use log::{debug, error, info, warn, LevelFilter}; use log::{debug, error, info, warn, LevelFilter};
use miette::{miette, IntoDiagnostic, Result, WrapErr};
use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode}; use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode};
use structopt::StructOpt; use structopt::StructOpt;
use tempfile::TempDir; use tempfile::TempDir;
use tokio::process::Command; use tokio::{process::Command, runtime::Runtime};
use cargo_binstall::{ use cargo_binstall::{
bins, bins,
@ -74,8 +75,31 @@ struct Options {
pkg_url: Option<String>, pkg_url: Option<String>,
} }
#[tokio::main] fn main() -> Result<()> {
async fn main() -> Result<(), anyhow::Error> { 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::<BinstallError>() {
warn!("Installation cancelled");
Ok(())
} else {
Err(err)
}
} else {
info!("Installation complete! [{done:?}]");
Ok(())
}
}
async fn entry() -> Result<()> {
// Filter extraneous arg when invoked by cargo // Filter extraneous arg when invoked by cargo
// `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"] // `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"]
// `cargo binstall --help` gives ["/home/ryan/.cargo/bin/cargo-binstall", "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(); .unwrap();
// Create a temporary directory for downloads etc. // 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); info!("Installing package: '{}'", opts.name);
@ -129,9 +155,8 @@ async fn main() -> Result<(), anyhow::Error> {
o=opts.version, p=package.version o=opts.version, p=package.version
); );
if opts.no_confirm || opts.dry_run || !confirm()? { if !opts.no_confirm && !opts.dry_run {
warn!("Installation cancelled"); confirm()?;
return Ok(());
} }
} }
@ -156,7 +181,7 @@ async fn main() -> Result<(), anyhow::Error> {
// Compute install directory // Compute install directory
let install_path = get_install_path(opts.install_path.as_deref()).ok_or_else(|| { 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`"); 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()); debug!("Using install path: {}", install_path.display());
@ -211,16 +236,15 @@ async fn install_from_package(
package: Package<Meta>, package: Package<Meta>,
pkg_path: PathBuf, pkg_path: PathBuf,
temp_dir: TempDir, temp_dir: TempDir,
) -> Result<(), anyhow::Error> { ) -> Result<()> {
// Prompt user for third-party source // Prompt user for third-party source
if fetcher.is_third_party() { if fetcher.is_third_party() {
warn!( warn!(
"The package will be downloaded from third-party source {}", "The package will be downloaded from third-party source {}",
fetcher.source_name() fetcher.source_name()
); );
if !opts.no_confirm && !opts.dry_run && !confirm()? { if !opts.no_confirm && !opts.dry_run {
warn!("Installation cancelled"); confirm()?;
return Ok(());
} }
} else { } else {
info!( info!(
@ -274,7 +298,7 @@ async fn install_from_package(
if binaries.is_empty() { if binaries.is_empty() {
error!("No binaries specified (or inferred from file system)"); error!("No binaries specified (or inferred from file system)");
return Err(anyhow::anyhow!( return Err(miette!(
"No binaries specified (or inferred from file system)" "No binaries specified (or inferred from file system)"
)); ));
} }
@ -295,7 +319,7 @@ async fn install_from_package(
let bin_files = binaries let bin_files = binaries
.iter() .iter()
.map(|p| bins::BinFile::from_product(&bin_data, p)) .map(|p| bins::BinFile::from_product(&bin_data, p))
.collect::<Result<Vec<_>, anyhow::Error>>()?; .collect::<Result<Vec<_>, BinstallError>>()?;
// Prompt user for confirmation // Prompt user for confirmation
info!("This will install the following binaries:"); info!("This will install the following binaries:");
@ -315,9 +339,8 @@ async fn install_from_package(
return Ok(()); return Ok(());
} }
if !opts.no_confirm && !confirm()? { if !opts.no_confirm {
warn!("Installation cancelled"); confirm()?;
return Ok(());
} }
info!("Installing binaries..."); info!("Installing binaries...");
@ -332,8 +355,6 @@ async fn install_from_package(
} }
} }
info!("Installation complete!");
if opts.no_cleanup { if opts.no_cleanup {
let _ = temp_dir.into_path(); let _ = temp_dir.into_path();
} else { } else {
@ -345,12 +366,11 @@ async fn install_from_package(
Ok(()) Ok(())
} }
async fn install_from_source(opts: Options, package: Package<Meta>) -> Result<(), anyhow::Error> { async fn install_from_source(opts: Options, package: Package<Meta>) -> Result<()> {
// Prompt user for source install // Prompt user for source install
warn!("The package will be installed from source (with cargo)",); warn!("The package will be installed from source (with cargo)",);
if !opts.no_confirm && !opts.dry_run && !confirm()? { if !opts.no_confirm && !opts.dry_run {
warn!("Installation cancelled"); confirm()?;
return Ok(());
} }
if opts.dry_run { if opts.dry_run {
@ -371,16 +391,22 @@ async fn install_from_source(opts: Options, package: Package<Meta>) -> Result<()
.arg(package.version) .arg(package.version)
.arg("--target") .arg("--target")
.arg(opts.target) .arg(opts.target)
.spawn()?; .spawn()
.into_diagnostic()
.wrap_err("Spawning cargo install failed.")?;
debug!("Spawned command pid={:?}", child.id()); 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() { if status.success() {
info!("Cargo finished successfully"); info!("Cargo finished successfully");
Ok(()) Ok(())
} else { } else {
error!("Cargo errored! {:?}", status); error!("Cargo errored! {:?}", status);
Err(anyhow::anyhow!("Cargo install error")) Err(miette!("Cargo install error"))
} }
} }
} }