diff --git a/src/binstall.rs b/src/binstall.rs index 5de574bb..02e608c8 100644 --- a/src/binstall.rs +++ b/src/binstall.rs @@ -1,5 +1,7 @@ use std::path::PathBuf; +use crate::{DesiredTargets, PkgOverride}; + mod resolve; pub use resolve::*; @@ -11,4 +13,6 @@ pub struct Options { pub dry_run: bool, pub version: Option, pub manifest_path: Option, + pub cli_overrides: PkgOverride, + pub desired_targets: DesiredTargets, } diff --git a/src/binstall/install.rs b/src/binstall/install.rs index 6c1830ea..c1b78fc3 100644 --- a/src/binstall/install.rs +++ b/src/binstall/install.rs @@ -11,7 +11,6 @@ use crate::{bins, fetchers::Fetcher, *}; pub async fn install( resolution: Resolution, opts: Arc, - desired_targets: DesiredTargets, jobserver_client: LazyJobserverClient, ) -> Result<()> { match resolution { @@ -32,7 +31,7 @@ pub async fn install( install_from_package(fetcher, opts, cvs, version, bin_path, bin_files).await } Resolution::InstallFromSource { package } => { - let desired_targets = desired_targets.get().await; + let desired_targets = opts.desired_targets.get().await; let target = desired_targets .first() .ok_or_else(|| miette!("No viable targets found, try with `--targets`"))?; diff --git a/src/binstall/resolve.rs b/src/binstall/resolve.rs index 33592e60..f2b4f8c6 100644 --- a/src/binstall/resolve.rs +++ b/src/binstall/resolve.rs @@ -75,11 +75,10 @@ impl Resolution { pub async fn resolve( opts: Arc, crate_name: CrateName, - desired_targets: DesiredTargets, - cli_overrides: Arc, temp_dir: Arc, install_path: Arc, client: Client, + crates_io_api_client: crates_io_api::AsyncClient, ) -> Result { info!("Installing package: '{}'", crate_name); @@ -90,6 +89,7 @@ pub async fn resolve( (None, None) => "*".to_string(), }; + // Treat 0.1.2 as =0.1.2 if version .chars() .next() @@ -104,7 +104,9 @@ pub async fn resolve( // TODO: support git-based fetches (whole repo name rather than just crate name) let manifest = match opts.manifest_path.clone() { Some(manifest_path) => load_manifest_path(manifest_path.join("Cargo.toml"))?, - None => fetch_crate_cratesio(&client, &crate_name.name, &version).await?, + None => { + fetch_crate_cratesio(&client, &crates_io_api_client, &crate_name.name, &version).await? + } }; let package = manifest.package.unwrap(); @@ -120,7 +122,7 @@ pub async fn resolve( let mut fetchers = MultiFetcher::default(); - let desired_targets = desired_targets.get().await; + let desired_targets = opts.desired_targets.get().await; for target in desired_targets { debug!("Building metadata for target: {target}"); @@ -131,7 +133,7 @@ pub async fn resolve( target_meta.merge(&o); } - target_meta.merge(&cli_overrides); + target_meta.merge(&opts.cli_overrides); debug!("Found metadata: {target_meta:?}"); let fetcher_data = Data { @@ -153,7 +155,7 @@ pub async fn resolve( if let Some(o) = meta.overrides.get(&fetcher_target.to_owned()).cloned() { meta.merge(&o); } - meta.merge(&cli_overrides); + meta.merge(&opts.cli_overrides); // Generate temporary binary path let bin_path = temp_dir.join(format!("bin-{}", crate_name.name)); diff --git a/src/drivers/crates_io.rs b/src/drivers/crates_io.rs index a389f5d1..5d297d40 100644 --- a/src/drivers/crates_io.rs +++ b/src/drivers/crates_io.rs @@ -1,5 +1,4 @@ use std::path::PathBuf; -use std::time::Duration; use cargo_toml::Manifest; use crates_io_api::AsyncClient; @@ -18,46 +17,25 @@ use visitor::ManifestVisitor; /// Fetch a crate Cargo.toml by name and version from crates.io pub async fn fetch_crate_cratesio( client: &Client, + crates_io_api_client: &AsyncClient, name: &str, version_req: &str, ) -> Result, BinstallError> { // Fetch / update index debug!("Looking up crate information"); - // Build crates.io api client - 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 base_info = - api_client - .get_crate(name.as_ref()) - .await - .map_err(|err| BinstallError::CratesIoApi { - crate_name: name.into(), - err, - })?; + let base_info = crates_io_api_client + .get_crate(name.as_ref()) + .await + .map_err(|err| BinstallError::CratesIoApi { + crate_name: name.into(), + err, + })?; // Locate matching version - let version_iter = - base_info - .versions - .iter() - .filter_map(|v| if !v.yanked { Some(&v.num) } else { None }); - let version_name = find_version(version_req, version_iter)?; - - // Fetch information for the filtered version - 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(), - })?; + let version_iter = base_info.versions.iter().filter(|v| !v.yanked); + let (version, version_name) = find_version(version_req, version_iter)?; debug!("Found information for crate version: '{}'", version.num); diff --git a/src/drivers/version.rs b/src/drivers/version.rs index 7d5f4a74..35e11299 100644 --- a/src/drivers/version.rs +++ b/src/drivers/version.rs @@ -1,48 +1,56 @@ -use std::collections::BTreeSet; - use log::debug; -use semver::{Version, VersionReq}; +use semver::VersionReq; use crate::BinstallError; -pub(super) fn find_version<'a, V: Iterator>( +pub(super) trait Version { + /// Return `None` on error. + fn get_version(&self) -> Option; +} + +impl Version for &T { + fn get_version(&self) -> Option { + (*self).get_version() + } +} + +impl Version for crates_io_api::Version { + fn get_version(&self) -> Option { + // Remove leading `v` for git tags + let ver_str = match self.num.strip_prefix('v') { + Some(v) => v, + None => &self.num, + }; + + // Parse out version + semver::Version::parse(ver_str).ok() + } +} + +pub(super) fn find_version>( requirement: &str, - version_iter: V, -) -> Result { + version_iter: VersionIter, +) -> Result<(Item, semver::Version), BinstallError> { // Parse version requirement let version_req = VersionReq::parse(requirement).map_err(|err| BinstallError::VersionReq { req: requirement.into(), err, })?; - // Filter for matching versions - 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, - None => v, - }; - - // Parse out version - let ver = Version::parse(ver_str).ok()?; - debug!("Version: {:?}", ver); + version_iter + // Filter for matching versions + .filter_map(|item| { + let ver = item.get_version()?; // Filter by version match if version_req.matches(&ver) { - Some(ver) + debug!("Version: {:?}", ver); + Some((item, ver)) } else { None } }) - .collect(); - - debug!("Filtered: {:?}", filtered); - - // Return highest version - filtered - .iter() - .max() - .cloned() + // Return highest version + .max_by_key(|(_item, ver)| ver.clone()) .ok_or(BinstallError::VersionMismatch { req: version_req }) } diff --git a/src/fetchers.rs b/src/fetchers.rs index 237a2b85..46917e0f 100644 --- a/src/fetchers.rs +++ b/src/fetchers.rs @@ -47,30 +47,21 @@ pub struct Data { pub meta: PkgMeta, } +type FetcherJoinHandle = AutoAbortJoinHandle>; + #[derive(Default)] -pub struct MultiFetcher { - fetchers: Vec>, -} +pub struct MultiFetcher(Vec<(Arc, FetcherJoinHandle)>); impl MultiFetcher { pub fn add(&mut self, fetcher: Arc) { - self.fetchers.push(fetcher); + self.0.push(( + fetcher.clone(), + AutoAbortJoinHandle::new(tokio::spawn(async move { fetcher.check().await })), + )); } - pub async fn first_available(&self) -> Option> { - let handles: Vec<_> = self - .fetchers - .iter() - .cloned() - .map(|fetcher| { - ( - fetcher.clone(), - AutoAbortJoinHandle::new(tokio::spawn(async move { fetcher.check().await })), - ) - }) - .collect(); - - for (fetcher, handle) in handles { + pub async fn first_available(self) -> Option> { + for (fetcher, handle) in self.0 { match handle.await { Ok(Ok(true)) => return Some(fetcher), Ok(Ok(false)) => (), @@ -83,7 +74,7 @@ impl MultiFetcher { } Err(join_err) => { debug!( - "Error while checking fetcher {}: {}", + "Error while joining the task that checks the fetcher {}: {}", fetcher.source_name(), join_err ); diff --git a/src/helpers.rs b/src/helpers.rs index 3dc2de96..3381c839 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,12 +1,14 @@ use std::fmt::Debug; use std::fs; use std::io; +use std::ops; use std::path::{Path, PathBuf}; use bytes::Bytes; use cargo_toml::Manifest; use futures_util::stream::Stream; use log::debug; +use once_cell::sync::OnceCell; use reqwest::{tls, Client, ClientBuilder, Method, Response}; use serde::Serialize; use tempfile::NamedTempFile; @@ -40,6 +42,14 @@ pub use tls_version::TLSVersion; mod crate_name; pub use crate_name::CrateName; +pub fn cargo_home() -> Result<&'static Path, io::Error> { + static CARGO_HOME: OnceCell = OnceCell::new(); + + CARGO_HOME + .get_or_try_init(home::cargo_home) + .map(ops::Deref::deref) +} + pub async fn await_task(task: tokio::task::JoinHandle>) -> miette::Result { match task.await { Ok(res) => res, diff --git a/src/main.rs b/src/main.rs index 51847dcb..31daa6fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -186,17 +186,28 @@ async fn entry(jobserver_client: LazyJobserverClient) -> Result<()> { // `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"] // `cargo binstall --help` gives ["/home/ryan/.cargo/bin/cargo-binstall", "binstall", "--help"] let mut args: Vec = std::env::args_os().collect(); - if args.len() > 1 && args[1] == "binstall" { - args.remove(1); - } + let args = if args.len() > 1 && args[1] == "binstall" { + // Equivalent to + // + // args.remove(1); + // + // But is O(1) + args.swap(0, 1); + let mut args = args.into_iter(); + drop(args.next().unwrap()); + + args + } else { + args.into_iter() + }; // Load options let mut opts = Options::parse_from(args); - let cli_overrides = Arc::new(PkgOverride { + let cli_overrides = PkgOverride { pkg_url: opts.pkg_url.take(), pkg_fmt: opts.pkg_fmt.take(), bin_dir: opts.bin_dir.take(), - }); + }; let crate_names = take(&mut opts.crate_names); if crate_names.len() > 1 && opts.manifest_path.is_some() { return Err(BinstallError::ManifestPathConflictedWithBatchInstallation.into()); @@ -205,6 +216,13 @@ async fn entry(jobserver_client: LazyJobserverClient) -> Result<()> { // Initialize reqwest client let client = create_reqwest_client(opts.secure, opts.min_tls_version.map(|v| v.into()))?; + // Build crates.io api client + let crates_io_api_client = crates_io_api::AsyncClient::new( + "cargo-binstall (https://github.com/ryankurte/cargo-binstall)", + Duration::from_millis(100), + ) + .expect("bug: invalid user agent"); + // Setup logging let mut log_config = ConfigBuilder::new(); log_config.add_filter_ignore("hyper".to_string()); @@ -247,6 +265,8 @@ async fn entry(jobserver_client: LazyJobserverClient) -> Result<()> { dry_run: opts.dry_run, version: opts.version.take(), manifest_path: opts.manifest_path.take(), + cli_overrides, + desired_targets, }); let tasks: Vec<_> = if !opts.dry_run && !opts.no_confirm { @@ -257,11 +277,10 @@ async fn entry(jobserver_client: LazyJobserverClient) -> Result<()> { tokio::spawn(binstall::resolve( binstall_opts.clone(), crate_name, - desired_targets.clone(), - cli_overrides.clone(), temp_dir_path.clone(), install_path.clone(), client.clone(), + crates_io_api_client.clone(), )) }) .collect(); @@ -281,7 +300,6 @@ async fn entry(jobserver_client: LazyJobserverClient) -> Result<()> { tokio::spawn(binstall::install( resolution, binstall_opts.clone(), - desired_targets.clone(), jobserver_client.clone(), )) }) @@ -293,26 +311,23 @@ async fn entry(jobserver_client: LazyJobserverClient) -> Result<()> { .map(|crate_name| { let opts = binstall_opts.clone(); let temp_dir_path = temp_dir_path.clone(); - let desired_target = desired_targets.clone(); let jobserver_client = jobserver_client.clone(); - let desired_targets = desired_targets.clone(); let client = client.clone(); - let cli_overrides = cli_overrides.clone(); + let crates_io_api_client = crates_io_api_client.clone(); let install_path = install_path.clone(); tokio::spawn(async move { let resolution = binstall::resolve( opts.clone(), crate_name, - desired_targets.clone(), - cli_overrides, temp_dir_path, install_path, client, + crates_io_api_client, ) .await?; - binstall::install(resolution, opts, desired_target, jobserver_client).await + binstall::install(resolution, opts, jobserver_client).await }) }) .collect() diff --git a/src/metafiles/v1.rs b/src/metafiles/v1.rs index 1180814a..19f0ab8f 100644 --- a/src/metafiles/v1.rs +++ b/src/metafiles/v1.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use super::CrateVersionSource; +use crate::cargo_home; #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct CratesToml { @@ -18,7 +19,7 @@ pub struct CratesToml { impl CratesToml { pub fn default_path() -> Result { - Ok(home::cargo_home()?.join(".crates.toml")) + Ok(cargo_home()?.join(".crates.toml")) } pub fn load() -> Result { diff --git a/src/metafiles/v2.rs b/src/metafiles/v2.rs index 70c4e46e..4724b1e8 100644 --- a/src/metafiles/v2.rs +++ b/src/metafiles/v2.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use super::CrateVersionSource; +use crate::cargo_home; #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct Crates2Json { @@ -39,7 +40,7 @@ pub struct CrateInfo { impl Crates2Json { pub fn default_path() -> Result { - Ok(home::cargo_home()?.join(".crates2.json")) + Ok(cargo_home()?.join(".crates2.json")) } pub fn load() -> Result { diff --git a/src/target.rs b/src/target.rs index c6bf8dd9..8dfb75b6 100644 --- a/src/target.rs +++ b/src/target.rs @@ -8,18 +8,18 @@ use tokio::sync::OnceCell; /// Compiled target triple, used as default for binary fetching pub const TARGET: &str = env!("TARGET"); -#[derive(Debug, Clone)] +#[derive(Debug)] enum DesiredTargetsInner { AutoDetect(Arc>>), - Initialized(Arc>), + Initialized(Vec), } -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct DesiredTargets(DesiredTargetsInner); impl DesiredTargets { fn initialized(targets: Vec) -> Self { - Self(DesiredTargetsInner::Initialized(Arc::new(targets))) + Self(DesiredTargetsInner::Initialized(targets)) } fn auto_detect() -> Self {