refactoring to library

This commit is contained in:
ryan 2020-12-30 15:27:39 +13:00
parent 0c72b89627
commit 8f7f7f5530
7 changed files with 263 additions and 184 deletions

1
Cargo.lock generated
View file

@ -130,6 +130,7 @@ dependencies = [
"flate2", "flate2",
"log", "log",
"reqwest", "reqwest",
"semver",
"serde", "serde",
"serde_derive", "serde_derive",
"simplelog", "simplelog",

View file

@ -27,3 +27,5 @@ strum_macros = "0.20.1"
strum = "0.20.0" strum = "0.20.0"
dirs = "3.0.1" dirs = "3.0.1"
serde_derive = "1.0.118" serde_derive = "1.0.118"
#github = "0.1.2"
semver = "0.11.0"

View file

@ -38,7 +38,7 @@ Cargo metadata is used to avoid the need for an additional centralised index or
First you'll need to install `cargo-binstall` either via `cargo install cargo-binstall` (and it'll have to compile, sorry...), or by grabbing a pre-compiled version from the [releases](https://github.com/ryankurte/cargo-binstall/releases) page and putting that somewhere on your path. It's like there's a problem we're trying to solve? First you'll need to install `cargo-binstall` either via `cargo install cargo-binstall` (and it'll have to compile, sorry...), or by grabbing a pre-compiled version from the [releases](https://github.com/ryankurte/cargo-binstall/releases) page and putting that somewhere on your path. It's like there's a problem we're trying to solve?
Once a project supports `binstall` you can then install binaries via `cargo binstall NAME` where `NAME` is the name of the crate. This will then fetch the metadata for the provided crate, lookup the associated binary file, and download this onto your system. Once a project supports `binstall` you can then install binaries via `cargo binstall NAME` where `NAME` is the name of the crate. This will then fetch the metadata for the provided crate, lookup the associated binary file, and download this onto your system.
By default the latest version is installed, which can be overridden using the `--version` argument, and packages are installed to `$HOME/.cargo/bin` as is consistent with `cargo install`, which can be overridden via the `--install-path` argument. As always `--help` will show available options. By default the latest version from is installed, which can be overridden using the `--version` argument, and packages are installed to `$HOME/.cargo/bin` as is consistent with `cargo install`, which can be overridden via the `--install-path` argument. As always `--help` will show available options.
We hope the defaults will work without configuration in _some_ cases, however, different projects have wildly different CI and build output configurations. You will likely need to add some cargo metadata to support `binstall` in your project, see [Supporting Binary Installation](#Supporting-Binary-Installation) for details. We hope the defaults will work without configuration in _some_ cases, however, different projects have wildly different CI and build output configurations. You will likely need to add some cargo metadata to support `binstall` in your project, see [Supporting Binary Installation](#Supporting-Binary-Installation) for details.

80
src/drivers.rs Normal file
View file

@ -0,0 +1,80 @@
use std::time::Duration;
use std::path::{Path, PathBuf};
use log::{debug, error};
use crates_io_api::AsyncClient;
use semver::Version;
use crate::PkgFmt;
use crate::helpers::*;
/// Fetch a crate by name and version from crates.io
pub async fn fetch_crate_cratesio(name: &str, version: Option<&str>, temp_dir: &Path) -> Result<PathBuf, anyhow::Error> {
// Build crates.io api client and fetch info
// TODO: support git-based fetches (whole repo name rather than just crate name)
let api_client = AsyncClient::new("cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100))?;
debug!("Fetching information for crate: '{}'", name);
// Fetch overall crate info
let info = match api_client.get_crate(name.as_ref()).await {
Ok(i) => i,
Err(e) => {
error!("Error fetching information for crate {}: {}", name, e);
return Err(e.into())
}
};
// Use specified or latest version
let version_num = match version {
Some(v) => v.to_string(),
None => info.crate_data.max_version,
};
// Fetch crates.io information for the specified version
// TODO: Filter by semver matches instead of literal match
let mut versions = info.versions.clone();
versions.sort_by(|a, b| {
let ver_a = Version::parse(&a.num).unwrap();
let ver_b = Version::parse(&b.num).unwrap();
ver_a.partial_cmp(&ver_b).unwrap()
} );
let version = match versions.iter().find(|v| v.num == version_num) {
Some(v) => v,
None => {
error!("No crates.io information found for crate: '{}' version: '{}'",
name, version_num);
return Err(anyhow::anyhow!("No crate information found"));
}
};
debug!("Found information for crate version: '{}'", version.num);
// Download crate to temporary dir (crates.io or git?)
let crate_url = format!("https://crates.io/{}", version.dl_path);
let tgz_path = temp_dir.join(format!("{}.tgz", name));
debug!("Fetching crate from: {}", crate_url);
// Download crate
download(&crate_url, &tgz_path).await?;
// Decompress downloaded tgz
debug!("Decompressing crate archive");
extract(&tgz_path, PkgFmt::Tgz, &temp_dir)?;
let crate_path = temp_dir.join(format!("{}-{}", name, version_num));
// Return crate directory
Ok(crate_path)
}
/// Fetch a crate by name and version from github
/// TODO: implement this
pub async fn fetch_crate_gh_releases(_name: &str, _version: Option<&str>, _temp_dir: &Path) -> Result<PathBuf, anyhow::Error> {
unimplemented!();
}

65
src/helpers.rs Normal file
View file

@ -0,0 +1,65 @@
use std::path::Path;
use log::{debug, error};
use flate2::read::GzDecoder;
use tar::Archive;
use super::PkgFmt;
/// 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> {
debug!("Downloading from: '{}'", url);
let resp = reqwest::get(url).await?;
if !resp.status().is_success() {
error!("Download error: {}", resp.status());
return Err(anyhow::anyhow!(resp.status()));
}
let bytes = resp.bytes().await?;
debug!("Download OK, writing to file: '{:?}'", path.as_ref());
std::fs::write(&path, bytes)?;
Ok(())
}
/// Extract files from the specified source onto the specified path
pub fn extract<S: AsRef<Path>, P: AsRef<Path>>(source: S, fmt: PkgFmt, path: P) -> Result<(), anyhow::Error> {
match fmt {
PkgFmt::Tar => {
// Extract to install dir
debug!("Extracting from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref());
let dat = std::fs::File::open(source)?;
let mut tar = Archive::new(dat);
tar.unpack(path)?;
},
PkgFmt::Tgz => {
// Extract to install dir
debug!("Decompressing from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref());
let dat = std::fs::File::open(source)?;
let tar = GzDecoder::new(dat);
let mut tgz = Archive::new(tar);
tgz.unpack(path)?;
},
PkgFmt::Bin => {
debug!("Copying data from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref());
// Copy to install dir
std::fs::copy(source, path)?;
},
};
Ok(())
}

75
src/lib.rs Normal file
View file

@ -0,0 +1,75 @@
use structopt::StructOpt;
use serde::{Serialize, Deserialize};
use strum_macros::{Display, EnumString, EnumVariantNames};
use tinytemplate::TinyTemplate;
pub mod helpers;
pub use helpers::*;
pub mod drivers;
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
pub const DEFAULT_PKG_PATH: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }";
/// Binary format enumeration
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Display, EnumString, EnumVariantNames)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum PkgFmt {
/// Download format is TAR (uncompressed)
Tar,
/// Download format is TGZ (TAR + GZip)
Tgz,
/// Download format is raw / binary
Bin,
}
/// 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 binary downloads
pub pkg_url: Option<String>,
/// Package name override for binary downloads
pub pkg_name: Option<String>,
/// Format override for binary downloads
pub pkg_fmt: Option<PkgFmt>,
}
/// 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: PkgFmt,
}
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)
}
}

