mirror of
https://github.com/cargo-bins/cargo-binstall.git
synced 2025-06-15 07:06:36 +00:00
Refactor fetch and install to be more self-contained
This commit is contained in:
parent
81cf2fc526
commit
b584038d0d
8 changed files with 268 additions and 116 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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
106
src/bins.rs
Normal 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
30
src/fetchers.rs
Normal 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,
|
||||
}
|
50
src/fetchers/gh_release.rs
Normal file
50
src/fetchers/gh_release.rs
Normal 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> {}
|
|
@ -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)?)
|
||||
}
|
||||
}
|
32
src/lib.rs
32
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<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};
|
||||
|
|
132
src/main.rs
132
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::<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(())
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue