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