View file

@ -1,62 +1,57 @@
use std::time::Duration; use std::path::{PathBuf};
use std::path::{PathBuf, Path};
use log::{debug, info, error, LevelFilter}; use log::{debug, info, error, LevelFilter};
use simplelog::{TermLogger, ConfigBuilder, TerminalMode}; use simplelog::{TermLogger, ConfigBuilder, TerminalMode};
use structopt::StructOpt; use structopt::StructOpt;
use serde::{Serialize, Deserialize};
use crates_io_api::AsyncClient;
use cargo_toml::Manifest; use cargo_toml::Manifest;
use tempdir::TempDir; use tempdir::TempDir;
use flate2::read::GzDecoder;
use tar::Archive;
use tinytemplate::TinyTemplate; use cargo_binstall::*;
/// Compiled target triple, used as default for binary fetching
const TARGET: &'static str = env!("TARGET");
/// Default binary path for use if no path is specified
const DEFAULT_BIN_PATH: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }";
/// Binary format enumeration
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[derive(strum_macros::Display, strum_macros::EnumString, strum_macros::EnumVariantNames)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum PkgFmt {
/// Download format is TAR (uncompressed)
Tar,
/// Download format is TGZ (TAR + GZip)
Tgz,
/// Download format is raw / binary
Bin,
}
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
struct Options { struct Options {
/// Crate name to install /// Package name or URL for installation
/// This must be either a crates.io package name or github or gitlab url
#[structopt()] #[structopt()]
name: String, name: String,
/// Crate version to install /// Package version to instal
#[structopt(long)] #[structopt(long)]
version: Option<String>, version: Option<String>,
/// Override the package path template. /// Override binary target, ignoring compiled version
/// If no `metadata.pkg_url` key is set or `--pkg-url` argument provided, this #[structopt(long, default_value = TARGET)]
/// defaults to `{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.tgz` target: String,
#[structopt(long)]
pkg_url: Option<String>,
/// Override format for binary file download. /// Override format for binary file download.
/// Defaults to `tgz` /// Defaults to `tgz`
#[structopt(long)] #[structopt(long)]
pkg_fmt: Option<PkgFmt>, pkg_fmt: Option<PkgFmt>,
/// Override install path for downloaded binary.
/// Defaults to `$HOME/.cargo/bin`
#[structopt(long)]
install_path: Option<String>,
#[structopt(flatten)]
overrides: Overrides,
/// Do not cleanup temporary files on success
#[structopt(long)]
no_cleanup: bool,
/// Utility log level
#[structopt(long, default_value = "info")]
log_level: LevelFilter,
}
#[derive(Debug, StructOpt)]
pub struct Overrides {
/// Override the package name. /// Override the package name.
/// This is only useful for diagnostics when using the default `pkg_url` /// This is only useful for diagnostics when using the default `pkg_url`
/// as you can otherwise customise this in the path. /// as you can otherwise customise this in the path.
@ -64,53 +59,20 @@ struct Options {
#[structopt(long)] #[structopt(long)]
pkg_name: Option<String>, pkg_name: Option<String>,
/// Override install path for downloaded binary. /// Override the package path template.
/// Defaults to `$HOME/.cargo/bin` /// If no `metadata.pkg_url` key is set or `--pkg-url` argument provided, this
/// defaults to `{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.tgz`
#[structopt(long)] #[structopt(long)]
install_path: Option<String>, pkg_url: Option<String>,
/// Override binary target, ignoring compiled version
#[structopt(long, default_value = TARGET)]
target: String,
/// Override manifest source. /// Override manifest source.
/// This skips searching crates.io for a manifest and uses /// This skips searching crates.io for a manifest and uses
/// the specified path directly, useful for debugging /// the specified path directly, useful for debugging
#[structopt(long)] #[structopt(long)]
manifest_path: Option<PathBuf>, manifest_path: Option<PathBuf>,
/// Utility log level
#[structopt(long, default_value = "info")]
log_level: LevelFilter,
/// Do not cleanup temporary files on success
#[structopt(long)]
no_cleanup: bool,
} }
/// Metadata for cargo-binstall exposed via cargo.toml
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Meta {
/// Path template override for binary downloads
pub pkg_url: Option<String>,
/// Package name override for binary downloads
pub pkg_name: Option<String>,
/// Format override for binary downloads
pub pkg_fmt: Option<PkgFmt>,
}
/// Template for constructing download paths
#[derive(Clone, Debug, Serialize)]
pub struct Context {
name: String,
repo: Option<String>,
target: String,
version: String,
format: PkgFmt,
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), anyhow::Error> { async fn main() -> Result<(), anyhow::Error> {
@ -138,7 +100,7 @@ async fn main() -> Result<(), anyhow::Error> {
// Fetch crate via crates.io, git, or use a local manifest path // 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: work out which of these to do based on `opts.name`
let crate_path = match opts.manifest_path { let crate_path = match opts.overrides.manifest_path {
Some(p) => p, Some(p) => p,
None => fetch_crate_cratesio(&opts.name, opts.version.as_deref(), temp_dir.path()).await?, None => fetch_crate_cratesio(&opts.name, opts.version.as_deref(), temp_dir.path()).await?,
}; };
@ -158,8 +120,8 @@ async fn main() -> Result<(), anyhow::Error> {
let meta = package.metadata; let meta = package.metadata;
debug!("Retrieved metadata: {:?}", meta); debug!("Retrieved metadata: {:?}", meta);
// Select which binary path to use // Select which package path to use
let pkg_url = match (opts.pkg_url, meta.as_ref().map(|m| m.pkg_url.clone() ).flatten()) { let pkg_url = match (opts.overrides.pkg_url, meta.as_ref().map(|m| m.pkg_url.clone() ).flatten()) {
(Some(p), _) => { (Some(p), _) => {
info!("Using package url override: '{}'", p); info!("Using package url override: '{}'", p);
p p
@ -170,8 +132,8 @@ async fn main() -> Result<(), anyhow::Error> {
}, },
_ => { _ => {
info!("No `pkg-url` key found in Cargo.toml or `--pkg-url` argument provided"); info!("No `pkg-url` key found in Cargo.toml or `--pkg-url` argument provided");
info!("Using default url: {}", DEFAULT_BIN_PATH); info!("Using default url: {}", DEFAULT_PKG_PATH);
DEFAULT_BIN_PATH.to_string() DEFAULT_PKG_PATH.to_string()
}, },
}; };
@ -183,7 +145,7 @@ async fn main() -> Result<(), anyhow::Error> {
}; };
// Override package name if required // Override package name if required
let pkg_name = match (&opts.pkg_name, meta.as_ref().map(|m| m.pkg_name.clone() ).flatten()) { let pkg_name = match (&opts.overrides.pkg_name, meta.as_ref().map(|m| m.pkg_name.clone() ).flatten()) {
(Some(o), _) => o.clone(), (Some(o), _) => o.clone(),
(_, Some(m)) => m, (_, Some(m)) => m,
_ => opts.name.clone(), _ => opts.name.clone(),
@ -201,9 +163,7 @@ async fn main() -> Result<(), anyhow::Error> {
debug!("Using context: {:?}", ctx); debug!("Using context: {:?}", ctx);
// Interpolate version / target / etc. // Interpolate version / target / etc.
let mut tt = TinyTemplate::new(); let rendered = ctx.render(&pkg_url)?;
tt.add_template("path", &pkg_url)?;
let rendered = tt.render("path", &ctx)?;
info!("Downloading package from: '{}'", rendered); info!("Downloading package from: '{}'", rendered);
@ -238,111 +198,7 @@ async fn main() -> Result<(), anyhow::Error> {
Ok(()) Ok(())
} }
/// Download a file from the provided URL to the provided path
async fn download<P: AsRef<Path>>(url: &str, path: P) -> Result<(), anyhow::Error> {
debug!("Downloading from: '{}'", url);
let resp = reqwest::get(url).await?;
if !resp.status().is_success() {
error!("Download error: {}", resp.status());
return Err(anyhow::anyhow!(resp.status()));
}
let bytes = resp.bytes().await?;
debug!("Download OK, writing to file: '{:?}'", path.as_ref());
std::fs::write(&path, bytes)?;
Ok(())
}
fn extract<S: AsRef<Path>, P: AsRef<Path>>(source: S, fmt: PkgFmt, path: P) -> Result<(), anyhow::Error> {
match fmt {
PkgFmt::Tar => {
// Extract to install dir
debug!("Extracting from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref());
let dat = std::fs::File::open(source)?;
let mut tar = Archive::new(dat);
tar.unpack(path)?;
},
PkgFmt::Tgz => {
// Extract to install dir
debug!("Decompressing from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref());
let dat = std::fs::File::open(source)?;
let tar = GzDecoder::new(dat);
let mut tgz = Archive::new(tar);
tgz.unpack(path)?;
},
PkgFmt::Bin => {
debug!("Copying data from archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref());
// Copy to install dir
std::fs::copy(source, path)?;
},
};
Ok(())
}
/// Fetch a crate by name and version from crates.io
async fn fetch_crate_cratesio(name: &str, version: Option<&str>, temp_dir: &Path) -> Result<PathBuf, anyhow::Error> {
// Build crates.io api client and fetch info
// TODO: support git-based fetches (whole repo name rather than just crate name)
let api_client = AsyncClient::new("cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100))?;
info!("Fetching information for crate: '{}'", name);
// Fetch overall crate info
let info = match api_client.get_crate(name.as_ref()).await {
Ok(i) => i,
Err(e) => {
error!("Error fetching information for crate {}: {}", name, e);
return Err(e.into())
}
};
// Use specified or latest version
let version_num = match version {
Some(v) => v.to_string(),
None => info.crate_data.max_version,
};
// Fetch crates.io information for the specified version
// TODO: could do a semver match and sort here?
let version = match info.versions.iter().find(|v| v.num == version_num) {
Some(v) => v,
None => {
error!("No crates.io information found for crate: '{}' version: '{}'",
name, version_num);
return Err(anyhow::anyhow!("No crate information found"));
}
};
info!("Found information for crate version: '{}'", version.num);
// Download crate to temporary dir (crates.io or git?)
let crate_url = format!("https://crates.io/{}", version.dl_path);
let tgz_path = temp_dir.join(format!("{}.tgz", name));
debug!("Fetching crate from: {}", crate_url);
// Download crate
download(&crate_url, &tgz_path).await?;
// Decompress downloaded tgz
debug!("Decompressing crate archive");
extract(&tgz_path, PkgFmt::Tgz, &temp_dir)?;
let crate_path = temp_dir.join(format!("{}-{}", name, version_num));
// Return crate directory
Ok(crate_path)
}
/// Fetch install path /// Fetch install path
/// roughly follows https://doc.rust-lang.org/cargo/commands/cargo-install.html#description /// roughly follows https://doc.rust-lang.org/cargo/commands/cargo-install.html#description