Don't prompt if there's nothing to do (#293)

Fixes #291
This commit is contained in:
Félix Saparelli 2022-08-09 21:09:21 +12:00 committed by GitHub
parent 4500e4af63
commit 763d4610e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 205 additions and 95 deletions

View file

@ -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?

View file

@ -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'

View file

@ -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<Options>,
jobserver_client: LazyJobserverClient,
) -> Result<Option<MetaData>> {
) -> Result<Option<MetaData>, 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<Options>,
bin_path: PathBuf,
bin_files: Vec<bins::BinFile>,
) -> Result<Option<Vec<CompactString>>> {
) -> Result<Option<Vec<CompactString>>, 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,
})
}
}

View file

@ -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<Path>,
client: Client,
crates_io_api_client: crates_io_api::AsyncClient,
) -> Result<Resolution> {
info!("Installing package: '{}'", crate_name);
) -> Result<Resolution, BinstallError> {
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<Options>,
crate_name: CrateName,
curr_version: Option<Version>,
temp_dir: Arc<Path>,
install_path: Arc<Path>,
client: Client,
crates_io_api_client: crates_io_api::AsyncClient,
) -> Result<Resolution, BinstallError> {
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<Product>,
bin_path: PathBuf,
install_path: PathBuf,
) -> Result<Vec<bins::BinFile>> {
) -> Result<Vec<bins::BinFile>, 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

View file

@ -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<BinstallError>,
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<CompactString>) -> Self {
Self::CrateContext {
error: Box::new(self),
crate_name: crate_name.into(),
}
}
}

View file

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

View file

@ -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<bool, BinstallError> {
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
}

View file

@ -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<CrateName>,
/// 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) {
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!(
"package {crate_name} is already installed and cannot be upgraded, use --force to override"
"{} v{} is already installed, use --force to override",
crate_name.name, metadata.current_version
);
None
} else {
}
// 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())))
}
} else {
info!("package {crate_name} is already installed, use --force to override");
None
_ => Some((crate_name, None)),
}
} else {
Some((crate_name, None))
})
.collect::<Vec<_>>();
if crate_names.is_empty() {
debug!("Nothing to do");
return Ok(());
}
} else {
Some((crate_name, None))
}
});
let temp_dir_path: Arc<Path> = 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?;