Refactor fetch and install to be more self-contained

This commit is contained in:
Félix Saparelli 2022-02-15 23:58:16 +13:00
parent 81cf2fc526
commit b584038d0d
No known key found for this signature in database
GPG key ID: B948C4BAE44FC474
8 changed files with 268 additions and 116 deletions

12
Cargo.lock generated
View file

@ -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",

View file

@ -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"

106
src/bins.rs Normal file
View file

@ -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<Self, anyhow::Error> {
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<String>,
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> {}

30
src/fetchers.rs Normal file
View file

@ -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<Self, anyhow::Error>
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<bool, anyhow::Error>;
}
/// Data required to fetch a package
pub struct Data {
pub name: String,
pub target: String,
pub version: String,
pub repo: Option<String>,
pub meta: PkgMeta,
}

View file

@ -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<Self, anyhow::Error> {
// 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<bool, anyhow::Error> {
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> {}

View file

@ -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<P: AsRef<Path>>(manifest_path: P) -> Result<Manifest<M
Ok(manifest)
}
pub async fn head(url: &str) -> Result<bool, anyhow::Error> {
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<P: AsRef<Path>>(url: &str, path: P) -> Result<(), anyhow::Error> {
@ -149,3 +156,17 @@ pub fn confirm() -> Result<bool, anyhow::Error> {
}
}
}
pub trait Template: Serialize {
fn render(&self, template: &str) -> Result<String, anyhow::Error>
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)?)
}
}

View file

@ -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<String>,
pub target: String,
pub version: String,
pub format: String,
pub bin: Option<String>,
}
impl Context {
/// Render the context into the provided template
pub fn render(&self, template: &str) -> Result<String, anyhow::Error> {
// 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};

View file

@ -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::<Result<Vec<_>, anyhow::Error>>()?;
let bin_files = binaries.iter()
.map(|p| bins::BinFile::from_product(&bin_data, p))
.collect::<Result<Vec<_>, 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(())
}