mirror of
https://github.com/cargo-bins/cargo-binstall.git
synced 2025-05-07 20:50:03 +00:00
QuickInstall support (#94)
See this issue: https://github.com/alsuren/cargo-quickinstall/issues/27 Quick Install is a hosted repo of built crates, essentially. The approach I've taken here is a list of strategies: 1. First, we check the crate meta or default and build the URL to the repo. Once we have that, we perform a `HEAD` request to the URL to see if it's available. 2. If it's not, we build the URL to the quickinstall repo, and perform a `HEAD` to there. As soon as we've got a hit, we use that. I've built it so it's extensible with more strategies. This could be useful for #4. This also adds a prompt before downloading from third-party sources, and logs a short name for a source, which is easier to glance than a full URL, and includes a quick refactor of the install/link machinery.
This commit is contained in:
parent
e691255650
commit
370ae05620
12 changed files with 600 additions and 209 deletions
197
src/main.rs
197
src/main.rs
|
@ -1,14 +1,17 @@
|
|||
use std::path::{PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use log::{debug, info, warn, error, LevelFilter};
|
||||
use simplelog::{TermLogger, ConfigBuilder, TerminalMode, ColorChoice};
|
||||
use log::{debug, error, info, warn, LevelFilter};
|
||||
use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode};
|
||||
|
||||
use structopt::StructOpt;
|
||||
|
||||
use tempdir::TempDir;
|
||||
|
||||
use cargo_binstall::*;
|
||||
|
||||
use cargo_binstall::{
|
||||
bins,
|
||||
fetchers::{Data, Fetcher, GhCrateMeta, MultiFetcher, QuickInstall},
|
||||
*,
|
||||
};
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
struct Options {
|
||||
|
@ -48,7 +51,7 @@ struct Options {
|
|||
|
||||
/// Override manifest source.
|
||||
/// This skips searching crates.io for a manifest and uses
|
||||
/// the specified path directly, useful for debugging and
|
||||
/// the specified path directly, useful for debugging and
|
||||
/// when adding `binstall` support.
|
||||
#[structopt(long)]
|
||||
manifest_path: Option<PathBuf>,
|
||||
|
@ -58,10 +61,8 @@ struct Options {
|
|||
log_level: LevelFilter,
|
||||
}
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
|
||||
// Filter extraneous arg when invoked by cargo
|
||||
// `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"]
|
||||
// `cargo binstall --help` gives ["/home/ryan/.cargo/bin/cargo-binstall", "binstall", "--help"]
|
||||
|
@ -78,7 +79,13 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
log_config.add_filter_ignore("hyper".to_string());
|
||||
log_config.add_filter_ignore("reqwest".to_string());
|
||||
log_config.set_location_level(LevelFilter::Off);
|
||||
TermLogger::init(opts.log_level, log_config.build(), TerminalMode::Mixed, ColorChoice::Auto).unwrap();
|
||||
TermLogger::init(
|
||||
opts.log_level,
|
||||
log_config.build(),
|
||||
TerminalMode::Mixed,
|
||||
ColorChoice::Auto,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Create a temporary directory for downloads etc.
|
||||
let temp_dir = TempDir::new("cargo-binstall")?;
|
||||
|
@ -92,13 +99,17 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
Some(p) => p,
|
||||
None => fetch_crate_cratesio(&opts.name, &opts.version, temp_dir.path()).await?,
|
||||
};
|
||||
|
||||
|
||||
debug!("Reading manifest: {}", manifest_path.display());
|
||||
let manifest = load_manifest_path(manifest_path.join("Cargo.toml"))?;
|
||||
let package = manifest.package.unwrap();
|
||||
|
||||
let (mut meta, binaries) = (
|
||||
package.metadata.map(|m| m.binstall ).flatten().unwrap_or(PkgMeta::default()),
|
||||
package
|
||||
.metadata
|
||||
.map(|m| m.binstall)
|
||||
.flatten()
|
||||
.unwrap_or(PkgMeta::default()),
|
||||
manifest.bin,
|
||||
);
|
||||
|
||||
|
@ -109,42 +120,61 @@ 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, then quickinstall
|
||||
let mut fetchers = MultiFetcher::default();
|
||||
fetchers.add(GhCrateMeta::new(&fetcher_data).await?);
|
||||
fetchers.add(QuickInstall::new(&fetcher_data).await?);
|
||||
|
||||
let fetcher = fetchers.first_available().await.ok_or_else(|| {
|
||||
error!("File does not exist remotely, cannot proceed");
|
||||
anyhow::anyhow!("No viable remote package found")
|
||||
})?;
|
||||
|
||||
// Prompt user for third-party source
|
||||
if fetcher.is_third_party() {
|
||||
warn!(
|
||||
"The package will be downloaded from third-party source {}",
|
||||
fetcher.source_name()
|
||||
);
|
||||
if !opts.no_confirm && !confirm()? {
|
||||
warn!("Installation cancelled");
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
"The package will be downloaded from {}",
|
||||
fetcher.source_name()
|
||||
);
|
||||
}
|
||||
|
||||
// Download package
|
||||
let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", opts.name, meta.pkg_fmt));
|
||||
download(&rendered, pkg_path.to_str().unwrap()).await?;
|
||||
fetcher.fetch(&pkg_path).await?;
|
||||
|
||||
#[cfg(incomplete)]
|
||||
{
|
||||
// Fetch and check package signature if available
|
||||
if let Some(pub_key) = meta.as_ref().map(|m| m.pub_key.clone() ).flatten() {
|
||||
if let Some(pub_key) = meta.as_ref().map(|m| m.pub_key.clone()).flatten() {
|
||||
debug!("Found public key: {}", pub_key);
|
||||
|
||||
// Generate signature file URL
|
||||
|
@ -160,7 +190,6 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
|
||||
// TODO: do the signature check
|
||||
unimplemented!()
|
||||
|
||||
} else {
|
||||
warn!("No public key found, package signature could not be validated");
|
||||
}
|
||||
|
@ -168,7 +197,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
|
||||
// Extract files
|
||||
let bin_path = temp_dir.path().join(format!("bin-{}", opts.name));
|
||||
extract(&pkg_path, meta.pkg_fmt, &bin_path)?;
|
||||
extract(&pkg_path, fetcher.pkg_fmt(), &bin_path)?;
|
||||
|
||||
// Bypass cleanup if disabled
|
||||
if opts.no_cleanup {
|
||||
|
@ -177,94 +206,58 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
|
||||
if binaries.len() == 0 {
|
||||
error!("No binaries specified (or inferred from file system)");
|
||||
return Err(anyhow::anyhow!("No binaries specified (or inferred from file system)"));
|
||||
return Err(anyhow::anyhow!(
|
||||
"No binaries specified (or inferred from file system)"
|
||||
));
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.no_confirm && !confirm()? {
|
||||
warn!("Installation cancelled");
|
||||
return Ok(())
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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!");
|
||||
|
||||
info!("Installation complete!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue