diff --git a/README.md b/README.md index e0378c81..cfa6930c 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,15 @@ `binstall` works by fetching the crate information from `crates.io`, then searching the linked `repository` for matching releases and artifacts, with fallbacks to [quickinstall](https://github.com/alsuren/cargo-quickinstall) and finally `cargo install` if these are not found. To support `binstall` maintainers must add configuration values to `Cargo.toml` to allow the tool to locate the appropriate binary package for a given version and target. See [SUPPORT.md](./SUPPORT.md) for more detail. - ## Status -![Build](https://github.com/ryankurte/cargo-binstall/workflows/Rust/badge.svg) -[![GitHub tag](https://img.shields.io/github/tag/ryankurte/cargo-binstall.svg)](https://github.com/ryankurte/cargo-binstall) +![Build](https://github.com/cargo-bins/cargo-binstall/workflows/Rust/badge.svg) +[![GitHub tag](https://img.shields.io/github/tag/cargo-bins/cargo-binstall.svg)](https://github.com/cargo-bins/cargo-binstall) [![Crates.io](https://img.shields.io/crates/v/cargo-binstall.svg)](https://crates.io/crates/cargo-binstall) -[![Docs.rs](https://docs.rs/cargo-binstall/badge.svg)](https://docs.rs/cargo-binstall) ## Installation -To get started _using_ `cargo-binstall` first install the binary (either via `cargo install cargo-binstall` or by downloading a pre-compiled [release](https://github.com/ryankurte/cargo-binstall/releases)). - +To get started _using_ `cargo-binstall` first install the binary (either via `cargo install cargo-binstall` or by downloading a pre-compiled [release](https://github.com/cargo-bins/cargo-binstall/releases)). | OS | Arch | URL | | ------- | ------- | ------------------------------------------------------------ | @@ -27,7 +24,7 @@ To get started _using_ `cargo-binstall` first install the binary (either via `ca | macos | m1 | https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-aarch64-apple-darwin.zip | | windows | x86\_64 | https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-pc-windows-msvc.zip | - +To upgrade, use `cargo binstall cargo-binstall`! ## Usage @@ -37,16 +34,15 @@ Package versions and targets may be specified using the `--version` and `--targe ``` [garry] ➜ ~ cargo binstall radio-sx128x --version 0.14.1-alpha.5 -21:14:09 [INFO] Installing package: 'radio-sx128x' -21:14:13 [INFO] Downloading package from: 'https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/sx128x-util-x86_64-apple-darwin.tgz' +21:14:15 [INFO] Resolving package: 'radio-sx128x' 21:14:18 [INFO] This will install the following binaries: 21:14:18 [INFO] - sx128x-util (sx128x-util-x86_64-apple-darwin -> /Users/ryankurte/.cargo/bin/sx128x-util-v0.14.1-alpha.5) 21:14:18 [INFO] And create (or update) the following symlinks: 21:14:18 [INFO] - sx128x-util (/Users/ryankurte/.cargo/bin/sx128x-util-v0.14.1-alpha.5 -> /Users/ryankurte/.cargo/bin/sx128x-util) -21:14:18 [INFO] Do you wish to continue? yes/no -yes -21:15:30 [INFO] Installing binaries... -21:15:30 [INFO] Installation complete! +21:14:18 [INFO] Do you wish to continue? yes/[no] +? yes +21:14:20 [INFO] Installing binaries... +21:14:21 [INFO] Done in 6.212736s ``` ### Unsupported crates @@ -60,7 +56,6 @@ $ binstall \ --pkg-fmt="txz" crate_name ``` - ## FAQ - Why use this? diff --git a/ci-scripts/tests.sh b/ci-scripts/tests.sh index e38259b6..29468795 100755 --- a/ci-scripts/tests.sh +++ b/ci-scripts/tests.sh @@ -48,11 +48,12 @@ cargo binstall --help >/dev/null cargo binstall --help >/dev/null # Test skip when installed -"./$1" binstall --no-confirm cargo-binstall | grep -q 'package cargo-binstall is already installed' -"./$1" binstall --no-confirm cargo-binstall@0.11.1 | grep -q 'package cargo-binstall@=0.11.1 is already installed' +"./$1" binstall --no-confirm --force cargo-binstall@0.11.1 +"./$1" binstall --no-confirm cargo-binstall@0.11.1 | grep -q 'cargo-binstall v0.11.1 is already installed' -"./$1" binstall --no-confirm cargo-binstall@0.10.0 | grep -q -v 'package cargo-binstall@=0.10.0 is already installed' +"./$1" binstall --no-confirm cargo-binstall@0.10.0 | grep -q -v 'cargo-binstall v0.10.0 is already installed' ## Test When 0.11.0 is installed but can be upgraded. -"./$1" binstall --force --log-level debug --no-confirm cargo-binstall@0.11.0 -"./$1" binstall --no-confirm cargo-binstall@^0.11.0 | grep -q -v 'package cargo-binstall@^0.11.0 is already installed' +"./$1" binstall --no-confirm cargo-binstall@0.11.0 +"./$1" binstall --no-confirm cargo-binstall@0.11.0 | grep -q 'cargo-binstall v0.11.0 is already installed' +"./$1" binstall --no-confirm cargo-binstall@^0.11.0 | grep -q -v 'cargo-binstall v0.11.0 is already installed' diff --git a/src/binstall/install.rs b/src/binstall/install.rs index 9e57d6c1..8e78b2e3 100644 --- a/src/binstall/install.rs +++ b/src/binstall/install.rs @@ -3,17 +3,16 @@ use std::{path::PathBuf, process, sync::Arc}; use cargo_toml::Package; use compact_str::CompactString; use log::{debug, error, info}; -use miette::{miette, IntoDiagnostic, Result, WrapErr}; use tokio::{process::Command, task::block_in_place}; use super::{MetaData, Options, Resolution}; -use crate::{bins, fetchers::Fetcher, metafiles::binstall_v1::Source, *}; +use crate::{bins, fetchers::Fetcher, metafiles::binstall_v1::Source, BinstallError, *}; pub async fn install( resolution: Resolution, opts: Arc, jobserver_client: LazyJobserverClient, -) -> Result> { +) -> Result, BinstallError> { match resolution { Resolution::AlreadyUpToDate => Ok(None), Resolution::Fetch { @@ -24,7 +23,14 @@ pub async fn install( bin_path, bin_files, } => { - let current_version = package.version.parse().into_diagnostic()?; + let current_version = + package + .version + .parse() + .map_err(|err| BinstallError::VersionParse { + v: package.version, + err, + })?; let target = fetcher.target().into(); install_from_package(fetcher, opts, bin_path, bin_files) @@ -45,7 +51,7 @@ pub async fn install( let desired_targets = opts.desired_targets.get().await; let target = desired_targets .first() - .ok_or_else(|| miette!("No viable targets found, try with `--targets`"))?; + .ok_or(BinstallError::NoViableTargets)?; if !opts.dry_run { install_from_source(package, target, jobserver_client, opts.quiet, opts.force) @@ -67,7 +73,7 @@ async fn install_from_package( opts: Arc, bin_path: PathBuf, bin_files: Vec, -) -> Result>> { +) -> Result>, BinstallError> { // Download package if opts.dry_run { info!("Dry run, not downloading package"); @@ -129,7 +135,7 @@ async fn install_from_source( lazy_jobserver_client: LazyJobserverClient, quiet: bool, force: bool, -) -> Result<()> { +) -> Result<(), BinstallError> { let jobserver_client = lazy_jobserver_client.get().await?; debug!( @@ -156,22 +162,20 @@ async fn install_from_source( cmd.arg("--force"); } - let mut child = cmd - .spawn() - .into_diagnostic() - .wrap_err("Spawning cargo install failed.")?; + let command_string = format!("{:?}", cmd); + + let mut child = cmd.spawn()?; debug!("Spawned command pid={:?}", child.id()); - let status = child - .wait() - .await - .into_diagnostic() - .wrap_err("Running cargo install failed.")?; + let status = child.wait().await?; if status.success() { info!("Cargo finished successfully"); Ok(()) } else { error!("Cargo errored! {status:?}"); - Err(miette!("Cargo install error")) + Err(BinstallError::SubProcess { + command: command_string, + status, + }) } } diff --git a/src/binstall/resolve.rs b/src/binstall/resolve.rs index 0869106a..97cc6cec 100644 --- a/src/binstall/resolve.rs +++ b/src/binstall/resolve.rs @@ -5,8 +5,7 @@ use std::{ use cargo_toml::{Package, Product}; use compact_str::{CompactString, ToCompactString}; -use log::{debug, error, info, warn}; -use miette::{miette, Result}; +use log::{debug, info, warn}; use reqwest::Client; use semver::{Version, VersionReq}; @@ -14,7 +13,7 @@ use super::Options; use crate::{ bins, fetchers::{Data, Fetcher, GhCrateMeta, MultiFetcher, QuickInstall}, - *, + BinstallError, *, }; pub enum Resolution { @@ -84,8 +83,31 @@ pub async fn resolve( install_path: Arc, client: Client, crates_io_api_client: crates_io_api::AsyncClient, -) -> Result { - info!("Installing package: '{}'", crate_name); +) -> Result { + let crate_name_name = crate_name.name.clone(); + resolve_inner( + opts, + crate_name, + curr_version, + temp_dir, + install_path, + client, + crates_io_api_client, + ) + .await + .map_err(|err| err.crate_context(crate_name_name)) +} + +async fn resolve_inner( + opts: Arc, + crate_name: CrateName, + curr_version: Option, + temp_dir: Arc, + install_path: Arc, + client: Client, + crates_io_api_client: crates_io_api::AsyncClient, +) -> Result { + info!("Resolving package: '{}'", crate_name); let version_req: VersionReq = match (&crate_name.version_req, &opts.version_req) { (Some(version), None) => version.clone(), @@ -120,7 +142,10 @@ pub async fn resolve( })?; if new_version == curr_version { - info!("package {crate_name} is already up to date {curr_version}"); + info!( + "{} v{curr_version} is already installed, use --force to override", + crate_name.name + ); return Ok(Resolution::AlreadyUpToDate); } } @@ -208,7 +233,7 @@ fn collect_bin_files( binaries: Vec, bin_path: PathBuf, install_path: PathBuf, -) -> Result> { +) -> Result, BinstallError> { // Update meta if fetcher.source_name() == "QuickInstall" { // TODO: less of a hack? @@ -217,10 +242,7 @@ fn collect_bin_files( // Check binaries if binaries.is_empty() { - error!("No binaries specified (or inferred from file system)"); - return Err(miette!( - "No binaries specified (or inferred from file system)" - )); + return Err(BinstallError::UnspecifiedBinaries); } // List files to be installed diff --git a/src/errors.rs b/src/errors.rs index c1d1de22..8d336dd4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,13 +1,13 @@ -use std::process::{ExitCode, Termination}; +use std::process::{ExitCode, ExitStatus, Termination}; +use compact_str::CompactString; use log::{error, warn}; use miette::{Diagnostic, Report}; use thiserror::Error; use tokio::task; -/// Errors emitted by cargo-binstall. +/// Error kinds emitted by cargo-binstall. #[derive(Error, Diagnostic, Debug)] -#[diagnostic(url(docsrs))] #[non_exhaustive] pub enum BinstallError { /// Internal: a task could not be joined. @@ -71,7 +71,7 @@ pub enum BinstallError { /// /// - Code: `binstall::http` /// - Exit: 69 - #[error("could not {method} {url}: {err}")] + #[error("could not {method} {url}")] #[diagnostic(severity(error), code(binstall::http))] Http { method: reqwest::Method, @@ -80,6 +80,16 @@ pub enum BinstallError { err: reqwest::Error, }, + /// A subprocess failed. + /// + /// This is often about cargo-install calls. + /// + /// - Code: `binstall::subprocess` + /// - Exit: 70 + #[error("subprocess {command} errored with {status}")] + #[diagnostic(severity(error), code(binstall::subprocess))] + SubProcess { command: String, status: ExitStatus }, + /// A generic I/O error. /// /// - Code: `binstall::io` @@ -94,7 +104,7 @@ pub enum BinstallError { /// /// - Code: `binstall::crates_io_api` /// - Exit: 76 - #[error("crates.io api error fetching crate information for '{crate_name}': {err}")] + #[error("crates.io API error")] #[diagnostic( severity(error), code(binstall::crates_io_api), @@ -137,7 +147,7 @@ pub enum BinstallError { /// /// - Code: `binstall::version::parse` /// - Exit: 80 - #[error("version string '{v}' is not semver: {err}")] + #[error("version string '{v}' is not semver")] #[diagnostic(severity(error), code(binstall::version::parse))] VersionParse { v: String, @@ -154,7 +164,7 @@ pub enum BinstallError { /// /// - Code: `binstall::version::requirement` /// - Exit: 81 - #[error("version requirement '{req}' is not semver: {err}")] + #[error("version requirement '{req}' is not semver")] #[diagnostic(severity(error), code(binstall::version::requirement))] VersionReq { req: String, @@ -220,17 +230,52 @@ pub enum BinstallError { help("You cannot use --{option} and specify multiple packages at the same time. Do one or the other.") )] OverrideOptionUsedWithMultiInstall { option: &'static str }, + + /// No binaries were found for the crate. + /// + /// When installing, either the binaries are specified in the crate's Cargo.toml, or they're + /// inferred from the crate layout (e.g. src/main.rs or src/bins/name.rs). If no binaries are + /// found through these methods, we can't know what to install! + /// + /// - Code: `binstall::resolve::binaries` + /// - Exit: 86 + #[error("no binaries specified nor inferred")] + #[diagnostic( + severity(error), + code(binstall::resolve::binaries), + help("This crate doesn't specify any binaries, so there's nothing to install.") + )] + UnspecifiedBinaries, + + /// No viable targets were found. + /// + /// When installing, we attempt to find which targets the host (your computer) supports, and + /// discover builds for these targets from the remote binary source. This error occurs when we + /// fail to discover the host's target. + /// + /// You should in this case specify --target manually. + /// + /// - Code: `binstall::targets::none_host` + /// - Exit: 87 + #[error("failed to discovered a viable target from the host")] + #[diagnostic( + severity(error), + code(binstall::targets::none_host), + help("Try to specify --target") + )] + NoViableTargets, + + /// A wrapped error providing the context of which crate the error is about. + #[error("for crate {crate_name}")] + CrateContext { + #[source] + error: Box, + crate_name: CompactString, + }, } 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) -> ExitCode { + fn exit_number(&self) -> u8 { use BinstallError::*; let code: u8 = match self { TaskJoinError(_) => 17, @@ -240,6 +285,7 @@ impl BinstallError { Template(_) => 67, Reqwest(_) => 68, Http { .. } => 69, + SubProcess { .. } => 70, Io(_) => 74, CratesIoApi { .. } => 76, CargoManifestPath => 77, @@ -250,12 +296,34 @@ impl BinstallError { VersionUnavailable { .. } => 83, SuperfluousVersionOption => 84, OverrideOptionUsedWithMultiInstall { .. } => 85, + UnspecifiedBinaries => 86, + NoViableTargets => 87, + CrateContext { error, .. } => error.exit_number(), }; // reserved codes debug_assert!(code != 64 && code != 16 && code != 1 && code != 2 && code != 0); - code.into() + code + } + + /// 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) -> ExitCode { + self.exit_number().into() + } + + /// Add crate context to the error + pub fn crate_context(self, crate_name: impl Into) -> Self { + Self::CrateContext { + error: Box::new(self), + crate_name: crate_name.into(), + } } } diff --git a/src/fetchers/gh_crate_meta.rs b/src/fetchers/gh_crate_meta.rs index 43603760..2c009ff8 100644 --- a/src/fetchers/gh_crate_meta.rs +++ b/src/fetchers/gh_crate_meta.rs @@ -2,7 +2,7 @@ use std::path::Path; use std::sync::Arc; use compact_str::{CompactString, ToCompactString}; -use log::{debug, info, warn}; +use log::{debug, warn}; use once_cell::sync::OnceCell; use reqwest::Client; use reqwest::Method; @@ -43,7 +43,7 @@ impl super::Fetcher for GhCrateMeta { let client = self.client.clone(); AutoAbortJoinHandle::spawn(async move { let url = url?; - info!("Checking for package at: '{url}'"); + debug!("Checking for package at: '{url}'"); remote_exists(client, url.clone(), Method::HEAD) .await .map(|exists| (url.clone(), exists)) @@ -61,7 +61,7 @@ impl super::Fetcher for GhCrateMeta { ); } - info!("Winning URL is {url}"); + debug!("Winning URL is {url}"); self.url.set(url).unwrap(); // find() is called first return Ok(true); } @@ -72,7 +72,7 @@ impl super::Fetcher for GhCrateMeta { async fn fetch_and_extract(&self, dst: &Path) -> Result<(), BinstallError> { let url = self.url.get().unwrap(); // find() is called first - info!("Downloading package from: '{url}'"); + debug!("Downloading package from: '{url}'"); download_and_extract(&self.client, url, self.pkg_fmt(), dst).await } diff --git a/src/fetchers/quickinstall.rs b/src/fetchers/quickinstall.rs index f79b9bc1..0bdaa290 100644 --- a/src/fetchers/quickinstall.rs +++ b/src/fetchers/quickinstall.rs @@ -2,7 +2,7 @@ use std::path::Path; use std::sync::Arc; use compact_str::CompactString; -use log::{debug, info}; +use log::debug; use reqwest::Client; use reqwest::Method; use tokio::task::JoinHandle; @@ -36,13 +36,13 @@ impl super::Fetcher for QuickInstall { async fn find(&self) -> Result { let url = self.package_url(); self.report(); - info!("Checking for package at: '{url}'"); + debug!("Checking for package at: '{url}'"); remote_exists(self.client.clone(), Url::parse(&url)?, Method::HEAD).await } async fn fetch_and_extract(&self, dst: &Path) -> Result<(), BinstallError> { let url = self.package_url(); - info!("Downloading package from: '{url}'"); + debug!("Downloading package from: '{url}'"); download_and_extract(&self.client, &Url::parse(&url)?, self.pkg_fmt(), dst).await } diff --git a/src/main.rs b/src/main.rs index 55628a22..77b51ddd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,11 @@ struct Options { /// /// If duplicate names are provided, the last one (and their version requirement) /// is kept. - #[clap(help_heading = "Package selection", value_name = "crate[@version]")] + #[clap( + help_heading = "Package selection", + value_name = "crate[@version]", + required_unless_present_any = ["version", "help"], + )] crate_names: Vec, /// Package version to install. @@ -202,7 +206,7 @@ impl Termination for MainExit { fn report(self) -> ExitCode { match self { Self::Success(spent) => { - info!("Installation completed in {spent:?}"); + info!("Done in {spent:?}"); ExitCode::SUCCESS } Self::Error(err) => err.report(), @@ -354,31 +358,39 @@ async fn entry(jobserver_client: LazyJobserverClient) -> Result<()> { })?; // Remove installed crates - let crate_names = CrateName::dedup(crate_names).filter_map(|crate_name| { - if opts.force { - Some((crate_name, None)) - } else if let Some(records) = &metadata { - if let Some(metadata) = records.get(&crate_name.name) { - if let Some(version_req) = &crate_name.version_req { - if version_req.is_latest_compatible(&metadata.current_version) { - info!( - "package {crate_name} is already installed and cannot be upgraded, use --force to override" - ); - None - } else { - Some((crate_name, Some(metadata.current_version.clone()))) - } - } else { - info!("package {crate_name} is already installed, use --force to override"); + let crate_names = CrateName::dedup(crate_names) + .filter_map(|crate_name| { + match ( + opts.force, + metadata.as_ref().and_then(|records| records.get(&crate_name.name)), + &crate_name.version_req, + ) { + (false, Some(metadata), Some(version_req)) + if version_req.is_latest_compatible(&metadata.current_version) => + { + debug!("Bailing out early because we can assume wanted is already installed from metafile"); + info!( + "{} v{} is already installed, use --force to override", + crate_name.name, metadata.current_version + ); None } - } else { - Some((crate_name, None)) + + // we have to assume that the version req could be *, + // and therefore a remote upgraded version could exist + (false, Some(metadata), _) => { + Some((crate_name, Some(metadata.current_version.clone()))) + } + + _ => Some((crate_name, None)), } - } else { - Some((crate_name, None)) - } - }); + }) + .collect::>(); + + if crate_names.is_empty() { + debug!("Nothing to do"); + return Ok(()); + } let temp_dir_path: Arc = Arc::from(temp_dir.path()); @@ -414,7 +426,15 @@ async fn entry(jobserver_client: LazyJobserverClient) -> Result<()> { // Confirm let mut resolutions = Vec::with_capacity(tasks.len()); for task in tasks { - resolutions.push(task.await??); + match task.await?? { + binstall::Resolution::AlreadyUpToDate => {} + res => resolutions.push(res), + } + } + + if resolutions.is_empty() { + debug!("Nothing to do"); + return Ok(()); } uithread.confirm().await?;