diff --git a/Cargo.lock b/Cargo.lock index 58b6a742..a93ae818 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,12 +134,14 @@ dependencies = [ "anyhow", "cargo_metadata", "cargo_toml", + "crates-index", "crates_io_api", "dirs", "env_logger", "flate2", "log", "reqwest", + "semver", "serde", "serde_derive", "simplelog", @@ -180,6 +182,9 @@ name = "cc" version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -244,6 +249,24 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "crates-index" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24823d553339d125040d989d2a593a01b034fe5ac17714423bcd2c3d168878" +dependencies = [ + "git2", + "glob", + "hex", + "home", + "memchr", + "semver", + "serde", + "serde_derive", + "serde_json", + "smartstring", +] + [[package]] name = "crates_io_api" version = "0.6.1" @@ -530,6 +553,27 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "git2" +version = "0.13.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f267c9da8a4de3c615b59e23606c75f164f84896e97f4dd6c15a4294de4359" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "h2" version = "0.2.7" @@ -574,6 +618,24 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +dependencies = [ + "serde", +] + +[[package]] +name = "home" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "http" version = "0.2.2" @@ -692,6 +754,15 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.46" @@ -723,6 +794,46 @@ version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" +[[package]] +name = "libgit2-sys" +version = "0.12.17+1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ebdf65ca745126df8824688637aa0535a88900b83362d8ca63893bcf4e8841" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df40b13fe7ea1be9b9dffa365a51273816c345fc1811478b57ed7d964fbfc4ce" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "log" version = "0.4.11" @@ -1384,6 +1495,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +[[package]] +name = "smartstring" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ada87540bf8ef4cf8a1789deb175626829bb59b1fefd816cf7f7f55efcdbae9" +dependencies = [ + "serde", + "static_assertions", +] + [[package]] name = "socket2" version = "0.3.19" @@ -1395,6 +1516,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 0e9fe478..571099d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,6 @@ authors = ["ryan "] edition = "2018" license = "GPL-3.0" -[package.metadata.binstall] -pkg-url = "https://github.com/ryankurte/cargo-binstall/releases/download/v{ version }/cargo-binstall-{ target }.tgz" -pkg-fmt = "tgz" - [[pkg_bin]] name = "cargo-binstall" path = "cargo-binstall-{ target }" @@ -34,10 +30,8 @@ strum_macros = "0.20.1" strum = "0.20.0" dirs = "3.0.1" serde_derive = "1.0.118" +crates-index = "0.16.2" +semver = "0.11.0" [dev-dependencies] env_logger = "0.8.2" -#github = "0.1.2" - -[patch.crates-io] -#reqwest = { git = "https://github.com/seanmonstar/reqwest.git" } diff --git a/src/drivers.rs b/src/drivers.rs index cffe9ad0..9270de59 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -2,45 +2,90 @@ use std::time::Duration; use std::path::{Path, PathBuf}; -use log::{debug, error}; +use log::{debug}; +use anyhow::{Context, anyhow}; +use semver::{Version, VersionReq}; use crates_io_api::AsyncClient; use crate::PkgFmt; use crate::helpers::*; +fn find_version<'a, V: Iterator>(requirement: &str, version_iter: V) -> Result { + // Parse version requirement + let version_req = VersionReq::parse(requirement)?; + + // Filter for matching versions + let mut filtered: Vec<_> = version_iter.filter(|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 = match Version::parse(ver_str) { + Ok(sv) => sv, + Err(_) => return false, + }; + + debug!("Version: {:?}", ver); + + // Filter by version match + version_req.matches(&ver) + }).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); + + // Return highest version + match filtered.get(0) { + Some(v) => Ok(v.to_string()), + None => Err(anyhow!("No matching version for requirement: '{}'", version_req)) + } +} + /// Fetch a crate by name and version from crates.io -pub async fn fetch_crate_cratesio(name: &str, version: Option<&str>, temp_dir: &Path) -> Result { - // Build crates.io api client and fetch info - let api_client = AsyncClient::new("cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100))?; +pub async fn fetch_crate_cratesio(name: &str, version_req: &str, temp_dir: &Path) -> Result { - debug!("Fetching information for crate: '{}'", name); + // Fetch / update index + debug!("Updating crates.io index"); + let index = crates_index::Index::new_cargo_default(); + index.retrieve_or_update()?; - // Fetch overall crate info - let info = match api_client.get_crate(name.as_ref()).await { - Ok(i) => i, - Err(e) => { - error!("Error fetching information for crate {}: {}", name, e); - return Err(e.into()) + // Lookup crate in index + debug!("Looking up crate information"); + let base_info = match index.crate_(name) { + Some(i) => i, + None => { + return Err(anyhow::anyhow!("Error fetching information for crate {}", name)); } }; - // Use specified or latest version - let version_num = match version { - Some(v) => v.to_string(), - None => info.crate_data.max_version, - }; + // Locate matching version + let version_iter = base_info.versions().iter().map(|v| v.version() ); + let version_name = find_version(version_req, version_iter)?; + + // Build crates.io api client + let api_client = AsyncClient::new("cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100))?; - // Fetch crates.io information for the specified version - // Note it is not viable to use a semver match here as crates.io - // appears to elide alpha and yanked versions in the generic response... - let versions = info.versions.clone(); - let version = match versions.iter().find(|v| v.num == version_num) { + // Fetch online crate information + let crate_info = api_client.get_crate(name.as_ref()).await + .context("Error fetching crate information")?; + + // Fetch information for the filtered version + let version = match crate_info.versions.iter().find(|v| v.num == version_name) { Some(v) => v, None => { - error!("No crates.io information found for crate: '{}' version: '{}'", - name, version_num); - return Err(anyhow::anyhow!("No crate information found")); + return Err(anyhow::anyhow!("No information found for crate: '{}' version: '{}'", + name, version_name)); } }; @@ -58,7 +103,7 @@ pub async fn fetch_crate_cratesio(name: &str, version: Option<&str>, temp_dir: & // Decompress downloaded tgz debug!("Decompressing crate archive"); extract(&tgz_path, PkgFmt::Tgz, &temp_dir)?; - let crate_path = temp_dir.join(format!("{}-{}", name, version_num)); + let crate_path = temp_dir.join(format!("{}-{}", name, version_name)); // Return crate directory Ok(crate_path) diff --git a/src/main.rs b/src/main.rs index d5ce70f5..a3dab985 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,8 +18,8 @@ struct Options { name: String, /// Filter for package version to install - #[structopt(long)] - version: Option, + #[structopt(long, default_value = "*")] + version: String, /// Override binary target, ignoring compiled version #[structopt(long, default_value = TARGET)] @@ -30,14 +30,6 @@ struct Options { #[structopt(long)] install_path: Option, - /// Do not cleanup temporary files on success - #[structopt(long)] - no_cleanup: bool, - - /// Disable interactive mode / confirmation - #[structopt(long)] - no_confirm: bool, - /// Disable symlinking / versioned updates #[structopt(long)] no_symlinks: bool, @@ -46,6 +38,14 @@ struct Options { #[structopt(long)] dry_run: bool, + /// Disable interactive mode / confirmation + #[structopt(long)] + no_confirm: bool, + + /// Do not cleanup temporary files on success + #[structopt(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 @@ -59,7 +59,6 @@ struct Options { } - #[tokio::main] async fn main() -> Result<(), anyhow::Error> { @@ -91,7 +90,7 @@ async fn main() -> Result<(), anyhow::Error> { // TODO: support git-based fetches (whole repo name rather than just crate name) let manifest_path = match opts.manifest_path.clone() { Some(p) => p, - None => fetch_crate_cratesio(&opts.name, opts.version.as_deref(), temp_dir.path()).await?, + None => fetch_crate_cratesio(&opts.name, &opts.version, temp_dir.path()).await?, }; debug!("Reading manifest: {}", manifest_path.display());