From 5fd78341c8ee7c6774c4093a17104a0f6feff6f3 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 31 Dec 2020 13:19:58 +1300 Subject: [PATCH] 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()); }