diff --git a/Cargo.lock b/Cargo.lock index 8c985cad1..11499219a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,7 +69,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "clap", + "clap 2.34.0", "env_logger", "lazy_static", "lazycell", @@ -144,6 +144,7 @@ dependencies = [ "async-trait", "cargo_metadata", "cargo_toml", + "clap 3.1.18", "crates_io_api", "dirs", "env_logger", @@ -155,7 +156,6 @@ dependencies = [ "semver", "serde", "simplelog", - "structopt", "strum", "strum_macros", "tar", @@ -257,12 +257,51 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", + "strsim 0.8.0", "textwrap 0.11.0", "unicode-width", "vec_map", ] +[[package]] +name = "clap" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "lazy_static", + "strsim 0.10.0", + "termcolor", + "textwrap 0.15.0", +] + +[[package]] +name = "clap_derive" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "cmake" version = "0.1.48" @@ -568,15 +607,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.0" @@ -922,6 +952,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" +[[package]] +name = "os_str_bytes" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" + [[package]] name = "owo-colors" version = "3.4.0" @@ -1261,28 +1297,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] -name = "structopt" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck 0.3.3", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" @@ -1296,7 +1314,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6878079b17446e4d3eba6192bb0a2950d5b14f0ed8424b852310e5a94345d0ef" dependencies = [ - "heck 0.4.0", + "heck", "proc-macro2", "quote", "rustversion", @@ -1595,12 +1613,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" - [[package]] name = "unicode-width" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 1a4cfe4df..9f1f5f1f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ pkg-fmt = "zip" async-trait = "0.1.56" cargo_metadata = "0.14.2" cargo_toml = "0.11.4" +clap = { version = "3.1.18", features = ["derive"] } crates_io_api = { version = "0.8.0", default-features = false, features = ["rustls"] } dirs = "4.0.0" flate2 = { version = "1.0.24", features = ["zlib-ng"], default-features = false } @@ -31,7 +32,6 @@ reqwest = { version = "0.11.10", features = [ "rustls-tls" ], default-features = semver = "1.0.7" serde = { version = "1.0.136", features = [ "derive" ] } simplelog = "0.12.0" -structopt = "0.3.26" strum = "0.24.0" strum_macros = "0.24.0" tar = "0.4.38" diff --git a/src/fetchers/quickinstall.rs b/src/fetchers/quickinstall.rs index e433194c1..ec83493cb 100644 --- a/src/fetchers/quickinstall.rs +++ b/src/fetchers/quickinstall.rs @@ -1,7 +1,7 @@ use std::path::Path; use std::sync::Arc; -use log::info; +use log::{debug, info}; use reqwest::Method; use url::Url; @@ -77,8 +77,14 @@ impl QuickInstall { } pub async fn report(&self) -> Result<(), BinstallError> { - info!("Sending installation report to quickinstall (anonymous)"); + if cfg!(debug_assertions) { + debug!("Not sending quickinstall report in debug mode"); + return Ok(()); + } + let url = Url::parse(&self.stats_url())?; + debug!("Sending installation report to quickinstall ({url})"); + reqwest::Client::builder() .user_agent(USER_AGENT) .build()? diff --git a/src/main.rs b/src/main.rs index 3d04fd3d5..256ff987b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,10 @@ use std::{ }; use cargo_toml::{Package, Product}; +use clap::Parser; 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, runtime::Runtime, task::JoinError}; @@ -20,64 +20,94 @@ use cargo_binstall::{ *, }; -#[derive(Debug, StructOpt)] +#[derive(Debug, Parser)] +#[clap(version, about = "Install a Rust binary... from binaries!")] struct Options { - /// Package name or URL for installation - /// This must be either a crates.io package name or github or gitlab url - #[structopt()] + /// Package name for installation. + /// + /// This must be a crates.io package name. + #[clap(value_name = "crate")] name: String, - /// Filter for package version to install, in Cargo.toml format. - /// Use `=1.2.3` to install a specific version. - #[structopt(long, default_value = "*")] + /// Semver filter to select the package version to install. + /// + /// This is in Cargo.toml dependencies format: `--version 1.2.3` is equivalent to + /// `--version "^1.2.3"`. Use `=1.2.3` to install a specific version. + #[clap(long, default_value = "*")] version: String, - /// Override binary target, ignoring compiled version - #[structopt(long, default_value = TARGET)] - target: String, + /// Override binary target set. + /// + /// Binstall is able to look for binaries for several targets, installing the first one it finds + /// in the order the targets were given. For example, on a 64-bit glibc Linux distribution, the + /// default is to look first for a `x86_64-unknown-linux-gnu` binary, then for a + /// `x86_64-unknown-linux-musl` binary. However, on a musl system, the gnu version will not be + /// considered. + /// + /// This option takes a comma-separated list of target triples, which will be tried in order. + /// They override the default list, which is detected automatically from the current platform. + /// + /// If falling back to installing from source, the first target will be used. + #[clap( + help_heading = "OVERRIDES", + alias = "target", + long, + value_name = "TRIPLE" + )] + targets: Option, /// Override install path for downloaded binary. + /// /// Defaults to `$HOME/.cargo/bin` - #[structopt(long)] + #[clap(help_heading = "OVERRIDES", long)] install_path: Option, - /// Disable symlinking / versioned updates - #[structopt(long)] + /// Disable symlinking / versioned updates. + /// + /// By default, Binstall will install a binary named `-` in the install path, and + /// either symlink or copy it to (depending on platform) the plain binary name. This makes it + /// possible to have multiple versions of the same binary, for example for testing or rollback. + /// + /// Pass this flag to disable this behavior. + #[clap(long)] no_symlinks: bool, - /// Dry run, fetch and show changes without installing binaries - #[structopt(long)] + /// Dry run, fetch and show changes without installing binaries. + #[clap(long)] dry_run: bool, - /// Disable interactive mode / confirmation - #[structopt(long)] + /// Disable interactive mode / confirmation prompts. + #[clap(long)] no_confirm: bool, - /// Do not cleanup temporary files on success - #[structopt(long)] + /// Do not cleanup temporary files. + #[clap(long)] no_cleanup: bool, /// Override manifest source. - /// This skips searching crates.io for a manifest and uses - /// the specified path directly, useful for debugging and - /// when adding `binstall` support. - #[structopt(long)] + /// + /// This skips searching crates.io for a manifest and uses the specified path directly, useful + /// for debugging and when adding Binstall support. This must be the path to the folder + /// containing a Cargo.toml file, not the Cargo.toml file itself. + #[clap(help_heading = "OVERRIDES", long)] manifest_path: Option, /// Utility log level - #[structopt(long, default_value = "info")] + /// + /// Set to `debug` when submitting a bug report. + #[clap(long, default_value = "info", value_name = "LEVEL")] log_level: LevelFilter, /// Override Cargo.toml package manifest bin-dir. - #[structopt(long)] + #[clap(help_heading = "OVERRIDES", long)] bin_dir: Option, /// Override Cargo.toml package manifest pkg-fmt. - #[structopt(long)] + #[clap(help_heading = "OVERRIDES", long)] pkg_fmt: Option, /// Override Cargo.toml package manifest pkg-url. - #[structopt(long)] + #[clap(help_heading = "OVERRIDES", long)] pkg_url: Option, } @@ -140,7 +170,7 @@ async fn entry() -> Result<()> { } // Load options - let mut opts = Options::from_iter(args.iter()); + let mut opts = Options::parse_from(args); let cli_overrides = PkgOverride { pkg_url: opts.pkg_url.take(), pkg_fmt: opts.pkg_fmt.take(), @@ -151,6 +181,7 @@ async fn entry() -> Result<()> { let mut log_config = ConfigBuilder::new(); log_config.add_filter_ignore("hyper".to_string()); log_config.add_filter_ignore("reqwest".to_string()); + log_config.add_filter_ignore("rustls".to_string()); log_config.set_location_level(LevelFilter::Off); TermLogger::init( opts.log_level, @@ -160,6 +191,13 @@ async fn entry() -> Result<()> { ) .unwrap(); + // 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`"); + miette!("No install path found or specified") + })?; + debug!("Using install path: {}", install_path.display()); + // Create a temporary directory for downloads etc. let temp_dir = TempDir::new() .map_err(BinstallError::from) @@ -204,51 +242,68 @@ async fn entry() -> Result<()> { manifest.bin, ); - // Merge any overrides - if let Some(o) = meta.overrides.remove(&opts.target) { - meta.merge(&o); - } + let desired_targets = { + let from_opts = opts + .targets + .as_ref() + .map(|ts| ts.split(',').map(|t| t.to_string()).collect()); - meta.merge(&cli_overrides); - debug!("Found metadata: {:?}", meta); + if let Some(ts) = from_opts { + ts + } else { + detect_targets().await + } + }; - // 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`"); - miette!("No install path found or specified") - })?; - debug!("Using install path: {}", install_path.display()); + let mut fetchers = MultiFetcher::default(); + + for target in &desired_targets { + debug!("Building metadata for target: {target}"); + let mut target_meta = meta.clone(); - // Compute temporary directory for downloads - let pkg_path = temp_dir - .path() - .join(format!("pkg-{}.{}", opts.name, meta.pkg_fmt)); - debug!("Using temporary download path: {}", pkg_path.display()); + // Merge any overrides + if let Some(o) = target_meta.overrides.get(target).cloned() { + target_meta.merge(&o); + } + + target_meta.merge(&cli_overrides); + debug!("Found metadata: {target_meta:?}"); - let fetcher_data: Vec<_> = detect_targets() - .await - .into_iter() - .map(|target| Data { + let fetcher_data = Data { name: package.name.clone(), - target: target.into(), + target: target.clone(), version: package.version.clone(), repo: package.repository.clone(), - meta: meta.clone(), - }) - .collect(); + meta: target_meta, + }; - // Try github releases, then quickinstall - let mut fetchers = MultiFetcher::default(); - for data in &fetcher_data { - fetchers.add(GhCrateMeta::new(data).await); - fetchers.add(QuickInstall::new(data).await); + fetchers.add(GhCrateMeta::new(&fetcher_data).await); + fetchers.add(QuickInstall::new(&fetcher_data).await); } match fetchers.first_available().await { Some(fetcher) => { + // Build final metadata + let fetcher_target = fetcher.target(); + if let Some(o) = meta.overrides.get(&fetcher_target.to_owned()).cloned() { + meta.merge(&o); + } + meta.merge(&cli_overrides); + + debug!( + "Found a binary install source: {} ({fetcher_target})", + fetcher.source_name() + ); + + // Compute temporary directory for downloads + let pkg_path = temp_dir + .path() + .join(format!("pkg-{}.{}", opts.name, meta.pkg_fmt)); + debug!("Using temporary download path: {}", pkg_path.display()); + install_from_package( binaries, - &*fetcher, + fetcher.as_ref(), install_path, meta, opts, @@ -259,10 +314,17 @@ async fn entry() -> Result<()> { .await } None => { - temp_dir.close().unwrap_or_else(|err| { - warn!("Failed to clean up some resources: {err}"); - }); - install_from_source(opts, package).await + if !opts.no_cleanup { + temp_dir.close().unwrap_or_else(|err| { + warn!("Failed to clean up some resources: {err}"); + }); + } + + let target = desired_targets + .first() + .ok_or_else(|| miette!("No viable targets found, try with `--targets`"))?; + + install_from_source(opts, package, target).await } } } @@ -407,7 +469,7 @@ async fn install_from_package( Ok(()) } -async fn install_from_source(opts: Options, package: Package) -> Result<()> { +async fn install_from_source(opts: Options, package: Package, target: &str) -> Result<()> { // Prompt user for source install warn!("The package will be installed from source (with cargo)",); if !opts.no_confirm && !opts.dry_run { @@ -416,14 +478,14 @@ async fn install_from_source(opts: Options, package: Package) -> Result<() if opts.dry_run { info!( - "Dry-run: running `cargo install {} --version {} --target {}`", - package.name, package.version, opts.target + "Dry-run: running `cargo install {} --version {} --target {target}`", + package.name, package.version ); Ok(()) } else { debug!( - "Running `cargo install {} --version {} --target {}`", - package.name, package.version, opts.target + "Running `cargo install {} --version {} --target {target}`", + package.name, package.version ); let mut child = Command::new("cargo") .arg("install") @@ -431,7 +493,7 @@ async fn install_from_source(opts: Options, package: Package) -> Result<() .arg("--version") .arg(package.version) .arg("--target") - .arg(opts.target) + .arg(target) .spawn() .into_diagnostic() .wrap_err("Spawning cargo install failed.")?; diff --git a/src/target.rs b/src/target.rs index 7f5e4f7b4..ac6b6be9a 100644 --- a/src/target.rs +++ b/src/target.rs @@ -18,13 +18,13 @@ pub const TARGET: &str = env!("TARGET"); /// /// Check [this issue](https://github.com/ryankurte/cargo-binstall/issues/155) /// for more information. -pub async fn detect_targets() -> Vec> { +pub async fn detect_targets() -> Vec { if let Some(target) = get_target_from_rustc().await { let mut v = vec![target]; #[cfg(target_os = "linux")] if v[0].contains("gnu") { - v.push(v[0].replace("gnu", "musl").into_boxed_str()); + v.push(v[0].replace("gnu", "musl")); } #[cfg(target_os = "macos")] @@ -51,7 +51,7 @@ pub async fn detect_targets() -> Vec> { /// Figure out what the host target is using `rustc`. /// If `rustc` is absent, then it would return `None`. -async fn get_target_from_rustc() -> Option> { +async fn get_target_from_rustc() -> Option { let Output { status, stdout, .. } = Command::new("rustc").arg("-vV").output().await.ok()?; if !status.success() { return None; @@ -60,17 +60,14 @@ async fn get_target_from_rustc() -> Option> { Cursor::new(stdout) .lines() .filter_map(|line| line.ok()) - .find_map(|line| { - line.strip_prefix("host: ") - .map(|host| host.to_owned().into_boxed_str()) - }) + .find_map(|line| line.strip_prefix("host: ").map(|host| host.to_owned())) } #[cfg(target_os = "linux")] mod linux { use super::{Command, Output, TARGET}; - pub(super) async fn detect_targets_linux() -> Vec> { + pub(super) async fn detect_targets_linux() -> Vec { let abi = parse_abi(); if let Ok(Output { @@ -123,16 +120,13 @@ mod linux { } } - fn create_target_str(libc_version: &str, abi: &str) -> Box { - let prefix = TARGET.rsplit_once('-').unwrap().0; + fn create_target_str(libc_version: &str, abi: &str) -> String { + let prefix = TARGET + .rsplit_once('-') + .expect("unwrap: TARGET always has a -") + .0; - let mut target = String::with_capacity(prefix.len() + 1 + libc_version.len() + abi.len()); - target.push_str(prefix); - target.push('-'); - target.push_str(libc_version); - target.push_str(abi); - - target.into_boxed_str() + format!("{prefix}-{libc_version}{abi}") } } @@ -143,7 +137,7 @@ mod macos { pub(super) const AARCH64: &str = "aarch64-apple-darwin"; pub(super) const X86: &str = "x86_64-apple-darwin"; - pub(super) fn detect_targets_macos() -> Vec> { + pub(super) fn detect_targets_macos() -> Vec { if guess_host_triple() == Some(AARCH64) { vec![AARCH64.into(), X86.into()] } else {