From b584038d0d43b3930ebdaea9b9dfc169b35fec86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Saparelli?= Date: Tue, 15 Feb 2022 23:58:16 +1300 Subject: [PATCH] Refactor fetch and install to be more self-contained --- Cargo.lock | 12 ++++ Cargo.toml | 1 + src/bins.rs | 106 +++++++++++++++++++++++++++++ src/fetchers.rs | 30 +++++++++ src/fetchers/gh_release.rs | 50 ++++++++++++++ src/helpers.rs | 21 ++++++ src/lib.rs | 32 +-------- src/main.rs | 132 +++++++++++++------------------------ 8 files changed, 268 insertions(+), 116 deletions(-) create mode 100644 src/bins.rs create mode 100644 src/fetchers.rs create mode 100644 src/fetchers/gh_release.rs diff --git a/Cargo.lock b/Cargo.lock index 2af949be..b347dafa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,17 @@ version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" +[[package]] +name = "async-trait" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -114,6 +125,7 @@ name = "cargo-binstall" version = "0.5.0" dependencies = [ "anyhow", + "async-trait", "cargo_metadata", "cargo_toml", "crates-index", diff --git a/Cargo.toml b/Cargo.toml index 8e3cafd4..5eccc5d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ crates-index = "0.18.1" semver = "1.0.4" xz2 = "0.1.6" zip = "0.5.13" +async-trait = "0.1.52" [dev-dependencies] env_logger = "0.9.0" diff --git a/src/bins.rs b/src/bins.rs new file mode 100644 index 00000000..26c33865 --- /dev/null +++ b/src/bins.rs @@ -0,0 +1,106 @@ +use std::path::PathBuf; + +use cargo_toml::Product; +use serde::Serialize; + +use crate::{Template, PkgFmt, PkgMeta}; + +pub struct BinFile { + pub base_name: String, + pub source: PathBuf, + pub dest: PathBuf, + pub link: PathBuf, +} + +impl BinFile { + pub fn from_product(data: &Data, product: &Product) -> Result { + let base_name = product.name.clone().unwrap(); + + // Generate binary path via interpolation + let ctx = Context { + name: &data.name, + repo: data.repo.as_ref().map(|s| &s[..]), + target: &data.target, + version: &data.version, + format: if data.target.contains("windows") { ".exe" } else { "" }, + bin: &base_name, + }; + + // Generate install paths + // Source path is the download dir + the generated binary path + let source_file_path = ctx.render(&data.meta.bin_dir)?; + let source = if data.meta.pkg_fmt == PkgFmt::Bin { + data.bin_path.clone() + } else { + data.bin_path.join(&source_file_path) + }; + + // Destination path is the install dir + base-name-version{.format} + let dest_file_path = ctx.render("{ bin }-v{ version }{ format }")?; + let dest = data.install_path.join(dest_file_path); + + // Link at install dir + base name + let link = data.install_path.join(&base_name); + + Ok(Self { base_name, source, dest, link }) + } + + pub fn preview_bin(&self) -> String { + format!("{} ({} -> {})", self.base_name, self.source.file_name().unwrap().to_string_lossy(), self.dest.display()) + } + + pub fn preview_link(&self) -> String { + format!("{} ({} -> {})", self.base_name, self.dest.display(), self.link.display()) + } + + pub fn install_bin(&self) -> Result<(), anyhow::Error> { + // TODO: check if file already exists + std::fs::copy(&self.source, &self.dest)?; + + #[cfg(target_family = "unix")] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&self.dest, std::fs::Permissions::from_mode(0o755))?; + } + + Ok(()) + } + + pub fn install_link(&self) -> Result<(), anyhow::Error> { + // Remove existing symlink + // TODO: check if existing symlink is correct + if self.link.exists() { + std::fs::remove_file(&self.link)?; + } + + #[cfg(target_family = "unix")] + std::os::unix::fs::symlink(&self.dest, &self.link)?; + #[cfg(target_family = "windows")] + std::os::windows::fs::symlink_file(&self.dest, &self.link)?; + + Ok(()) + } +} + +/// Data required to get bin paths +pub struct Data { + pub name: String, + pub target: String, + pub version: String, + pub repo: Option, + pub meta: PkgMeta, + pub bin_path: PathBuf, + pub install_path: PathBuf, +} + +#[derive(Clone, Debug, Serialize)] +struct Context<'c> { + pub name: &'c str, + pub repo: Option<&'c str>, + pub target: &'c str, + pub version: &'c str, + pub format: &'c str, + pub bin: &'c str, +} + +impl<'c> Template for Context<'c> {} \ No newline at end of file diff --git a/src/fetchers.rs b/src/fetchers.rs new file mode 100644 index 00000000..85a29a51 --- /dev/null +++ b/src/fetchers.rs @@ -0,0 +1,30 @@ +use std::path::Path; + +pub use gh_release::*; + +use crate::PkgMeta; + +mod gh_release; + +#[async_trait::async_trait] +pub trait Fetcher { + /// Create a new fetcher from some data + async fn new(data: &Data) -> Result + where + Self: std::marker::Sized; + + /// Fetch a package + async fn fetch(&self, dst: &Path) -> Result<(), anyhow::Error>; + + /// Check if a package is available for download + async fn check(&self) -> Result; +} + +/// Data required to fetch a package +pub struct Data { + pub name: String, + pub target: String, + pub version: String, + pub repo: Option, + pub meta: PkgMeta, +} \ No newline at end of file diff --git a/src/fetchers/gh_release.rs b/src/fetchers/gh_release.rs new file mode 100644 index 00000000..17bf4a79 --- /dev/null +++ b/src/fetchers/gh_release.rs @@ -0,0 +1,50 @@ +use std::path::Path; + +use log::{debug, info}; +use serde::Serialize; + +use crate::{download, head, Template}; +use super::Data; + +pub struct GhRelease { + url: String, +} + +#[async_trait::async_trait] +impl super::Fetcher for GhRelease { + async fn new(data: &Data) -> Result { + // Generate context for URL interpolation + let ctx = Context { + name: &data.name, + repo: data.repo.as_ref().map(|s| &s[..]), + target: &data.target, + version: &data.version, + format: data.meta.pkg_fmt.to_string(), + }; + debug!("Using context: {:?}", ctx); + + Ok(Self { url: ctx.render(&data.meta.pkg_url)? }) + } + + async fn check(&self) -> Result { + info!("Checking for package at: '{}'", self.url); + head(&self.url).await + } + + async fn fetch(&self, dst: &Path) -> Result<(), anyhow::Error> { + info!("Downloading package from: '{}'", self.url); + download(&self.url, dst).await + } +} + +/// Template for constructing download paths +#[derive(Clone, Debug, Serialize)] +struct Context<'c> { + pub name: &'c str, + pub repo: Option<&'c str>, + pub target: &'c str, + pub version: &'c str, + pub format: String, +} + +impl<'c> Template for Context<'c> {} \ No newline at end of file diff --git a/src/helpers.rs b/src/helpers.rs index dce5c9f3..216fa9fe 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -5,7 +5,9 @@ use log::{debug, info, error}; use cargo_toml::{Manifest}; use flate2::read::GzDecoder; +use serde::Serialize; use tar::Archive; +use tinytemplate::TinyTemplate; use xz2::read::XzDecoder; use zip::read::ZipArchive; @@ -24,6 +26,11 @@ pub fn load_manifest_path>(manifest_path: P) -> Result Result { + let req = reqwest::Client::new().head(url).send().await?; + Ok(req.status().is_success()) +} + /// Download a file from the provided URL to the provided path pub async fn download>(url: &str, path: P) -> Result<(), anyhow::Error> { @@ -149,3 +156,17 @@ pub fn confirm() -> Result { } } } + +pub trait Template: Serialize { + fn render(&self, template: &str) -> Result + where Self: Sized { + // Create template instance + let mut tt = TinyTemplate::new(); + + // Add template to instance + tt.add_template("path", &template)?; + + // Render output + Ok(tt.render("path", self)?) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index b8564f38..bd668427 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,6 @@ use std::collections::HashMap; use serde::{Serialize, Deserialize}; use strum_macros::{Display, EnumString, EnumVariantNames}; -use tinytemplate::TinyTemplate; - pub mod helpers; pub use helpers::*; @@ -12,6 +10,9 @@ pub use helpers::*; pub mod drivers; pub use drivers::*; +pub mod bins; +pub mod fetchers; + /// Compiled target triple, used as default for binary fetching pub const TARGET: &'static str = env!("TARGET"); @@ -141,33 +142,6 @@ pub struct BinMeta { pub path: String, } -/// 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: String, - pub bin: Option, -} - -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) - } -} - #[cfg(test)] mod test { use crate::{load_manifest_path}; diff --git a/src/main.rs b/src/main.rs index 6f7cf72e..7b809684 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use structopt::StructOpt; use tempdir::TempDir; -use cargo_binstall::*; +use cargo_binstall::{*, fetchers::{GhRelease, Data, Fetcher}, bins}; #[derive(Debug, StructOpt)] @@ -109,37 +109,34 @@ async fn main() -> Result<(), anyhow::Error> { debug!("Found metadata: {:?}", meta); - // Generate context for URL interpolation - let ctx = Context { - name: opts.name.clone(), - repo: package.repository, - target: opts.target.clone(), - version: package.version.clone(), - format: meta.pkg_fmt.to_string(), - bin: None, - }; - - debug!("Using context: {:?}", ctx); - - // Interpolate version / target / etc. - let rendered = ctx.render(&meta.pkg_url)?; - // Compute install directory - 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`"); - return Err(anyhow::anyhow!("No install path found or specified")); - } - }; - + let install_path = get_install_path(opts.install_path.as_deref()).ok_or_else(|| { + error!("No viable install path found of specified, try `--install-path`"); + anyhow::anyhow!("No install path found or specified") + })?; debug!("Using install path: {}", install_path.display()); - info!("Downloading package from: '{}'", rendered); + // Compute temporary directory for downloads + let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", opts.name, meta.pkg_fmt)); + debug!("Using temporary download path: {}", pkg_path.display()); + + let fetcher_data = Data { + name: package.name.clone(), + target: opts.target.clone(), + version: package.version.clone(), + repo: package.repository.clone(), + meta: meta.clone(), + }; + + // Try github releases + let gh = GhRelease::new(&fetcher_data).await?; + if !gh.check().await? { + error!("No file found in github releases, cannot proceed"); + return Err(anyhow::anyhow!("No viable remote package found")); + } // Download package - let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", opts.name, meta.pkg_fmt)); - download(&rendered, pkg_path.to_str().unwrap()).await?; + gh.fetch(&pkg_path).await?; #[cfg(incomplete)] { @@ -182,49 +179,30 @@ async fn main() -> Result<(), anyhow::Error> { // List files to be installed // 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(); + let bin_data = bins::Data { + name: package.name.clone(), + target: opts.target.clone(), + version: package.version.clone(), + repo: package.repository.clone(), + meta, + bin_path, + install_path, + }; - // Generate binary path via interpolation - let mut bin_ctx = ctx.clone(); - bin_ctx.bin = Some(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 paths - // Source path is the download dir + the generated binary path - let source_file_path = bin_ctx.render(&meta.bin_dir)?; - let source = if meta.pkg_fmt == PkgFmt::Bin { - bin_path.clone() - } else { - bin_path.join(&source_file_path) - }; - - // Destination path is the install dir + base-name-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 - let link = install_path.join(&base_name); - - Ok((base_name, source, dest, link)) - }).collect::, anyhow::Error>>()?; + let bin_files = binaries.iter() + .map(|p| bins::BinFile::from_product(&bin_data, p)) + .collect::, anyhow::Error>>()?; // Prompt user for confirmation 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()); + for file in &bin_files { + info!(" - {}", file.preview_bin()); } 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()); + for file in &bin_files { + info!(" - {}", file.preview_link()); } } @@ -234,37 +212,17 @@ async fn main() -> Result<(), anyhow::Error> { } info!("Installing binaries..."); - - // Install binaries - for (_name, source, dest, _link) in &bin_files { - // TODO: check if file already exists - std::fs::copy(source, dest)?; - - #[cfg(target_family = "unix")] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(dest, std::fs::Permissions::from_mode(0o755))?; - } + for file in &bin_files { + file.install_bin()?; } // 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)?; + for file in &bin_files { + file.install_link()?; } } info!("Installation complete!"); - Ok(()) -} - +} \ No newline at end of file