From 8f7f7f553063a446b54fa90616a2926aac080ba0 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 30 Dec 2020 15:27:39 +1300 Subject: [PATCH 1/9] refactoring to library --- Cargo.lock | 1 + Cargo.toml | 2 + README.md | 2 +- src/drivers.rs | 80 ++++++++++++++++++ src/helpers.rs | 65 +++++++++++++++ src/lib.rs | 75 +++++++++++++++++ src/main.rs | 222 +++++++++---------------------------------------- 7 files changed, 263 insertions(+), 184 deletions(-) create mode 100644 src/drivers.rs create mode 100644 src/helpers.rs create mode 100644 src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f26c3e60..373832ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,7 @@ dependencies = [ "flate2", "log", "reqwest", + "semver", "serde", "serde_derive", "simplelog", diff --git a/Cargo.toml b/Cargo.toml index 6c1f27db..efa4e1de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,5 @@ strum_macros = "0.20.1" strum = "0.20.0" dirs = "3.0.1" serde_derive = "1.0.118" +#github = "0.1.2" +semver = "0.11.0" diff --git a/README.md b/README.md index ae39872f..f78a4f7e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Cargo metadata is used to avoid the need for an additional centralised index or First you'll need to install `cargo-binstall` either via `cargo install cargo-binstall` (and it'll have to compile, sorry...), or by grabbing a pre-compiled version from the [releases](https://github.com/ryankurte/cargo-binstall/releases) page and putting that somewhere on your path. It's like there's a problem we're trying to solve? Once a project supports `binstall` you can then install binaries via `cargo binstall NAME` where `NAME` is the name of the crate. This will then fetch the metadata for the provided crate, lookup the associated binary file, and download this onto your system. -By default the latest version is installed, which can be overridden using the `--version` argument, and packages are installed to `$HOME/.cargo/bin` as is consistent with `cargo install`, which can be overridden via the `--install-path` argument. As always `--help` will show available options. +By default the latest version from is installed, which can be overridden using the `--version` argument, and packages are installed to `$HOME/.cargo/bin` as is consistent with `cargo install`, which can be overridden via the `--install-path` argument. As always `--help` will show available options. We hope the defaults will work without configuration in _some_ cases, however, different projects have wildly different CI and build output configurations. You will likely need to add some cargo metadata to support `binstall` in your project, see [Supporting Binary Installation](#Supporting-Binary-Installation) for details. diff --git a/src/drivers.rs b/src/drivers.rs new file mode 100644 index 00000000..41349fc8 --- /dev/null +++ b/src/drivers.rs @@ -0,0 +1,80 @@ + +use std::time::Duration; +use std::path::{Path, PathBuf}; + +use log::{debug, error}; + +use crates_io_api::AsyncClient; +use semver::Version; + +use crate::PkgFmt; +use crate::helpers::*; + +/// 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 + // TODO: support git-based fetches (whole repo name rather than just crate name) + let api_client = AsyncClient::new("cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100))?; + + debug!("Fetching information for crate: '{}'", name); + + // 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()) + } + }; + + // Use specified or latest version + let version_num = match version { + Some(v) => v.to_string(), + None => info.crate_data.max_version, + }; + + // Fetch crates.io information for the specified version + // TODO: Filter by semver matches instead of literal match + let mut versions = info.versions.clone(); + versions.sort_by(|a, b| { + let ver_a = Version::parse(&a.num).unwrap(); + let ver_b = Version::parse(&b.num).unwrap(); + + ver_a.partial_cmp(&ver_b).unwrap() + } ); + + let version = match versions.iter().find(|v| v.num == version_num) { + Some(v) => v, + None => { + error!("No crates.io information found for crate: '{}' version: '{}'", + name, version_num); + return Err(anyhow::anyhow!("No crate information found")); + } + }; + + debug!("Found information for crate version: '{}'", version.num); + + // Download crate to temporary dir (crates.io or git?) + let crate_url = format!("https://crates.io/{}", version.dl_path); + let tgz_path = temp_dir.join(format!("{}.tgz", name)); + + debug!("Fetching crate from: {}", crate_url); + + // Download crate + download(&crate_url, &tgz_path).await?; + + // Decompress downloaded tgz + debug!("Decompressing crate archive"); + extract(&tgz_path, PkgFmt::Tgz, &temp_dir)?; + let crate_path = temp_dir.join(format!("{}-{}", name, version_num)); + + // Return crate directory + Ok(crate_path) +} + +/// Fetch a crate by name and version from github +/// TODO: implement this +pub async fn fetch_crate_gh_releases(_name: &str, _version: Option<&str>, _temp_dir: &Path) -> Result { + + unimplemented!(); +} \ No newline at end of file diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 00000000..6d419858 --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,65 @@ + +use std::path::Path; + +use log::{debug, error}; + +use flate2::read::GzDecoder; +use tar::Archive; + + +use super::PkgFmt; + + +/// Download a file from the provided URL to the provided path +pub async fn download>(url: &str, path: P) -> Result<(), anyhow::Error> { + + debug!("Downloading from: '{}'", url); + + let resp = reqwest::get(url).await?; + + if !resp.status().is_success() { + error!("Download error: {}", resp.status()); + return Err(anyhow::anyhow!(resp.status())); + } + + let bytes = resp.bytes().await?; + + debug!("Download OK, writing to file: '{:?}'", path.as_ref()); + + std::fs::write(&path, bytes)?; + + Ok(()) +} + +/// Extract files from the specified source onto the specified path +pub fn extract, P: AsRef>(source: S, fmt: PkgFmt, path: P) -> Result<(), anyhow::Error> { + match fmt { + PkgFmt::Tar => { + // Extract to install dir + debug!("Extracting from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref()); + + let dat = std::fs::File::open(source)?; + let mut tar = Archive::new(dat); + + tar.unpack(path)?; + }, + PkgFmt::Tgz => { + // Extract to install dir + debug!("Decompressing from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref()); + + let dat = std::fs::File::open(source)?; + let tar = GzDecoder::new(dat); + let mut tgz = Archive::new(tar); + + tgz.unpack(path)?; + }, + PkgFmt::Bin => { + debug!("Copying data from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref()); + // Copy to install dir + std::fs::copy(source, path)?; + }, + }; + + Ok(()) +} + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..5e7e2a18 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,75 @@ +use structopt::StructOpt; +use serde::{Serialize, Deserialize}; +use strum_macros::{Display, EnumString, EnumVariantNames}; +use tinytemplate::TinyTemplate; + + +pub mod helpers; +pub use helpers::*; + +pub mod drivers; +pub use drivers::*; + + +/// Compiled target triple, used as default for binary fetching +pub const TARGET: &'static str = env!("TARGET"); + +/// Default package path for use if no path is specified +pub const DEFAULT_PKG_PATH: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }"; + + +/// Binary format enumeration +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Display, EnumString, EnumVariantNames)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum PkgFmt { + /// Download format is TAR (uncompressed) + Tar, + /// Download format is TGZ (TAR + GZip) + Tgz, + /// Download format is raw / binary + Bin, +} + + +/// Metadata for binary installation use. +/// +/// Exposed via `[package.metadata]` in `Cargo.toml` +#[derive(Clone, Debug, StructOpt, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Meta { + /// Path template override for binary downloads + pub pkg_url: Option, + /// Package name override for binary downloads + pub pkg_name: Option, + /// Format override for binary downloads + pub pkg_fmt: Option, +} + + +/// Template for constructing download paths +#[derive(Clone, Debug, Serialize)] +pub struct Context { + pub name: String, + pub repo: Option, + pub target: String, + pub version: String, + pub format: PkgFmt, +} + +impl Context { + /// Render the context into the provided template + pub fn render(&self, template: &str) -> Result { + // Create template instance + let mut tt = TinyTemplate::new(); + + // Add template to instance + tt.add_template("path", &template)?; + + // Render output + let rendered = tt.render("path", self)?; + + Ok(rendered) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index b5ec71af..8ee3cfb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,62 +1,57 @@ -use std::time::Duration; -use std::path::{PathBuf, Path}; +use std::path::{PathBuf}; use log::{debug, info, error, LevelFilter}; use simplelog::{TermLogger, ConfigBuilder, TerminalMode}; use structopt::StructOpt; -use serde::{Serialize, Deserialize}; -use crates_io_api::AsyncClient; use cargo_toml::Manifest; use tempdir::TempDir; -use flate2::read::GzDecoder; -use tar::Archive; -use tinytemplate::TinyTemplate; +use cargo_binstall::*; -/// Compiled target triple, used as default for binary fetching -const TARGET: &'static str = env!("TARGET"); - -/// Default binary path for use if no path is specified -const DEFAULT_BIN_PATH: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }"; - -/// Binary format enumeration -#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] -#[derive(strum_macros::Display, strum_macros::EnumString, strum_macros::EnumVariantNames)] -#[strum(serialize_all = "snake_case")] -#[serde(rename_all = "snake_case")] -pub enum PkgFmt { - /// Download format is TAR (uncompressed) - Tar, - /// Download format is TGZ (TAR + GZip) - Tgz, - /// Download format is raw / binary - Bin, -} #[derive(Debug, StructOpt)] struct Options { - /// Crate name to install + /// Package name or URL for installation + /// This must be either a crates.io package name or github or gitlab url #[structopt()] name: String, - /// Crate version to install + /// Package version to instal #[structopt(long)] version: Option, - /// Override the package path template. - /// If no `metadata.pkg_url` key is set or `--pkg-url` argument provided, this - /// defaults to `{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.tgz` - #[structopt(long)] - pkg_url: Option, + /// Override binary target, ignoring compiled version + #[structopt(long, default_value = TARGET)] + target: String, /// Override format for binary file download. /// Defaults to `tgz` #[structopt(long)] pkg_fmt: Option, + /// Override install path for downloaded binary. + /// Defaults to `$HOME/.cargo/bin` + #[structopt(long)] + install_path: Option, + + #[structopt(flatten)] + overrides: Overrides, + + /// Do not cleanup temporary files on success + #[structopt(long)] + no_cleanup: bool, + + /// Utility log level + #[structopt(long, default_value = "info")] + log_level: LevelFilter, +} + +#[derive(Debug, StructOpt)] +pub struct Overrides { + /// Override the package name. /// This is only useful for diagnostics when using the default `pkg_url` /// as you can otherwise customise this in the path. @@ -64,53 +59,20 @@ struct Options { #[structopt(long)] pkg_name: Option, - /// Override install path for downloaded binary. - /// Defaults to `$HOME/.cargo/bin` + /// Override the package path template. + /// If no `metadata.pkg_url` key is set or `--pkg-url` argument provided, this + /// defaults to `{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.tgz` #[structopt(long)] - install_path: Option, - - /// Override binary target, ignoring compiled version - #[structopt(long, default_value = TARGET)] - target: String, + pkg_url: Option, /// Override manifest source. /// This skips searching crates.io for a manifest and uses /// the specified path directly, useful for debugging #[structopt(long)] manifest_path: Option, - - /// Utility log level - #[structopt(long, default_value = "info")] - log_level: LevelFilter, - - /// Do not cleanup temporary files on success - #[structopt(long)] - no_cleanup: bool, } -/// Metadata for cargo-binstall exposed via cargo.toml -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct Meta { - /// Path template override for binary downloads - pub pkg_url: Option, - /// Package name override for binary downloads - pub pkg_name: Option, - /// Format override for binary downloads - pub pkg_fmt: Option, -} - -/// Template for constructing download paths -#[derive(Clone, Debug, Serialize)] -pub struct Context { - name: String, - repo: Option, - target: String, - version: String, - format: PkgFmt, -} - #[tokio::main] async fn main() -> Result<(), anyhow::Error> { @@ -138,7 +100,7 @@ async fn main() -> Result<(), anyhow::Error> { // Fetch crate via crates.io, git, or use a local manifest path // TODO: work out which of these to do based on `opts.name` - let crate_path = match opts.manifest_path { + let crate_path = match opts.overrides.manifest_path { Some(p) => p, None => fetch_crate_cratesio(&opts.name, opts.version.as_deref(), temp_dir.path()).await?, }; @@ -158,8 +120,8 @@ async fn main() -> Result<(), anyhow::Error> { let meta = package.metadata; debug!("Retrieved metadata: {:?}", meta); - // Select which binary path to use - let pkg_url = match (opts.pkg_url, meta.as_ref().map(|m| m.pkg_url.clone() ).flatten()) { + // Select which package path to use + let pkg_url = match (opts.overrides.pkg_url, meta.as_ref().map(|m| m.pkg_url.clone() ).flatten()) { (Some(p), _) => { info!("Using package url override: '{}'", p); p @@ -170,8 +132,8 @@ async fn main() -> Result<(), anyhow::Error> { }, _ => { info!("No `pkg-url` key found in Cargo.toml or `--pkg-url` argument provided"); - info!("Using default url: {}", DEFAULT_BIN_PATH); - DEFAULT_BIN_PATH.to_string() + info!("Using default url: {}", DEFAULT_PKG_PATH); + DEFAULT_PKG_PATH.to_string() }, }; @@ -183,7 +145,7 @@ async fn main() -> Result<(), anyhow::Error> { }; // Override package name if required - let pkg_name = match (&opts.pkg_name, meta.as_ref().map(|m| m.pkg_name.clone() ).flatten()) { + let pkg_name = match (&opts.overrides.pkg_name, meta.as_ref().map(|m| m.pkg_name.clone() ).flatten()) { (Some(o), _) => o.clone(), (_, Some(m)) => m, _ => opts.name.clone(), @@ -201,9 +163,7 @@ async fn main() -> Result<(), anyhow::Error> { debug!("Using context: {:?}", ctx); // Interpolate version / target / etc. - let mut tt = TinyTemplate::new(); - tt.add_template("path", &pkg_url)?; - let rendered = tt.render("path", &ctx)?; + let rendered = ctx.render(&pkg_url)?; info!("Downloading package from: '{}'", rendered); @@ -238,111 +198,7 @@ async fn main() -> Result<(), anyhow::Error> { Ok(()) } -/// Download a file from the provided URL to the provided path -async fn download>(url: &str, path: P) -> Result<(), anyhow::Error> { - debug!("Downloading from: '{}'", url); - - let resp = reqwest::get(url).await?; - - if !resp.status().is_success() { - error!("Download error: {}", resp.status()); - return Err(anyhow::anyhow!(resp.status())); - } - - let bytes = resp.bytes().await?; - - debug!("Download OK, writing to file: '{:?}'", path.as_ref()); - - std::fs::write(&path, bytes)?; - - Ok(()) -} - -fn extract, P: AsRef>(source: S, fmt: PkgFmt, path: P) -> Result<(), anyhow::Error> { - match fmt { - PkgFmt::Tar => { - // Extract to install dir - debug!("Extracting from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref()); - - let dat = std::fs::File::open(source)?; - let mut tar = Archive::new(dat); - - tar.unpack(path)?; - }, - PkgFmt::Tgz => { - // Extract to install dir - debug!("Decompressing from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref()); - - let dat = std::fs::File::open(source)?; - let tar = GzDecoder::new(dat); - let mut tgz = Archive::new(tar); - - tgz.unpack(path)?; - }, - PkgFmt::Bin => { - debug!("Copying data from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref()); - // Copy to install dir - std::fs::copy(source, path)?; - }, - }; - - Ok(()) -} - -/// Fetch a crate by name and version from crates.io -async fn fetch_crate_cratesio(name: &str, version: Option<&str>, temp_dir: &Path) -> Result { - // Build crates.io api client and fetch info - // TODO: support git-based fetches (whole repo name rather than just crate name) - let api_client = AsyncClient::new("cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100))?; - - info!("Fetching information for crate: '{}'", name); - - // 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()) - } - }; - - // Use specified or latest version - let version_num = match version { - Some(v) => v.to_string(), - None => info.crate_data.max_version, - }; - - // Fetch crates.io information for the specified version - // TODO: could do a semver match and sort here? - let version = match info.versions.iter().find(|v| v.num == version_num) { - Some(v) => v, - None => { - error!("No crates.io information found for crate: '{}' version: '{}'", - name, version_num); - return Err(anyhow::anyhow!("No crate information found")); - } - }; - - info!("Found information for crate version: '{}'", version.num); - - // Download crate to temporary dir (crates.io or git?) - let crate_url = format!("https://crates.io/{}", version.dl_path); - let tgz_path = temp_dir.join(format!("{}.tgz", name)); - - debug!("Fetching crate from: {}", crate_url); - - // Download crate - download(&crate_url, &tgz_path).await?; - - // Decompress downloaded tgz - debug!("Decompressing crate archive"); - extract(&tgz_path, PkgFmt::Tgz, &temp_dir)?; - let crate_path = temp_dir.join(format!("{}-{}", name, version_num)); - - // Return crate directory - Ok(crate_path) -} /// Fetch install path /// roughly follows https://doc.rust-lang.org/cargo/commands/cargo-install.html#description From 8777c355c552d04211fde673707d8511adfae698 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 30 Dec 2020 17:34:07 +1300 Subject: [PATCH 2/9] swapped to symlink-based installation --- Cargo.lock | 21 ++++++ Cargo.toml | 1 + src/helpers.rs | 59 +++++++++++++++- src/lib.rs | 30 ++++++--- src/main.rs | 178 ++++++++++++++++++++++++++++++++----------------- 5 files changed, 217 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 373832ac..3959c745 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,7 @@ dependencies = [ "anyhow", "cargo_metadata", "cargo_toml", + "clt", "crates_io_api", "dirs", "flate2", @@ -213,6 +214,17 @@ dependencies = [ "vec_map", ] +[[package]] +name = "clt" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada85b237ebf9bfe564adaded2ec1786e75deb71f215f59323b26a5c70d6afa0" +dependencies = [ + "getopts", + "libc", + "tempdir", +] + [[package]] name = "console_error_panic_hook" version = "0.1.6" @@ -507,6 +519,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.1.15" diff --git a/Cargo.toml b/Cargo.toml index efa4e1de..b2abb1ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,4 @@ dirs = "3.0.1" serde_derive = "1.0.118" #github = "0.1.2" semver = "0.11.0" +clt = "0.0.6" diff --git a/src/helpers.rs b/src/helpers.rs index 6d419858..61c0b4bb 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,7 +1,7 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; -use log::{debug, error}; +use log::{debug, info, error}; use flate2::read::GzDecoder; use tar::Archive; @@ -63,3 +63,58 @@ pub fn extract, P: AsRef>(source: S, fmt: PkgFmt, path: P) Ok(()) } + + +/// Fetch install path from environment +/// roughly follows https://doc.rust-lang.org/cargo/commands/cargo-install.html#description +pub fn get_install_path>(install_path: Option

) -> Option { + // Command line override first first + if let Some(p) = install_path { + return Some(PathBuf::from(p.as_ref())) + } + + // Environmental variables + if let Ok(p) = std::env::var("CARGO_INSTALL_ROOT") { + debug!("using CARGO_INSTALL_ROOT ({})", p); + let b = PathBuf::from(p); + return Some(b.join("bin")); + } + if let Ok(p) = std::env::var("CARGO_HOME") { + debug!("using CARGO_HOME ({})", p); + let b = PathBuf::from(p); + return Some(b.join("bin")); + } + + // Standard $HOME/.cargo/bin + if let Some(d) = dirs::home_dir() { + let d = d.join(".cargo/bin"); + if d.exists() { + debug!("using $HOME/.cargo/bin"); + + return Some(d); + } + } + + // Local executable dir if no cargo is found + if let Some(d) = dirs::executable_dir() { + debug!("Fallback to {}", d.display()); + return Some(d.into()); + } + + None +} + +pub fn confirm() -> Result { + info!("Do you wish to continue? yes/no"); + + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + + match input.as_str().trim() { + "yes" => Ok(true), + "no" => Ok(false), + _ => { + Err(anyhow::anyhow!("Valid options are 'yes', 'no', please try again")) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 5e7e2a18..6459fd64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,9 +14,12 @@ pub use drivers::*; /// Compiled target triple, used as default for binary fetching pub const TARGET: &'static str = env!("TARGET"); -/// Default package path for use if no path is specified +/// Default package path template (may be overridden in package Cargo.toml) pub const DEFAULT_PKG_PATH: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }"; +/// Default binary name template (may be overridden in package Cargo.toml) +pub const DEFAULT_BIN_NAME: &'static str = "{ name }-{ target }-v{ version }"; + /// Binary format enumeration #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] @@ -39,14 +42,22 @@ pub enum PkgFmt { #[derive(Clone, Debug, StructOpt, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Meta { - /// Path template override for binary downloads + /// Path template override for package downloads pub pkg_url: Option, - /// Package name override for binary downloads - pub pkg_name: Option, - /// Format override for binary downloads - pub pkg_fmt: Option, -} + /// Package name override for package downloads + pub pkg_name: Option, + + /// Format override for package downloads + pub pkg_fmt: Option, + + #[serde(default)] + /// Filters for binary files allowed in the package + pub pkg_bins: Vec, + + /// Public key for package verification (base64 encoded) + pub pkg_pub_key: Option, +} /// Template for constructing download paths #[derive(Clone, Debug, Serialize)] @@ -55,7 +66,7 @@ pub struct Context { pub repo: Option, pub target: String, pub version: String, - pub format: PkgFmt, + pub format: String, } impl Context { @@ -72,4 +83,5 @@ impl Context { Ok(rendered) } -} \ No newline at end of file +} + diff --git a/src/main.rs b/src/main.rs index 8ee3cfb3..598bdc1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use std::path::{PathBuf}; -use log::{debug, info, error, LevelFilter}; +use log::{debug, info, warn, error, LevelFilter}; use simplelog::{TermLogger, ConfigBuilder, TerminalMode}; use structopt::StructOpt; @@ -27,11 +27,6 @@ struct Options { #[structopt(long, default_value = TARGET)] target: String, - /// Override format for binary file download. - /// Defaults to `tgz` - #[structopt(long)] - pkg_fmt: Option, - /// Override install path for downloaded binary. /// Defaults to `$HOME/.cargo/bin` #[structopt(long)] @@ -44,6 +39,14 @@ struct Options { #[structopt(long)] no_cleanup: bool, + /// Disable interactive mode / confirmation + #[structopt(long)] + no_confirm: bool, + + /// Disable symlinking / versioned updates + #[structopt(long)] + no_symlinks: bool, + /// Utility log level #[structopt(long, default_value = "info")] log_level: LevelFilter, @@ -65,6 +68,11 @@ pub struct Overrides { #[structopt(long)] pkg_url: Option, + /// Override format for binary file download. + /// Defaults to `tgz` + #[structopt(long)] + pkg_fmt: Option, + /// Override manifest source. /// This skips searching crates.io for a manifest and uses /// the specified path directly, useful for debugging @@ -138,7 +146,7 @@ async fn main() -> Result<(), anyhow::Error> { }; // Select bin format to use - let pkg_fmt = match (opts.pkg_fmt, meta.as_ref().map(|m| m.pkg_fmt.clone() ).flatten()) { + let pkg_fmt = match (opts.overrides.pkg_fmt, meta.as_ref().map(|m| m.pkg_fmt.clone() ).flatten()) { (Some(o), _) => o, (_, Some(m)) => m.clone(), _ => PkgFmt::Tgz, @@ -157,7 +165,7 @@ async fn main() -> Result<(), anyhow::Error> { repo: package.repository, target: opts.target.clone(), version: package.version.clone(), - format: pkg_fmt.clone(), + format: pkg_fmt.to_string(), }; debug!("Using context: {:?}", ctx); @@ -165,20 +173,6 @@ async fn main() -> Result<(), anyhow::Error> { // Interpolate version / target / etc. let rendered = ctx.render(&pkg_url)?; - info!("Downloading package from: '{}'", rendered); - - // Download package - let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", pkg_name, pkg_fmt)); - download(&rendered, pkg_path.to_str().unwrap()).await?; - - - if opts.no_cleanup { - // Do not delete temporary directory - let _ = temp_dir.into_path(); - } - - // TODO: check signature - // Compute install directory let install_path = match get_install_path(opts.install_path) { Some(p) => p, @@ -188,48 +182,110 @@ async fn main() -> Result<(), anyhow::Error> { } }; - // Install package - info!("Installing to: '{}'", install_path); - extract(&pkg_path, pkg_fmt, &install_path)?; + debug!("Using install path: {}", install_path.display()); + info!("Downloading package from: '{}'", rendered); + + // Download package + let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", pkg_name, pkg_fmt)); + download(&rendered, pkg_path.to_str().unwrap()).await?; + + // Fetch and check package signature if available + if let Some(pub_key) = meta.as_ref().map(|m| m.pkg_pub_key.clone() ).flatten() { + debug!("Found public key: {}", pub_key); + + // Generate signature file URL + let mut sig_ctx = ctx.clone(); + sig_ctx.format = "sig".to_string(); + let sig_url = sig_ctx.render(&pkg_url)?; + + debug!("Fetching signature file: {}", sig_url); + + // Download signature file + let sig_path = temp_dir.path().join(format!("{}.sig", pkg_name)); + download(&sig_url, &sig_path).await?; + + // TODO: do the signature check + unimplemented!() + + } else { + warn!("No public key found, package signature could not be validated"); + } + + // Extract files + let bin_path = temp_dir.path().join(format!("bin-{}", pkg_name)); + extract(&pkg_path, pkg_fmt, &bin_path)?; + + // Bypass cleanup if disabled + if opts.no_cleanup { + let _ = temp_dir.into_path(); + } + + // List files to be installed + // TODO: check extracted files are sensible / filter by allowed files + // TODO: this seems overcomplicated / should be able to be simplified? + let bin_files = std::fs::read_dir(&bin_path)?; + let bin_files: Vec<_> = bin_files.filter_map(|f| f.ok() ).map(|f| { + let source = f.path().to_owned(); + let name = source.file_name().map(|v| v.to_str()).flatten().unwrap().to_string(); + + // Trim target and version from name if included in binary file name + let base_name = name.replace(&format!("-{}", ctx.target), "") + .replace(&format!("-v{}", ctx.version), "") + .replace(&format!("-{}", ctx.version), ""); + + // Generate install destination with version suffix + let dest = install_path.join(format!("{}-v{}", base_name, ctx.version)); + + // Generate symlink path from base name + let link = install_path.join(&base_name); + + (base_name, source, dest, link) + }).collect(); + + + // Prompt user for confirmation + info!("This will install the following files:"); + for (name, source, dest, _link) in &bin_files { + info!(" - {} ({} -> {})", name, source.file_name().unwrap().to_string_lossy(), dest.display()); + } + + if !opts.no_symlinks { + info!("And create (or update) the following symlinks:"); + for (name, _source, dest, link) in &bin_files { + info!(" - {} ({} -> {})", name, dest.display(), link.display()); + } + } + + if !opts.no_confirm && !confirm()? { + warn!("Installation cancelled"); + return Ok(()) + } + + // Install binaries + for (_name, source, dest, _link) in &bin_files { + // TODO: check if file already exists + std::fs::copy(source, dest)?; + } + + // Generate symlinks + if !opts.no_symlinks { + for (_name, _source, dest, link) in &bin_files { + // Remove existing symlink + // TODO: check if existing symlink is correct + if link.exists() { + std::fs::remove_file(&link)?; + } + + #[cfg(target_family = "unix")] + std::os::unix::fs::symlink(dest, link)?; + #[cfg(target_family = "windows")] + std::os::windows::fs::symlink_file(dest, link)?; + } + } - info!("Installation done!"); + info!("Installation complete!"); Ok(()) } - - -/// Fetch install path -/// roughly follows https://doc.rust-lang.org/cargo/commands/cargo-install.html#description -fn get_install_path(opt: Option) -> Option { - // Command line override first first - if let Some(p) = opt { - return Some(p) - } - - // Environmental variables - if let Ok(p) = std::env::var("CARGO_INSTALL_ROOT") { - return Some(format!("{}/bin", p)) - } - if let Ok(p) = std::env::var("CARGO_HOME") { - return Some(format!("{}/bin", p)) - } - - // Standard $HOME/.cargo/bin - if let Some(mut d) = dirs::home_dir() { - d.push(".cargo/bin"); - let p = d.as_path(); - - if p.exists() { - return Some(p.to_str().unwrap().to_owned()); - } - } - - // Local executable dir if no cargo is found - if let Some(d) = dirs::executable_dir() { - return Some(d.to_str().unwrap().to_owned()); - } - - None -} \ No newline at end of file From 223c6ef43a43c669aecb6caf106fc2e7b4e697f7 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 30 Dec 2020 18:13:38 +1300 Subject: [PATCH 3/9] playing with version matching Can't use semver because crates.io hides alpha versions? not sure how this works in cargo --- src/drivers.rs | 17 +++---- src/lib.rs | 8 +++- src/main.rs | 118 ++++++++++++++++++++----------------------------- 3 files changed, 61 insertions(+), 82 deletions(-) diff --git a/src/drivers.rs b/src/drivers.rs index 41349fc8..cffe9ad0 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -5,7 +5,6 @@ use std::path::{Path, PathBuf}; use log::{debug, error}; use crates_io_api::AsyncClient; -use semver::Version; use crate::PkgFmt; use crate::helpers::*; @@ -13,7 +12,6 @@ use crate::helpers::*; /// 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 - // TODO: support git-based fetches (whole repo name rather than just crate name) let api_client = AsyncClient::new("cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100))?; debug!("Fetching information for crate: '{}'", name); @@ -34,15 +32,9 @@ pub async fn fetch_crate_cratesio(name: &str, version: Option<&str>, temp_dir: & }; // Fetch crates.io information for the specified version - // TODO: Filter by semver matches instead of literal match - let mut versions = info.versions.clone(); - versions.sort_by(|a, b| { - let ver_a = Version::parse(&a.num).unwrap(); - let ver_b = Version::parse(&b.num).unwrap(); - - ver_a.partial_cmp(&ver_b).unwrap() - } ); - + // 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) { Some(v) => v, None => { @@ -77,4 +69,5 @@ pub async fn fetch_crate_cratesio(name: &str, version: Option<&str>, temp_dir: & pub async fn fetch_crate_gh_releases(_name: &str, _version: Option<&str>, _temp_dir: &Path) -> Result { unimplemented!(); -} \ No newline at end of file +} + diff --git a/src/lib.rs b/src/lib.rs index 6459fd64..30b63cbb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,11 @@ pub enum PkgFmt { Bin, } +impl Default for PkgFmt { + fn default() -> Self { + Self::Tgz + } +} /// Metadata for binary installation use. /// @@ -49,6 +54,7 @@ pub struct Meta { pub pkg_name: Option, /// Format override for package downloads + #[serde(default)] pub pkg_fmt: Option, #[serde(default)] @@ -56,7 +62,7 @@ pub struct Meta { pub pkg_bins: Vec, /// Public key for package verification (base64 encoded) - pub pkg_pub_key: Option, + pub pub_key: Option, } /// Template for constructing download paths diff --git a/src/main.rs b/src/main.rs index 598bdc1d..e57938f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ struct Options { #[structopt()] name: String, - /// Package version to instal + /// Package version to install #[structopt(long)] version: Option, @@ -32,9 +32,6 @@ struct Options { #[structopt(long)] install_path: Option, - #[structopt(flatten)] - overrides: Overrides, - /// Do not cleanup temporary files on success #[structopt(long)] no_cleanup: bool, @@ -47,41 +44,18 @@ struct Options { #[structopt(long)] no_symlinks: 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)] + manifest_path: Option, + /// Utility log level #[structopt(long, default_value = "info")] log_level: LevelFilter, } -#[derive(Debug, StructOpt)] -pub struct Overrides { - - /// Override the package name. - /// This is only useful for diagnostics when using the default `pkg_url` - /// as you can otherwise customise this in the path. - /// Defaults to the crate name. - #[structopt(long)] - pkg_name: Option, - - /// Override the package path template. - /// If no `metadata.pkg_url` key is set or `--pkg-url` argument provided, this - /// defaults to `{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.tgz` - #[structopt(long)] - pkg_url: Option, - - /// Override format for binary file download. - /// Defaults to `tgz` - #[structopt(long)] - pkg_fmt: Option, - - /// Override manifest source. - /// This skips searching crates.io for a manifest and uses - /// the specified path directly, useful for debugging - #[structopt(long)] - manifest_path: Option, -} - - - #[tokio::main] async fn main() -> Result<(), anyhow::Error> { @@ -106,11 +80,18 @@ async fn main() -> Result<(), anyhow::Error> { // Create a temporary directory for downloads etc. let temp_dir = TempDir::new("cargo-binstall")?; + info!("Installing package: '{}'", opts.name); + // Fetch crate via crates.io, git, or use a local manifest path // TODO: work out which of these to do based on `opts.name` - let crate_path = match opts.overrides.manifest_path { - Some(p) => p, - None => fetch_crate_cratesio(&opts.name, opts.version.as_deref(), temp_dir.path()).await?, + // TODO: support git-based fetches (whole repo name rather than just crate name) + let crate_path = match opts.manifest_path { + Some(p) => { + p + }, + None => { + fetch_crate_cratesio(&opts.name, opts.version.as_deref(), temp_dir.path()).await? + }, }; // Read cargo manifest @@ -129,37 +110,31 @@ async fn main() -> Result<(), anyhow::Error> { debug!("Retrieved metadata: {:?}", meta); // Select which package path to use - let pkg_url = match (opts.overrides.pkg_url, meta.as_ref().map(|m| m.pkg_url.clone() ).flatten()) { - (Some(p), _) => { - info!("Using package url override: '{}'", p); - p - }, - (_, Some(m)) => { - info!("Using package url: '{}'", &m); + let pkg_url = match meta.as_ref().map(|m| m.pkg_url.clone() ).flatten() { + Some(m) => { + debug!("Using package url: '{}'", &m); m }, _ => { - info!("No `pkg-url` key found in Cargo.toml or `--pkg-url` argument provided"); - info!("Using default url: {}", DEFAULT_PKG_PATH); + debug!("No `pkg-url` key found in Cargo.toml or `--pkg-url` argument provided"); + debug!("Using default url: {}", DEFAULT_PKG_PATH); DEFAULT_PKG_PATH.to_string() }, }; // Select bin format to use - let pkg_fmt = match (opts.overrides.pkg_fmt, meta.as_ref().map(|m| m.pkg_fmt.clone() ).flatten()) { - (Some(o), _) => o, - (_, Some(m)) => m.clone(), + let pkg_fmt = match meta.as_ref().map(|m| m.pkg_fmt.clone() ).flatten() { + Some(m) => m.clone(), _ => PkgFmt::Tgz, }; // Override package name if required - let pkg_name = match (&opts.overrides.pkg_name, meta.as_ref().map(|m| m.pkg_name.clone() ).flatten()) { - (Some(o), _) => o.clone(), - (_, Some(m)) => m, + let pkg_name = match meta.as_ref().map(|m| m.pkg_name.clone() ).flatten() { + Some(m) => m, _ => opts.name.clone(), }; - // Generate context for interpolation + // Generate context for URL interpolation let ctx = Context { name: pkg_name.to_string(), repo: package.repository, @@ -190,26 +165,29 @@ async fn main() -> Result<(), anyhow::Error> { let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", pkg_name, pkg_fmt)); download(&rendered, pkg_path.to_str().unwrap()).await?; - // Fetch and check package signature if available - if let Some(pub_key) = meta.as_ref().map(|m| m.pkg_pub_key.clone() ).flatten() { - debug!("Found public key: {}", pub_key); + #[cfg(incomplete)] + { + // Fetch and check package signature if available + if let Some(pub_key) = meta.as_ref().map(|m| m.pub_key.clone() ).flatten() { + debug!("Found public key: {}", pub_key); - // Generate signature file URL - let mut sig_ctx = ctx.clone(); - sig_ctx.format = "sig".to_string(); - let sig_url = sig_ctx.render(&pkg_url)?; + // Generate signature file URL + let mut sig_ctx = ctx.clone(); + sig_ctx.format = "sig".to_string(); + let sig_url = sig_ctx.render(&pkg_url)?; - debug!("Fetching signature file: {}", sig_url); + debug!("Fetching signature file: {}", sig_url); - // Download signature file - let sig_path = temp_dir.path().join(format!("{}.sig", pkg_name)); - download(&sig_url, &sig_path).await?; + // Download signature file + let sig_path = temp_dir.path().join(format!("{}.sig", pkg_name)); + download(&sig_url, &sig_path).await?; - // TODO: do the signature check - unimplemented!() + // TODO: do the signature check + unimplemented!() - } else { - warn!("No public key found, package signature could not be validated"); + } else { + warn!("No public key found, package signature could not be validated"); + } } // Extract files @@ -230,7 +208,7 @@ async fn main() -> Result<(), anyhow::Error> { let name = source.file_name().map(|v| v.to_str()).flatten().unwrap().to_string(); // Trim target and version from name if included in binary file name - let base_name = name.replace(&format!("-{}", ctx.target), "") + let base_name = name.replace(&format!("-{}", TARGET), "") .replace(&format!("-v{}", ctx.version), "") .replace(&format!("-{}", ctx.version), ""); @@ -262,6 +240,8 @@ async fn main() -> Result<(), anyhow::Error> { return Ok(()) } + info!("Installing binaries..."); + // Install binaries for (_name, source, dest, _link) in &bin_files { // TODO: check if file already exists From a012eb1fb744ee718549c18858ec093d84310d89 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 30 Dec 2020 18:15:04 +1300 Subject: [PATCH 4/9] remove unused deps --- Cargo.lock | 22 ---------------------- Cargo.toml | 3 +-- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3959c745..f26c3e60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,13 +125,11 @@ dependencies = [ "anyhow", "cargo_metadata", "cargo_toml", - "clt", "crates_io_api", "dirs", "flate2", "log", "reqwest", - "semver", "serde", "serde_derive", "simplelog", @@ -214,17 +212,6 @@ dependencies = [ "vec_map", ] -[[package]] -name = "clt" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada85b237ebf9bfe564adaded2ec1786e75deb71f215f59323b26a5c70d6afa0" -dependencies = [ - "getopts", - "libc", - "tempdir", -] - [[package]] name = "console_error_panic_hook" version = "0.1.6" @@ -519,15 +506,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "getopts" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" -dependencies = [ - "unicode-width", -] - [[package]] name = "getrandom" version = "0.1.15" diff --git a/Cargo.toml b/Cargo.toml index b2abb1ea..93526963 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,5 +28,4 @@ strum = "0.20.0" dirs = "3.0.1" serde_derive = "1.0.118" #github = "0.1.2" -semver = "0.11.0" -clt = "0.0.6" + From d7aae3275dc5ca1c9412215b2be12a0482b64d6b Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 30 Dec 2020 18:18:01 +1300 Subject: [PATCH 5/9] bump tokio version --- Cargo.lock | 129 ++++++++++++++++++++--------------------------------- Cargo.toml | 4 +- 2 files changed, 51 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f26c3e60..6e376a5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,9 +17,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4" +checksum = "ee67c11feeac938fae061b232e38e0b6d94f97a9df10e6271319325ac4c56a86" [[package]] name = "arrayref" @@ -139,7 +139,7 @@ dependencies = [ "tar", "tempdir", "tinytemplate", - "tokio", + "tokio 1.0.1", ] [[package]] @@ -212,16 +212,6 @@ dependencies = [ "vec_map", ] -[[package]] -name = "console_error_panic_hook" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" -dependencies = [ - "cfg-if 0.1.10", - "wasm-bindgen", -] - [[package]] name = "constant_time_eq" version = "0.1.5" @@ -257,7 +247,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "tokio", + "tokio 0.2.24", "url", ] @@ -531,7 +521,7 @@ dependencies = [ "http", "indexmap", "slab", - "tokio", + "tokio 0.2.24", "tokio-util", "tracing", "tracing-futures", @@ -545,9 +535,9 @@ checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" [[package]] name = "heck" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" dependencies = [ "unicode-segmentation", ] @@ -563,9 +553,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +checksum = "84129d298a6d57d246960ff8eb831ca4af3f96d29e2e28848dae275408658e26" dependencies = [ "bytes", "fnv", @@ -612,7 +602,7 @@ dependencies = [ "itoa", "pin-project 1.0.2", "socket2", - "tokio", + "tokio 0.2.24", "tower-service", "tracing", "want", @@ -627,7 +617,7 @@ dependencies = [ "bytes", "hyper", "native-tls", - "tokio", + "tokio 0.2.24", "tokio-tls", ] @@ -644,9 +634,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" dependencies = [ "autocfg", "hashbrown", @@ -669,9 +659,9 @@ checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" [[package]] name = "itoa" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "js-sys" @@ -790,9 +780,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcc7939b5edc4e4f86b1b4a04bb1498afaaf871b1a6691838ed06fcb48d3a3f" +checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" dependencies = [ "lazy_static", "libc", @@ -860,9 +850,9 @@ checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" [[package]] name = "openssl" -version = "0.10.31" +version = "0.10.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d008f51b1acffa0d3450a68606e6a51c123012edaacb0f4e1426bd978869187" +checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -880,9 +870,9 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-sys" -version = "0.9.59" +version = "0.9.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de52d8eabd217311538a39bba130d7dea1f1e118010fee7a033d966845e7d5fe" +checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" dependencies = [ "autocfg", "cc", @@ -1057,9 +1047,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" dependencies = [ "proc-macro2", ] @@ -1170,9 +1160,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.10.9" +version = "0.10.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb15d6255c792356a0f578d8a645c677904dc02e862bebe2ecc18e0c01b9a0ce" +checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" dependencies = [ "base64", "bytes", @@ -1195,12 +1185,11 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "tokio", + "tokio 0.2.24", "tokio-tls", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-bindgen-test", "web-sys", "winreg", ] @@ -1233,12 +1222,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "scoped-tls" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" - [[package]] name = "security-framework" version = "2.0.0" @@ -1304,9 +1287,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" +checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" dependencies = [ "itoa", "ryu", @@ -1356,13 +1339,12 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "socket2" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", "winapi 0.3.9", ] @@ -1416,9 +1398,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.54" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44" +checksum = "a9802ddde94170d186eeee5005b798d9c159fa970403f1be19976d0cfb939b72" dependencies = [ "proc-macro2", "quote", @@ -1531,14 +1513,25 @@ dependencies = [ "num_cpus", "pin-project-lite 0.1.11", "slab", +] + +[[package]] +name = "tokio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258221f566b6c803c7b4714abadc080172b272090cdc5e244a6d4dd13c3a6bd" +dependencies = [ + "autocfg", + "num_cpus", + "pin-project-lite 0.2.0", "tokio-macros", ] [[package]] name = "tokio-macros" -version = "0.2.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +checksum = "42517d2975ca3114b22a16192634e8241dc5cc1f130be194645970cc1c371494" dependencies = [ "proc-macro2", "quote", @@ -1552,7 +1545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" dependencies = [ "native-tls", - "tokio", + "tokio 0.2.24", ] [[package]] @@ -1566,14 +1559,14 @@ dependencies = [ "futures-sink", "log", "pin-project-lite 0.1.11", - "tokio", + "tokio 0.2.24", ] [[package]] name = "toml" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ "serde", ] @@ -1798,30 +1791,6 @@ version = "0.2.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" -[[package]] -name = "wasm-bindgen-test" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0355fa0c1f9b792a09b6dcb6a8be24d51e71e6d74972f9eb4a44c4c004d24a25" -dependencies = [ - "console_error_panic_hook", - "js-sys", - "scoped-tls", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e07b46b98024c2ba2f9e83a10c2ef0515f057f2da299c1762a2017de80438b" -dependencies = [ - "proc-macro2", - "quote", -] - [[package]] name = "web-sys" version = "0.3.46" diff --git a/Cargo.toml b/Cargo.toml index 93526963..bd688bf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,12 @@ license = "GPL-3.0" crates_io_api = "0.6.1" cargo_metadata = "0.12.1" tinytemplate = "1.1.0" -tokio = { version = "0.2.24", features = [ "macros" ] } +tokio = { version = "1.0.1", features = [ "macros", "rt-multi-thread" ] } log = "0.4.11" structopt = "0.3.21" simplelog = "0.8.0" anyhow = "1.0.35" -reqwest = { version = "0.10.9" } +reqwest = "0.10.10" tempdir = "0.3.7" flate2 = "1.0.19" tar = "0.4.30" From 5fd78341c8ee7c6774c4093a17104a0f6feff6f3 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 31 Dec 2020 13:19:58 +1300 Subject: [PATCH 6/9] cleaning up binary logic, using standard [[bin]] definitions now --- Cargo.lock | 87 +++++++++++++++++++++++++-------- Cargo.toml | 14 +++++- src/helpers.rs | 15 +++++- src/lib.rs | 98 ++++++++++++++++++++++++++++++------- src/main.rs | 128 +++++++++++++++++++++---------------------------- 5 files changed, 228 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e376a5b..58b6a742 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -127,6 +136,7 @@ dependencies = [ "cargo_toml", "crates_io_api", "dirs", + "env_logger", "flate2", "log", "reqwest", @@ -139,7 +149,7 @@ dependencies = [ "tar", "tempdir", "tinytemplate", - "tokio 1.0.1", + "tokio", ] [[package]] @@ -247,7 +257,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "tokio 0.2.24", + "tokio", "url", ] @@ -309,6 +319,19 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "env_logger" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -521,7 +544,7 @@ dependencies = [ "http", "indexmap", "slab", - "tokio 0.2.24", + "tokio", "tokio-util", "tracing", "tracing-futures", @@ -584,6 +607,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" +[[package]] +name = "humantime" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1ad908cc71012b7bea4d0c53ba96a8cba9962f048fa68d143376143d863b7a" + [[package]] name = "hyper" version = "0.13.9" @@ -602,7 +631,7 @@ dependencies = [ "itoa", "pin-project 1.0.2", "socket2", - "tokio 0.2.24", + "tokio", "tower-service", "tracing", "want", @@ -617,7 +646,7 @@ dependencies = [ "bytes", "hyper", "native-tls", - "tokio 0.2.24", + "tokio", "tokio-tls", ] @@ -1149,6 +1178,24 @@ dependencies = [ "rust-argon2", ] +[[package]] +name = "regex" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -1185,7 +1232,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "tokio 0.2.24", + "tokio", "tokio-tls", "url", "wasm-bindgen", @@ -1461,6 +1508,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + [[package]] name = "time" version = "0.1.44" @@ -1513,25 +1569,14 @@ dependencies = [ "num_cpus", "pin-project-lite 0.1.11", "slab", -] - -[[package]] -name = "tokio" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258221f566b6c803c7b4714abadc080172b272090cdc5e244a6d4dd13c3a6bd" -dependencies = [ - "autocfg", - "num_cpus", - "pin-project-lite 0.2.0", "tokio-macros", ] [[package]] name = "tokio-macros" -version = "1.0.0" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42517d2975ca3114b22a16192634e8241dc5cc1f130be194645970cc1c371494" +checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" dependencies = [ "proc-macro2", "quote", @@ -1545,7 +1590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" dependencies = [ "native-tls", - "tokio 0.2.24", + "tokio", ] [[package]] @@ -1559,7 +1604,7 @@ dependencies = [ "futures-sink", "log", "pin-project-lite 0.1.11", - "tokio 0.2.24", + "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bd688bf6..0e9fe478 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,12 +7,19 @@ 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 }" [dependencies] crates_io_api = "0.6.1" cargo_metadata = "0.12.1" tinytemplate = "1.1.0" -tokio = { version = "1.0.1", features = [ "macros", "rt-multi-thread" ] } +tokio = { version = "0.2.24", features = [ "macros" ] } log = "0.4.11" structopt = "0.3.21" simplelog = "0.8.0" @@ -27,5 +34,10 @@ strum_macros = "0.20.1" strum = "0.20.0" dirs = "3.0.1" serde_derive = "1.0.118" + +[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/helpers.rs b/src/helpers.rs index 61c0b4bb..153cedf3 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -3,12 +3,25 @@ use std::path::{Path, PathBuf}; use log::{debug, info, error}; +use cargo_toml::{Manifest}; use flate2::read::GzDecoder; use tar::Archive; +use crate::{Meta}; + use super::PkgFmt; +/// Load binstall metadata from the crate `Cargo.toml` at the provided path +pub fn load_manifest_path>(manifest_path: P) -> Result, anyhow::Error> { + debug!("Reading manifest: {}", manifest_path.as_ref().display()); + + // Load and parse manifest (this checks file system for binary output names) + let manifest = Manifest::::from_path_with_metadata(manifest_path)?; + + // Return metadata + Ok(manifest) +} /// Download a file from the provided URL to the provided path pub async fn download>(url: &str, path: P) -> Result<(), anyhow::Error> { @@ -63,8 +76,6 @@ pub fn extract, P: AsRef>(source: S, fmt: PkgFmt, path: P) Ok(()) } - - /// Fetch install path from environment /// roughly follows https://doc.rust-lang.org/cargo/commands/cargo-install.html#description pub fn get_install_path>(install_path: Option

) -> Option { diff --git a/src/lib.rs b/src/lib.rs index 30b63cbb..918b3712 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -use structopt::StructOpt; use serde::{Serialize, Deserialize}; use strum_macros::{Display, EnumString, EnumVariantNames}; use tinytemplate::TinyTemplate; @@ -15,10 +14,10 @@ pub use drivers::*; pub const TARGET: &'static str = env!("TARGET"); /// Default package path template (may be overridden in package Cargo.toml) -pub const DEFAULT_PKG_PATH: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }"; +pub const DEFAULT_PKG_URL: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }"; /// Default binary name template (may be overridden in package Cargo.toml) -pub const DEFAULT_BIN_NAME: &'static str = "{ name }-{ target }-v{ version }"; +pub const DEFAULT_BIN_PATH: &'static str = "{ name }-{ target }-v{ version }/{ name }{ format }"; /// Binary format enumeration @@ -41,30 +40,54 @@ impl Default for PkgFmt { } } +/// `binstall` metadata container +/// +/// Required to nest metadata under `package.metadata.binstall` +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Meta { + pub binstall: Option, +} + /// Metadata for binary installation use. /// /// Exposed via `[package.metadata]` in `Cargo.toml` -#[derive(Clone, Debug, StructOpt, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct Meta { - /// Path template override for package downloads - pub pkg_url: Option, +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default)] +pub struct PkgMeta { + /// URL template for package downloads + pub pkg_url: String, - /// Package name override for package downloads - pub pkg_name: Option, + /// Format for package downloads + pub pkg_fmt: PkgFmt, - /// Format override for package downloads - #[serde(default)] - pub pkg_fmt: Option, - - #[serde(default)] - /// Filters for binary files allowed in the package - pub pkg_bins: Vec, + /// Path template for binary files in packages + pub bin_dir: String, /// Public key for package verification (base64 encoded) pub pub_key: Option, } +impl Default for PkgMeta { + fn default() -> Self { + Self { + pkg_url: DEFAULT_PKG_URL.to_string(), + pkg_fmt: PkgFmt::default(), + bin_dir: DEFAULT_BIN_PATH.to_string(), + pub_key: None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct BinMeta { + /// Binary name + pub name: String, + /// Binary template path (within package) + pub path: String, +} + /// Template for constructing download paths #[derive(Clone, Debug, Serialize)] pub struct Context { @@ -91,3 +114,44 @@ impl Context { } } +#[cfg(test)] +mod test { + use crate::{load_manifest_path}; + + use cargo_toml::Product; + + fn init() { + let _ = env_logger::builder().is_test(true).try_init(); + } + + #[test] + fn parse_meta() { + init(); + + let mut manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + manifest_dir.push_str("/Cargo.toml"); + + let manifest = load_manifest_path(&manifest_dir).expect("Error parsing metadata"); + let package = manifest.package.unwrap(); + let meta = package.metadata.map(|m| m.binstall ).flatten().unwrap(); + + assert_eq!(&package.name, "cargo-binstall"); + + assert_eq!( + &meta.pkg_url, + "https://github.com/ryankurte/cargo-binstall/releases/download/v{ version }/cargo-binstall-{ target }.tgz" + ); + + assert_eq!( + manifest.bin.as_slice(), + &[ + Product{ + name: Some("cargo-binstall".to_string()), + path: Some("src/main.rs".to_string()), + edition: Some(cargo_toml::Edition::E2018), + ..Default::default() + }, + ], + ); + } +} diff --git a/src/main.rs b/src/main.rs index e57938f5..a4cb9120 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,6 @@ use simplelog::{TermLogger, ConfigBuilder, TerminalMode}; use structopt::StructOpt; -use cargo_toml::Manifest; - use tempdir::TempDir; use cargo_binstall::*; @@ -19,7 +17,7 @@ struct Options { #[structopt()] name: String, - /// Package version to install + /// Filter for package version to install #[structopt(long)] version: Option, @@ -44,6 +42,10 @@ struct Options { #[structopt(long)] no_symlinks: bool, + /// Dry run, fetch and show changes without installing binaries + #[structopt(long)] + dry_run: bool, + /// Override manifest source. /// This skips searching crates.io for a manifest and uses /// the specified path directly, useful for debugging and @@ -56,6 +58,8 @@ struct Options { log_level: LevelFilter, } + + #[tokio::main] async fn main() -> Result<(), anyhow::Error> { @@ -85,71 +89,36 @@ async fn main() -> Result<(), anyhow::Error> { // Fetch crate via crates.io, git, or use a local manifest path // TODO: work out which of these to do based on `opts.name` // TODO: support git-based fetches (whole repo name rather than just crate name) - let crate_path = match opts.manifest_path { - Some(p) => { - p - }, - None => { - fetch_crate_cratesio(&opts.name, opts.version.as_deref(), temp_dir.path()).await? - }, + 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?, }; + + debug!("Reading manifest: {}", manifest_path.display()); + let manifest = load_manifest_path(manifest_path.join("Cargo.toml"))?; + let package = manifest.package.unwrap(); - // Read cargo manifest - let manifest_path = crate_path.join("Cargo.toml"); - - debug!("Reading manifest: {}", manifest_path.to_str().unwrap()); - let package = match Manifest::::from_path_with_metadata(&manifest_path) { - Ok(m) => m.package.unwrap(), - Err(e) => { - error!("Error reading manifest '{}': {:?}", manifest_path.to_str().unwrap(), e); - return Err(e.into()); - }, - }; - - let meta = package.metadata; - debug!("Retrieved metadata: {:?}", meta); - - // Select which package path to use - let pkg_url = match meta.as_ref().map(|m| m.pkg_url.clone() ).flatten() { - Some(m) => { - debug!("Using package url: '{}'", &m); - m - }, - _ => { - debug!("No `pkg-url` key found in Cargo.toml or `--pkg-url` argument provided"); - debug!("Using default url: {}", DEFAULT_PKG_PATH); - DEFAULT_PKG_PATH.to_string() - }, - }; - - // Select bin format to use - let pkg_fmt = match meta.as_ref().map(|m| m.pkg_fmt.clone() ).flatten() { - Some(m) => m.clone(), - _ => PkgFmt::Tgz, - }; - - // Override package name if required - let pkg_name = match meta.as_ref().map(|m| m.pkg_name.clone() ).flatten() { - Some(m) => m, - _ => opts.name.clone(), - }; + let (meta, binaries) = ( + package.metadata.map(|m| m.binstall ).flatten().unwrap_or(PkgMeta::default()), + manifest.bin, + ); // Generate context for URL interpolation let ctx = Context { - name: pkg_name.to_string(), + name: opts.name.clone(), repo: package.repository, target: opts.target.clone(), version: package.version.clone(), - format: pkg_fmt.to_string(), + format: meta.pkg_fmt.to_string(), }; debug!("Using context: {:?}", ctx); // Interpolate version / target / etc. - let rendered = ctx.render(&pkg_url)?; + let rendered = ctx.render(&meta.pkg_url)?; // Compute install directory - let install_path = match get_install_path(opts.install_path) { + let install_path = match get_install_path(opts.install_path.as_deref()) { Some(p) => p, None => { error!("No viable install path found of specified, try `--install-path`"); @@ -162,7 +131,7 @@ async fn main() -> Result<(), anyhow::Error> { info!("Downloading package from: '{}'", rendered); // Download package - let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", pkg_name, pkg_fmt)); + let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", opts.name, meta.pkg_fmt)); download(&rendered, pkg_path.to_str().unwrap()).await?; #[cfg(incomplete)] @@ -191,39 +160,52 @@ async fn main() -> Result<(), anyhow::Error> { } // Extract files - let bin_path = temp_dir.path().join(format!("bin-{}", pkg_name)); - extract(&pkg_path, pkg_fmt, &bin_path)?; + let bin_path = temp_dir.path().join(format!("bin-{}", opts.name)); + extract(&pkg_path, meta.pkg_fmt, &bin_path)?; // Bypass cleanup if disabled if opts.no_cleanup { let _ = temp_dir.into_path(); } + if binaries.len() == 0 { + error!("No binaries specified (or inferred from file system)"); + return Err(anyhow::anyhow!("No binaries specified (or inferred from file system)")); + } + // List files to be installed - // TODO: check extracted files are sensible / filter by allowed files - // TODO: this seems overcomplicated / should be able to be simplified? - let bin_files = std::fs::read_dir(&bin_path)?; - let bin_files: Vec<_> = bin_files.filter_map(|f| f.ok() ).map(|f| { - let source = f.path().to_owned(); - let name = source.file_name().map(|v| v.to_str()).flatten().unwrap().to_string(); + // based on those found via Cargo.toml + let bin_files = binaries.iter().map(|p| { + // Fetch binary base name + let base_name = p.name.clone().unwrap(); - // Trim target and version from name if included in binary file name - let base_name = name.replace(&format!("-{}", TARGET), "") - .replace(&format!("-v{}", ctx.version), "") - .replace(&format!("-{}", ctx.version), ""); + // Generate binary path via interpolation + let mut bin_ctx = ctx.clone(); + bin_ctx.name = base_name.clone(); + + // Append .exe to windows binaries + bin_ctx.format = match &opts.target.clone().contains("windows") { + true => ".exe".to_string(), + false => "".to_string(), + }; - // Generate install destination with version suffix - let dest = install_path.join(format!("{}-v{}", base_name, ctx.version)); + // Generate install paths + // Source path is the download dir + the generated binary path + let source_file_path = bin_ctx.render(&meta.bin_dir)?; + let source = bin_path.join(&source_file_path); - // Generate symlink path from base name + // Destination path is the install dir + base-name-version{.format} + let dest_file_path = bin_ctx.render("{ name }-v{ version }{ format }")?; + let dest = install_path.join(dest_file_path); + + // Link at install dir + base name let link = install_path.join(&base_name); - (base_name, source, dest, link) - }).collect(); - + Ok((base_name, source, dest, link)) + }).collect::, anyhow::Error>>()?; // Prompt user for confirmation - info!("This will install the following files:"); + info!("This will install the following binaries:"); for (name, source, dest, _link) in &bin_files { info!(" - {} ({} -> {})", name, source.file_name().unwrap().to_string_lossy(), dest.display()); } From a6c70b41e24e0445134894854b5dff1cb5a42d02 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 31 Dec 2020 14:41:52 +1300 Subject: [PATCH 7/9] the big refactor --- README.md | 132 +++++++++++++++++++++++++++++++++------------------- src/lib.rs | 5 +- src/main.rs | 5 +- 3 files changed, 90 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index f78a4f7e..fc9df705 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,123 @@ # Cargo B(inary)Install -A helper for distributing / installing CI built rust binaries in a pseudo-distributed and maybe-one-day secure manner. -This tool is not intended to manage the _building_ of rust binaries as CI can already readily manage this, but to provide a simple project-level mechanism for the distribution and consumption of rust binary packages. +A helper for distribution and installation of CI built rust binaries in a pseudo-distributed and maybe-one-day secure manner. +This is part experiment, part solving a personal problem, and part hope that we can solve / never re-visit this. I hope you find it helpful and, good luck! -To support `binstall` maintainers must add configuration values to `Cargo.toml` to allow the tool to locate the appropriate CI-produced binary package for a given version and target. For further information on adding `binstall` support, see [Supporting Binary Installation](#Supporting-Binary-Installation) for further detail on supporting `binstall`. +To get started _using_ `cargo-binstall`, first install the binary (either via `cargo install cargo-binstall` or by downloading a precompiled [release](https://github.com/ryankurte/cargo-binstall/releases)). +Once `cargo-binstall` is installed, supported packages can be installed using `cargo binstall NAME` where `NAME` is the crate.io package name. +Package versions and targets may be specified using the `--version` and `--target` arguments respectively, and install directory with `--install-dir` (this defaults to `$HOME/.cargo/bin`, with fall-backs to `$HOME/.bin` if unavailable). For additional options please see `cargo binstall --help`. -For packages with `binstall` support, the command `cargo binstall PACKAGE` will then look-up the crate metadata for the specified (or latest) version, fetch the associated binary package, and install this to `$HOME/.cargo/bin` (or `$HOME/.bin` if this is unavailable). See [Installing Binaries](#Installing-Binaries) for further information on using `binstall`. - -Cargo metadata is used to avoid the need for an additional centralised index or binary repository, and to provide project maintainers with the maximum possible agency for binary distribution with no additional dependencies and minimal additional complexity. This is part experiment, part solving a personal problem, and part hope that we can solve / never re-visit this. I hope you find it helpful and, good luck! +To support `binstall` maintainers must add configuration values to `Cargo.toml` to allow the tool to locate the appropriate CI-produced binary package for a given version and target. See [Supporting Binary Installation](#Supporting-Binary-Installation) for instructions on how to support `binstall` in your projects. ## Status ![Rust](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) [![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) +[![Docs.rs](https://docs.rs/cargo-binstall/badge.svg)](https://docs.rs/cargo-binstal ## Features - Manifest discovery - - [x] Fetch manifest via crates.io - - [ ] Fetch manifest via git - - [x] Use local manifest (`--manifest-path`) + - [x] Fetch crate/manifest via crates.io + - [ ] Fetch crate/manifest via git + - [x] Use local crate/manifest (`--manifest-path`) - Package formats - [x] Tgz - [x] Tar - [x] Bin - Extraction / Transformation - - [ ] Extract from subdirectory in archive (ie. support archives with platform or target subdirectories) - - [ ] Extract specific files from archive (ie. support single archive with multiple platform binaries) + - [x] Extract from subdirectory in archive (ie. support archives with platform or target subdirectories) + - [x] Extract specific files from archive (ie. support single archive with multiple platform binaries) - Security - [ ] Package signing - [ ] Package verification -## Installing Binaries - -First you'll need to install `cargo-binstall` either via `cargo install cargo-binstall` (and it'll have to compile, sorry...), or by grabbing a pre-compiled version from the [releases](https://github.com/ryankurte/cargo-binstall/releases) page and putting that somewhere on your path. It's like there's a problem we're trying to solve? - -Once a project supports `binstall` you can then install binaries via `cargo binstall NAME` where `NAME` is the name of the crate. This will then fetch the metadata for the provided crate, lookup the associated binary file, and download this onto your system. -By default the latest version from is installed, which can be overridden using the `--version` argument, and packages are installed to `$HOME/.cargo/bin` as is consistent with `cargo install`, which can be overridden via the `--install-path` argument. As always `--help` will show available options. - -We hope the defaults will work without configuration in _some_ cases, however, different projects have wildly different CI and build output configurations. You will likely need to add some cargo metadata to support `binstall` in your project, see [Supporting Binary Installation](#Supporting-Binary-Installation) for details. - - ## Supporting Binary Installation -`cargo-binstall` installs binary packages first by reading `[package.metadata]` values from the Cargo manifest (`Cargo.toml`) to discover a template URL, then by building a download path from this template, and finally by downloading and extracting the binary package onto the users path. +`binstall` works with existing CI-built binary outputs, with configuration via `[package.metadata.binstall]` keys in the relevant crate manifest. +When configuring `binstall` you can test against a local manifest with `--manifest-path=PATH` argument to use the crate and manifest at the provided `PATH`, skipping crate discovery and download. -To support `binstall` first you need working CI that places binary outputs at a reasonably deterministic location (github releases, S3 bucket, so long as it's internet accessible you're good to go), then to add configuration values to your Cargo manifest to specify a template string for `binstall` to use when downloading packages. -By default `binstall` will look for pre-built packages using the following template: -``` -{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }`, +By default `binstall` is setup to work with github releases, and expects to find: + +- an archive named `{ name }-{ target }-v{ version }.tgz` + - so that this does not overwrite different targets or versions when manually downloaded +- located at `{ repo }/releases/download/v{ version }/` + - compatible with github tags / releases +- containing a folder named `{ name }-{ target }-v{ version }` + - so that prior binary files are not overwritten when manually executing `tar -xvf ...` +- containing binary files in the form `{ bin }{ format }` (where `bin` is the cargo binary name and `format` is `.exe` on windows and empty on other platforms) + + +These defaults can be overridden using the following configuration keys: + +- `pkg-url` specifies the binary package URL for a given target/version, templated (defaults to: `{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }`) +- `bin-path` specifies the binary path within the package, templated (defaults to: `{ name }-{ target }-v{ version }/{ bin }` with a `.exe` suffix on windows) +- `pkg-fmt` overrides the package format for download/extraction (defaults to: `tgz`) + + +Template variables use the format `{ VAR }` where `VAR` is the name of the variable, with the following variables available: +- `name` is the name of the crate / package +- `version` is the crate version (per `--version` and the crate manifest) +- `repo` is the repository linked in `Cargo.toml` +- `bin` is the name of a specific binary, inferred from the crate configuration +- `target` is the rust target name (defaults to your architecture, but can be overridden using the `--target` command line option if required(). + + +### Operation + +- Lookup a viable crate version (currently via `crates.io`, in future via git tags too) +- Download crate snapshot (currently via `crates.io`) +- Parse configuration metadata and binary information from the downloaded snapshot +- Download and extract binary package using configured URL (`pkg-url`, `pkg-fmt`) +- Install versioned binary files to the relevant install dir +- Generate symlinks to versioned binaries + +### Examples + +For example, the default configuration (if specified in `Cargo.toml`) would be: + +```toml +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }" +bin-dir = "{ name }-{ target }-v{ version }/{ bin }{ format }" +pkg-fmt = "tgz" ``` -Template variables use the format `{ NAME }` where `NAME` is the name of the variable. -`repo`, `name`, and `version` are those specified in the crate manifest (`Cargo.toml`). -`target` defaults to your (the machine calling `cargo binstall`) architecture, but can be overridden using the `--target` command line option if required. -`format` defaults to `tgz` and can be specified via the `pkg-fmt` key under `[package.metadata]`. You may need this if you have sneaky `tgz` files that are actually not gzipped. +For a crate called `radio-sx128x` ( at version `v0.14.1-alpha.5` on x86_64 linux), this would be interpolated to: -For example, for the `radio-sx128x` crate at version `v0.14.1-alpha.5`, this would be interpolated to a download URL of: -``` -https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/rust-radio-sx128x-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz` +- A download URL of `https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/rust-radio-sx128x-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz` +- Containing a single binary file `rust-radio-sx128x-x86_64-unknown-linux-gnu-v0.14.1-alpha.5/rust-radio-x86_64-unknown-linux-gnu` +- Installed to`$HOME/.cargo/bin/rust-radio-sx128x-v0.14.1-alpha.5` +- With a symlink from `$HOME/.cargo/bin/rust-radio-sx128x` + +#### If the package name does not match the crate name + +As is common with libraries / utilities (and the `radio-sx128x` example), this can be overridden by specifying: + +```toml +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ format }" ``` -Custom template URLS may be specified using the `pkg-url` field under `[package.metadata]`, using the same keywords for interpolation as discussed above. Again for the `radio-sx128x` package this could be: +Which provides a download URL of: `https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/sx128x-util-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz` -``` -[package.metadata] -pkg-url = "https://github.com/ryankurte/rust-radio-sx128x/releases/download/v{ version }/radio-sx128x-{ target }-v{ version }.tgz" + +#### If the package structure differs from the default + +While it's nice to have the default format discussed above, often it's not the case... + +Were the package to contain binaries in the form `name-target[.exe]`, this could be specified as: + +```toml +[package.metadata.binstall] +bin-dir = "{ bin }-{ target }{ format }" ``` -If you have separate crate and package names, you can specify a `pkg-name` under `[package.metadata]`, replacing _only_ the `{ name }` field in the default template. -This is useful if you have a library crate with utilities, and the crate and binary package names differ, but can equally well be addressed via defining the `pkg-url` field as described above. -For example, the real-world `ryankurte/radio-sx128x` crate produces a `sx128x-util` package, which can be configured using the following: +Which provides a binary path of: `sx128x-util-x86_64-unknown-linux-gnu[.exe]` (binary names are inferred from the crate, so long as cargo builds them this _should_ just work). -``` -[package.metadata] -pkg-name = "sx128x-util" -``` - -Once you've added a `pkg-name` or a `pkg-url` you should be good to go! For the purposes of testing this integration you may find it useful to manually specify the manifest path using the `--manifest-path=PATH` argument, this skips discovery of the crate manifest and uses the one at the provided `PATH`. For testing purposes you can also override a variety of configuration options on the command line, see `--help` for more details. --- -If anything is not working the way you expect, add a `--log-level debug` to see debug information, and feel free to open an issue or PR. +If you have ideas / contributions or anything is not working the way you expect (in which case, please include an output with `--log-level debug`) and feel free to open an issue or PR. diff --git a/src/lib.rs b/src/lib.rs index 918b3712..b96c9d00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,10 +14,10 @@ pub use drivers::*; pub const TARGET: &'static str = env!("TARGET"); /// Default package path template (may be overridden in package Cargo.toml) -pub const DEFAULT_PKG_URL: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }"; +pub const DEFAULT_PKG_URL: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.tgz"; /// Default binary name template (may be overridden in package Cargo.toml) -pub const DEFAULT_BIN_PATH: &'static str = "{ name }-{ target }-v{ version }/{ name }{ format }"; +pub const DEFAULT_BIN_PATH: &'static str = "{ name }-{ target }-v{ version }/{ bin }{ format }"; /// Binary format enumeration @@ -96,6 +96,7 @@ pub struct Context { pub target: String, pub version: String, pub format: String, + pub bin: Option, } impl Context { diff --git a/src/main.rs b/src/main.rs index a4cb9120..d5ce70f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -110,6 +110,7 @@ async fn main() -> Result<(), anyhow::Error> { target: opts.target.clone(), version: package.version.clone(), format: meta.pkg_fmt.to_string(), + bin: None, }; debug!("Using context: {:?}", ctx); @@ -181,7 +182,7 @@ async fn main() -> Result<(), anyhow::Error> { // Generate binary path via interpolation let mut bin_ctx = ctx.clone(); - bin_ctx.name = base_name.clone(); + bin_ctx.bin = Some(base_name.clone()); // Append .exe to windows binaries bin_ctx.format = match &opts.target.clone().contains("windows") { @@ -195,7 +196,7 @@ async fn main() -> Result<(), anyhow::Error> { let source = bin_path.join(&source_file_path); // Destination path is the install dir + base-name-version{.format} - let dest_file_path = bin_ctx.render("{ name }-v{ version }{ format }")?; + let dest_file_path = bin_ctx.render("{ bin }-v{ version }{ format }")?; let dest = install_path.join(dest_file_path); // Link at install dir + base name From ef6a3d0ef7a47a23cdafbd6fd36f8c1faad61b2e Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 31 Dec 2020 15:32:58 +1300 Subject: [PATCH 8/9] fix version matching, now works with semver --- Cargo.lock | 127 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 10 +--- src/drivers.rs | 95 ++++++++++++++++++++++++++---------- src/main.rs | 23 +++++---- 4 files changed, 210 insertions(+), 45 deletions(-) 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()); From dfb80803fa232c4efd373e69a565f8e88ab4110a Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 31 Dec 2020 15:33:43 +1300 Subject: [PATCH 9/9] revert default pkg url to use format template --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index b96c9d00..0d8cc478 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ pub use drivers::*; pub const TARGET: &'static str = env!("TARGET"); /// Default package path template (may be overridden in package Cargo.toml) -pub const DEFAULT_PKG_URL: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.tgz"; +pub const DEFAULT_PKG_URL: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }"; /// Default binary name template (may be overridden in package Cargo.toml) pub const DEFAULT_BIN_PATH: &'static str = "{ name }-{ target }-v{ version }/{ bin }{ format }";