mirror of
https://github.com/cargo-bins/cargo-binstall.git
synced 2025-04-24 22:30: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
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -32,6 +32,17 @@ version = "1.0.53"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
|
checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
|
@ -114,6 +125,7 @@ name = "cargo-binstall"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
"cargo_metadata",
|
"cargo_metadata",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
"crates-index",
|
"crates-index",
|
||||||
|
@ -133,6 +145,7 @@ dependencies = [
|
||||||
"tempdir",
|
"tempdir",
|
||||||
"tinytemplate",
|
"tinytemplate",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"url",
|
||||||
"xz2",
|
"xz2",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
|
@ -40,6 +40,8 @@ crates-index = "0.18.5"
|
||||||
semver = "1.0.5"
|
semver = "1.0.5"
|
||||||
xz2 = "0.1.6"
|
xz2 = "0.1.6"
|
||||||
zip = "0.5.13"
|
zip = "0.5.13"
|
||||||
|
async-trait = "0.1.52"
|
||||||
|
url = "2.2.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9.0"
|
||||||
|
|
|
@ -61,6 +61,7 @@ yes
|
||||||
- [x] Fetch crate / manifest via crates.io
|
- [x] Fetch crate / manifest via crates.io
|
||||||
- [ ] Fetch crate / manifest via git (/ github / gitlab)
|
- [ ] Fetch crate / manifest via git (/ github / gitlab)
|
||||||
- [x] Use local crate / manifest (`--manifest-path`)
|
- [x] Use local crate / manifest (`--manifest-path`)
|
||||||
|
- [x] Fetch build from the [quickinstall](https://github.com/alsuren/cargo-quickinstall) repository
|
||||||
- [ ] Unofficial packaging
|
- [ ] Unofficial packaging
|
||||||
- Package formats
|
- Package formats
|
||||||
- [x] Tgz
|
- [x] Tgz
|
||||||
|
@ -126,6 +127,10 @@ By default `binstall` is setup to work with github releases, and expects to find
|
||||||
|
|
||||||
If your package already uses this approach, you shouldn't need to set anything.
|
If your package already uses this approach, you shouldn't need to set anything.
|
||||||
|
|
||||||
|
### QuickInstall
|
||||||
|
|
||||||
|
[QuickInstall](https://github.com/alsuren/cargo-quickinstall) is an unofficial repository of prebuilt binaries for Crates, and `binstall` has built-in support for it! If your crate is built by QuickInstall, it will already work with `binstall`. However, binaries as configured above take precedence when they exist.
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
For example, the default configuration (as shown above) for a crate called `radio-sx128x` (version: `v0.14.1-alpha.5` on x86_64 linux) would be interpolated to:
|
For example, the default configuration (as shown above) for a crate called `radio-sx128x` (version: `v0.14.1-alpha.5` on x86_64 linux) would be interpolated to:
|
||||||
|
@ -166,9 +171,6 @@ Which provides a binary path of: `sx128x-util-x86_64-unknown-linux-gnu[.exe]`. I
|
||||||
- Why use the cargo manifest?
|
- Why use the cargo manifest?
|
||||||
- Crates already have these, and they already contain a significant portion of the required information.
|
- Crates already have these, and they already contain a significant portion of the required information.
|
||||||
Also there's this great and woefully underused (imo) `[package.metadata]` field.
|
Also there's this great and woefully underused (imo) `[package.metadata]` field.
|
||||||
- Why not use a binary repository instead?
|
|
||||||
- Then we'd need to _host_ a binary repository, and worry about publishing and all the other fun things that come with releasing software.
|
|
||||||
This way we can use existing CI infrastructure and build artifacts, and maintainers can choose how to distribute their packages.
|
|
||||||
- Is this secure?
|
- Is this secure?
|
||||||
- Yes and also no? We're not (yet? #1) doing anything to verify the CI binaries are produced by the right person / organisation.
|
- Yes and also no? We're not (yet? #1) doing anything to verify the CI binaries are produced by the right person / organisation.
|
||||||
However, we're pulling data from crates.io and the cargo manifest, both of which are _already_ trusted entities, and this is
|
However, we're pulling data from crates.io and the cargo manifest, both of which are _already_ trusted entities, and this is
|
||||||
|
|
1
build.rs
1
build.rs
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
// Fetch build target and define this for the compiler
|
// Fetch build target and define this for the compiler
|
||||||
fn main() {
|
fn main() {
|
||||||
println!(
|
println!(
|
||||||
|
|
125
src/bins.rs
Normal file
125
src/bins.rs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use cargo_toml::Product;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{PkgFmt, PkgMeta, Template};
|
||||||
|
|
||||||
|
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> {}
|
|
@ -1,39 +1,43 @@
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use log::{debug};
|
use anyhow::{anyhow, Context};
|
||||||
use anyhow::{Context, anyhow};
|
use log::debug;
|
||||||
use semver::{Version, VersionReq};
|
use semver::{Version, VersionReq};
|
||||||
|
|
||||||
use crates_io_api::AsyncClient;
|
use crates_io_api::AsyncClient;
|
||||||
|
|
||||||
use crate::PkgFmt;
|
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
|
use crate::PkgFmt;
|
||||||
|
|
||||||
fn find_version<'a, V: Iterator<Item=&'a str>>(requirement: &str, version_iter: V) -> Result<String, anyhow::Error> {
|
fn find_version<'a, V: Iterator<Item = &'a str>>(
|
||||||
|
requirement: &str,
|
||||||
|
version_iter: V,
|
||||||
|
) -> Result<String, anyhow::Error> {
|
||||||
// Parse version requirement
|
// Parse version requirement
|
||||||
let version_req = VersionReq::parse(requirement)?;
|
let version_req = VersionReq::parse(requirement)?;
|
||||||
|
|
||||||
// Filter for matching versions
|
// Filter for matching versions
|
||||||
let mut filtered: Vec<_> = version_iter.filter(|v| {
|
let mut filtered: Vec<_> = version_iter
|
||||||
// Remove leading `v` for git tags
|
.filter(|v| {
|
||||||
let ver_str = match v.strip_prefix("s") {
|
// Remove leading `v` for git tags
|
||||||
Some(v) => v,
|
let ver_str = match v.strip_prefix("s") {
|
||||||
None => v,
|
Some(v) => v,
|
||||||
};
|
None => v,
|
||||||
|
};
|
||||||
|
|
||||||
// Parse out version
|
// Parse out version
|
||||||
let ver = match Version::parse(ver_str) {
|
let ver = match Version::parse(ver_str) {
|
||||||
Ok(sv) => sv,
|
Ok(sv) => sv,
|
||||||
Err(_) => return false,
|
Err(_) => return false,
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Version: {:?}", ver);
|
debug!("Version: {:?}", ver);
|
||||||
|
|
||||||
// Filter by version match
|
// Filter by version match
|
||||||
version_req.matches(&ver)
|
version_req.matches(&ver)
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Sort by highest matching version
|
// Sort by highest matching version
|
||||||
filtered.sort_by(|a, b| {
|
filtered.sort_by(|a, b| {
|
||||||
|
@ -48,13 +52,19 @@ fn find_version<'a, V: Iterator<Item=&'a str>>(requirement: &str, version_iter:
|
||||||
// Return highest version
|
// Return highest version
|
||||||
match filtered.get(0) {
|
match filtered.get(0) {
|
||||||
Some(v) => Ok(v.to_string()),
|
Some(v) => Ok(v.to_string()),
|
||||||
None => Err(anyhow!("No matching version for requirement: '{}'", version_req))
|
None => Err(anyhow!(
|
||||||
|
"No matching version for requirement: '{}'",
|
||||||
|
version_req
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch a crate by name and version from crates.io
|
/// Fetch a crate by name and version from crates.io
|
||||||
pub async fn fetch_crate_cratesio(name: &str, version_req: &str, temp_dir: &Path) -> Result<PathBuf, anyhow::Error> {
|
pub async fn fetch_crate_cratesio(
|
||||||
|
name: &str,
|
||||||
|
version_req: &str,
|
||||||
|
temp_dir: &Path,
|
||||||
|
) -> Result<PathBuf, anyhow::Error> {
|
||||||
// Fetch / update index
|
// Fetch / update index
|
||||||
debug!("Updating crates.io index");
|
debug!("Updating crates.io index");
|
||||||
let mut index = crates_index::Index::new_cargo_default()?;
|
let mut index = crates_index::Index::new_cargo_default()?;
|
||||||
|
@ -65,37 +75,48 @@ pub async fn fetch_crate_cratesio(name: &str, version_req: &str, temp_dir: &Path
|
||||||
let base_info = match index.crate_(name) {
|
let base_info = match index.crate_(name) {
|
||||||
Some(i) => i,
|
Some(i) => i,
|
||||||
None => {
|
None => {
|
||||||
return Err(anyhow::anyhow!("Error fetching information for crate {}", name));
|
return Err(anyhow::anyhow!(
|
||||||
|
"Error fetching information for crate {}",
|
||||||
|
name
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Locate matching version
|
// Locate matching version
|
||||||
let version_iter = base_info.versions().iter().map(|v| v.version() );
|
let version_iter = base_info.versions().iter().map(|v| v.version());
|
||||||
let version_name = find_version(version_req, version_iter)?;
|
let version_name = find_version(version_req, version_iter)?;
|
||||||
|
|
||||||
// Build crates.io api client
|
// Build crates.io api client
|
||||||
let api_client = AsyncClient::new("cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100))?;
|
let api_client = AsyncClient::new(
|
||||||
|
"cargo-binstall (https://github.com/ryankurte/cargo-binstall)",
|
||||||
|
Duration::from_millis(100),
|
||||||
|
)?;
|
||||||
|
|
||||||
// Fetch online crate information
|
// Fetch online crate information
|
||||||
let crate_info = api_client.get_crate(name.as_ref()).await
|
let crate_info = api_client
|
||||||
|
.get_crate(name.as_ref())
|
||||||
|
.await
|
||||||
.context("Error fetching crate information")?;
|
.context("Error fetching crate information")?;
|
||||||
|
|
||||||
// Fetch information for the filtered version
|
// Fetch information for the filtered version
|
||||||
let version = match crate_info.versions.iter().find(|v| v.num == version_name) {
|
let version = match crate_info.versions.iter().find(|v| v.num == version_name) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
None => {
|
||||||
return Err(anyhow::anyhow!("No information found for crate: '{}' version: '{}'",
|
return Err(anyhow::anyhow!(
|
||||||
name, version_name));
|
"No information found for crate: '{}' version: '{}'",
|
||||||
|
name,
|
||||||
|
version_name
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Found information for crate version: '{}'", version.num);
|
debug!("Found information for crate version: '{}'", version.num);
|
||||||
|
|
||||||
// Download crate to temporary dir (crates.io or git?)
|
// Download crate to temporary dir (crates.io or git?)
|
||||||
let crate_url = format!("https://crates.io/{}", version.dl_path);
|
let crate_url = format!("https://crates.io/{}", version.dl_path);
|
||||||
let tgz_path = temp_dir.join(format!("{}.tgz", name));
|
let tgz_path = temp_dir.join(format!("{}.tgz", name));
|
||||||
|
|
||||||
debug!("Fetching crate from: {}", crate_url);
|
debug!("Fetching crate from: {}", crate_url);
|
||||||
|
|
||||||
// Download crate
|
// Download crate
|
||||||
download(&crate_url, &tgz_path).await?;
|
download(&crate_url, &tgz_path).await?;
|
||||||
|
@ -111,8 +132,10 @@ pub async fn fetch_crate_cratesio(name: &str, version_req: &str, temp_dir: &Path
|
||||||
|
|
||||||
/// Fetch a crate by name and version from github
|
/// Fetch a crate by name and version from github
|
||||||
/// TODO: implement this
|
/// TODO: implement this
|
||||||
pub async fn fetch_crate_gh_releases(_name: &str, _version: Option<&str>, _temp_dir: &Path) -> Result<PathBuf, anyhow::Error> {
|
pub async fn fetch_crate_gh_releases(
|
||||||
|
_name: &str,
|
||||||
|
_version: Option<&str>,
|
||||||
|
_temp_dir: &Path,
|
||||||
|
) -> Result<PathBuf, anyhow::Error> {
|
||||||
unimplemented!();
|
unimplemented!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
63
src/fetchers.rs
Normal file
63
src/fetchers.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub use gh_crate_meta::*;
|
||||||
|
pub use quickinstall::*;
|
||||||
|
|
||||||
|
use crate::{PkgFmt, PkgMeta};
|
||||||
|
|
||||||
|
mod gh_crate_meta;
|
||||||
|
mod quickinstall;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait Fetcher {
|
||||||
|
/// Create a new fetcher from some data
|
||||||
|
async fn new(data: &Data) -> Result<Box<Self>, anyhow::Error>
|
||||||
|
where
|
||||||
|
Self: 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>;
|
||||||
|
|
||||||
|
/// Return the package format
|
||||||
|
fn pkg_fmt(&self) -> PkgFmt;
|
||||||
|
|
||||||
|
/// A short human-readable name or descriptor for the package source
|
||||||
|
fn source_name(&self) -> String;
|
||||||
|
|
||||||
|
/// Should return true if the remote is from a third-party source
|
||||||
|
fn is_third_party(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data required to fetch a package
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Data {
|
||||||
|
pub name: String,
|
||||||
|
pub target: String,
|
||||||
|
pub version: String,
|
||||||
|
pub repo: Option<String>,
|
||||||
|
pub meta: PkgMeta,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MultiFetcher {
|
||||||
|
fetchers: Vec<Box<dyn Fetcher>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultiFetcher {
|
||||||
|
pub fn add(&mut self, fetcher: Box<dyn Fetcher>) {
|
||||||
|
self.fetchers.push(fetcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn first_available(&self) -> Option<&dyn Fetcher> {
|
||||||
|
for fetcher in &self.fetchers {
|
||||||
|
if fetcher.check().await.unwrap_or(false) {
|
||||||
|
return Some(&**fetcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
74
src/fetchers/gh_crate_meta.rs
Normal file
74
src/fetchers/gh_crate_meta.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use log::{debug, info};
|
||||||
|
use reqwest::Method;
|
||||||
|
use serde::Serialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use super::Data;
|
||||||
|
use crate::{download, remote_exists, PkgFmt, Template};
|
||||||
|
|
||||||
|
pub struct GhCrateMeta {
|
||||||
|
url: Url,
|
||||||
|
pkg_fmt: PkgFmt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl super::Fetcher for GhCrateMeta {
|
||||||
|
async fn new(data: &Data) -> Result<Box<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(Box::new(Self {
|
||||||
|
url: Url::parse(&ctx.render(&data.meta.pkg_url)?)?,
|
||||||
|
pkg_fmt: data.meta.pkg_fmt,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check(&self) -> Result<bool, anyhow::Error> {
|
||||||
|
info!("Checking for package at: '{}'", self.url);
|
||||||
|
remote_exists(self.url.as_str(), Method::HEAD).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch(&self, dst: &Path) -> Result<(), anyhow::Error> {
|
||||||
|
info!("Downloading package from: '{}'", self.url);
|
||||||
|
download(self.url.as_str(), dst).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pkg_fmt(&self) -> PkgFmt {
|
||||||
|
self.pkg_fmt
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_name(&self) -> String {
|
||||||
|
if let Some(domain) = self.url.domain() {
|
||||||
|
domain.to_string()
|
||||||
|
} else if let Some(host) = self.url.host_str() {
|
||||||
|
host.to_string()
|
||||||
|
} else {
|
||||||
|
self.url.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_third_party(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {}
|
80
src/fetchers/quickinstall.rs
Normal file
80
src/fetchers/quickinstall.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
use reqwest::Method;
|
||||||
|
|
||||||
|
use super::Data;
|
||||||
|
use crate::{download, remote_exists, PkgFmt};
|
||||||
|
|
||||||
|
const BASE_URL: &str = "https://github.com/alsuren/cargo-quickinstall/releases/download";
|
||||||
|
const STATS_URL: &str = "https://warehouse-clerk-tmp.vercel.app/api/crate";
|
||||||
|
const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
pub struct QuickInstall {
|
||||||
|
package: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl super::Fetcher for QuickInstall {
|
||||||
|
async fn new(data: &Data) -> Result<Box<Self>, anyhow::Error> {
|
||||||
|
let crate_name = &data.name;
|
||||||
|
let version = &data.version;
|
||||||
|
let target = &data.target;
|
||||||
|
Ok(Box::new(Self {
|
||||||
|
package: format!("{crate_name}-{version}-{target}"),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check(&self) -> Result<bool, anyhow::Error> {
|
||||||
|
let url = self.package_url();
|
||||||
|
self.report().await?;
|
||||||
|
info!("Checking for package at: '{url}'");
|
||||||
|
remote_exists(&url, Method::HEAD).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch(&self, dst: &Path) -> Result<(), anyhow::Error> {
|
||||||
|
let url = self.package_url();
|
||||||
|
info!("Downloading package from: '{url}'");
|
||||||
|
download(&url, dst).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pkg_fmt(&self) -> PkgFmt {
|
||||||
|
PkgFmt::Tgz
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_name(&self) -> String {
|
||||||
|
String::from("QuickInstall")
|
||||||
|
}
|
||||||
|
fn is_third_party(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuickInstall {
|
||||||
|
fn package_url(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"{base_url}/{package}/{package}.tar.gz",
|
||||||
|
base_url = BASE_URL,
|
||||||
|
package = self.package
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stats_url(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"{stats_url}/{package}.tar.gz",
|
||||||
|
stats_url = STATS_URL,
|
||||||
|
package = self.package
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn report(&self) -> Result<(), anyhow::Error> {
|
||||||
|
info!("Sending installation report to quickinstall (anonymous)");
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.user_agent(USER_AGENT)
|
||||||
|
.build()?
|
||||||
|
.request(Method::HEAD, &self.stats_url())
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +1,23 @@
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use log::{debug, info, error};
|
use log::{debug, error, info};
|
||||||
|
|
||||||
use cargo_toml::{Manifest};
|
use cargo_toml::Manifest;
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
|
use serde::Serialize;
|
||||||
use tar::Archive;
|
use tar::Archive;
|
||||||
|
use tinytemplate::TinyTemplate;
|
||||||
use xz2::read::XzDecoder;
|
use xz2::read::XzDecoder;
|
||||||
use zip::read::ZipArchive;
|
use zip::read::ZipArchive;
|
||||||
|
|
||||||
use crate::{Meta};
|
use crate::Meta;
|
||||||
|
|
||||||
use super::PkgFmt;
|
use super::PkgFmt;
|
||||||
|
|
||||||
/// Load binstall metadata from the crate `Cargo.toml` at the provided path
|
/// Load binstall metadata from the crate `Cargo.toml` at the provided path
|
||||||
pub fn load_manifest_path<P: AsRef<Path>>(manifest_path: P) -> Result<Manifest<Meta>, anyhow::Error> {
|
pub fn load_manifest_path<P: AsRef<Path>>(
|
||||||
|
manifest_path: P,
|
||||||
|
) -> Result<Manifest<Meta>, anyhow::Error> {
|
||||||
debug!("Reading manifest: {}", manifest_path.as_ref().display());
|
debug!("Reading manifest: {}", manifest_path.as_ref().display());
|
||||||
|
|
||||||
// Load and parse manifest (this checks file system for binary output names)
|
// Load and parse manifest (this checks file system for binary output names)
|
||||||
|
@ -24,9 +27,13 @@ pub fn load_manifest_path<P: AsRef<Path>>(manifest_path: P) -> Result<Manifest<M
|
||||||
Ok(manifest)
|
Ok(manifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn remote_exists(url: &str, method: reqwest::Method) -> Result<bool, anyhow::Error> {
|
||||||
|
let req = reqwest::Client::new().request(method, url).send().await?;
|
||||||
|
Ok(req.status().is_success())
|
||||||
|
}
|
||||||
|
|
||||||
/// Download a file from the provided URL to the provided path
|
/// 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> {
|
pub async fn download<P: AsRef<Path>>(url: &str, path: P) -> Result<(), anyhow::Error> {
|
||||||
|
|
||||||
debug!("Downloading from: '{}'", url);
|
debug!("Downloading from: '{}'", url);
|
||||||
|
|
||||||
let resp = reqwest::get(url).await?;
|
let resp = reqwest::get(url).await?;
|
||||||
|
@ -46,51 +53,75 @@ pub async fn download<P: AsRef<Path>>(url: &str, path: P) -> Result<(), anyhow::
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract files from the specified source onto the specified path
|
/// 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> {
|
pub fn extract<S: AsRef<Path>, P: AsRef<Path>>(
|
||||||
|
source: S,
|
||||||
|
fmt: PkgFmt,
|
||||||
|
path: P,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
match fmt {
|
match fmt {
|
||||||
PkgFmt::Tar => {
|
PkgFmt::Tar => {
|
||||||
// Extract to install dir
|
// Extract to install dir
|
||||||
debug!("Extracting from tar archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref());
|
debug!(
|
||||||
|
"Extracting from tar archive '{:?}' to `{:?}`",
|
||||||
|
source.as_ref(),
|
||||||
|
path.as_ref()
|
||||||
|
);
|
||||||
|
|
||||||
let dat = std::fs::File::open(source)?;
|
let dat = std::fs::File::open(source)?;
|
||||||
let mut tar = Archive::new(dat);
|
let mut tar = Archive::new(dat);
|
||||||
|
|
||||||
tar.unpack(path)?;
|
tar.unpack(path)?;
|
||||||
},
|
}
|
||||||
PkgFmt::Tgz => {
|
PkgFmt::Tgz => {
|
||||||
// Extract to install dir
|
// Extract to install dir
|
||||||
debug!("Decompressing from tgz archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref());
|
debug!(
|
||||||
|
"Decompressing from tgz archive '{:?}' to `{:?}`",
|
||||||
|
source.as_ref(),
|
||||||
|
path.as_ref()
|
||||||
|
);
|
||||||
|
|
||||||
let dat = std::fs::File::open(source)?;
|
let dat = std::fs::File::open(source)?;
|
||||||
let tar = GzDecoder::new(dat);
|
let tar = GzDecoder::new(dat);
|
||||||
let mut tgz = Archive::new(tar);
|
let mut tgz = Archive::new(tar);
|
||||||
|
|
||||||
tgz.unpack(path)?;
|
tgz.unpack(path)?;
|
||||||
},
|
}
|
||||||
PkgFmt::Txz => {
|
PkgFmt::Txz => {
|
||||||
// Extract to install dir
|
// Extract to install dir
|
||||||
debug!("Decompressing from txz archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref());
|
debug!(
|
||||||
|
"Decompressing from txz archive '{:?}' to `{:?}`",
|
||||||
|
source.as_ref(),
|
||||||
|
path.as_ref()
|
||||||
|
);
|
||||||
|
|
||||||
let dat = std::fs::File::open(source)?;
|
let dat = std::fs::File::open(source)?;
|
||||||
let tar = XzDecoder::new(dat);
|
let tar = XzDecoder::new(dat);
|
||||||
let mut txz = Archive::new(tar);
|
let mut txz = Archive::new(tar);
|
||||||
|
|
||||||
txz.unpack(path)?;
|
txz.unpack(path)?;
|
||||||
},
|
}
|
||||||
PkgFmt::Zip => {
|
PkgFmt::Zip => {
|
||||||
// Extract to install dir
|
// Extract to install dir
|
||||||
debug!("Decompressing from zip archive '{:?}' to `{:?}`", source.as_ref(), path.as_ref());
|
debug!(
|
||||||
|
"Decompressing from zip archive '{:?}' to `{:?}`",
|
||||||
|
source.as_ref(),
|
||||||
|
path.as_ref()
|
||||||
|
);
|
||||||
|
|
||||||
let dat = std::fs::File::open(source)?;
|
let dat = std::fs::File::open(source)?;
|
||||||
let mut zip = ZipArchive::new(dat)?;
|
let mut zip = ZipArchive::new(dat)?;
|
||||||
|
|
||||||
zip.extract(path)?;
|
zip.extract(path)?;
|
||||||
},
|
}
|
||||||
PkgFmt::Bin => {
|
PkgFmt::Bin => {
|
||||||
debug!("Copying binary '{:?}' to `{:?}`", source.as_ref(), path.as_ref());
|
debug!(
|
||||||
|
"Copying binary '{:?}' to `{:?}`",
|
||||||
|
source.as_ref(),
|
||||||
|
path.as_ref()
|
||||||
|
);
|
||||||
// Copy to install dir
|
// Copy to install dir
|
||||||
std::fs::copy(source, path)?;
|
std::fs::copy(source, path)?;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -101,7 +132,7 @@ pub fn extract<S: AsRef<Path>, P: AsRef<Path>>(source: S, fmt: PkgFmt, path: P)
|
||||||
pub fn get_install_path<P: AsRef<Path>>(install_path: Option<P>) -> Option<PathBuf> {
|
pub fn get_install_path<P: AsRef<Path>>(install_path: Option<P>) -> Option<PathBuf> {
|
||||||
// Command line override first first
|
// Command line override first first
|
||||||
if let Some(p) = install_path {
|
if let Some(p) = install_path {
|
||||||
return Some(PathBuf::from(p.as_ref()))
|
return Some(PathBuf::from(p.as_ref()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Environmental variables
|
// Environmental variables
|
||||||
|
@ -144,8 +175,24 @@ pub fn confirm() -> Result<bool, anyhow::Error> {
|
||||||
match input.as_str().trim() {
|
match input.as_str().trim() {
|
||||||
"yes" => Ok(true),
|
"yes" => Ok(true),
|
||||||
"no" => Ok(false),
|
"no" => Ok(false),
|
||||||
_ => {
|
_ => Err(anyhow::anyhow!(
|
||||||
Err(anyhow::anyhow!("Valid options are 'yes', 'no', please try again"))
|
"Valid options are 'yes', 'no', please try again"
|
||||||
}
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
62
src/lib.rs
62
src/lib.rs
|
@ -1,10 +1,7 @@
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum_macros::{Display, EnumString, EnumVariantNames};
|
use strum_macros::{Display, EnumString, EnumVariantNames};
|
||||||
use tinytemplate::TinyTemplate;
|
|
||||||
|
|
||||||
|
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
pub use helpers::*;
|
pub use helpers::*;
|
||||||
|
@ -12,20 +9,23 @@ pub use helpers::*;
|
||||||
pub mod drivers;
|
pub mod drivers;
|
||||||
pub use drivers::*;
|
pub use drivers::*;
|
||||||
|
|
||||||
|
pub mod bins;
|
||||||
|
pub mod fetchers;
|
||||||
|
|
||||||
/// Compiled target triple, used as default for binary fetching
|
/// Compiled target triple, used as default for binary fetching
|
||||||
pub const TARGET: &'static str = env!("TARGET");
|
pub const TARGET: &'static str = env!("TARGET");
|
||||||
|
|
||||||
/// Default package path template (may be overridden in package Cargo.toml)
|
/// Default package path template (may be overridden in package Cargo.toml)
|
||||||
pub const DEFAULT_PKG_URL: &'static str = "{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }";
|
pub const DEFAULT_PKG_URL: &'static str =
|
||||||
|
"{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ format }";
|
||||||
|
|
||||||
/// Default binary name template (may be overridden in package Cargo.toml)
|
/// Default binary name template (may be overridden in package Cargo.toml)
|
||||||
pub const DEFAULT_BIN_PATH: &'static str = "{ name }-{ target }-v{ version }/{ bin }{ format }";
|
pub const DEFAULT_BIN_PATH: &'static str = "{ name }-{ target }-v{ version }/{ bin }{ format }";
|
||||||
|
|
||||||
|
|
||||||
/// Binary format enumeration
|
/// Binary format enumeration
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(
|
||||||
#[derive(Display, EnumString, EnumVariantNames)]
|
Debug, Copy, Clone, PartialEq, Serialize, Deserialize, Display, EnumString, EnumVariantNames,
|
||||||
|
)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum PkgFmt {
|
pub enum PkgFmt {
|
||||||
|
@ -131,7 +131,6 @@ impl Default for PkgOverride {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct BinMeta {
|
pub struct BinMeta {
|
||||||
|
@ -141,36 +140,9 @@ pub struct BinMeta {
|
||||||
pub path: String,
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::{load_manifest_path};
|
use crate::load_manifest_path;
|
||||||
|
|
||||||
use cargo_toml::Product;
|
use cargo_toml::Product;
|
||||||
|
|
||||||
|
@ -187,7 +159,7 @@ mod test {
|
||||||
|
|
||||||
let manifest = load_manifest_path(&manifest_dir).expect("Error parsing metadata");
|
let manifest = load_manifest_path(&manifest_dir).expect("Error parsing metadata");
|
||||||
let package = manifest.package.unwrap();
|
let package = manifest.package.unwrap();
|
||||||
let meta = package.metadata.map(|m| m.binstall ).flatten().unwrap();
|
let meta = package.metadata.map(|m| m.binstall).flatten().unwrap();
|
||||||
|
|
||||||
assert_eq!(&package.name, "cargo-binstall");
|
assert_eq!(&package.name, "cargo-binstall");
|
||||||
|
|
||||||
|
@ -198,14 +170,12 @@ mod test {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
manifest.bin.as_slice(),
|
manifest.bin.as_slice(),
|
||||||
&[
|
&[Product {
|
||||||
Product{
|
name: Some("cargo-binstall".to_string()),
|
||||||
name: Some("cargo-binstall".to_string()),
|
path: Some("src/main.rs".to_string()),
|
||||||
path: Some("src/main.rs".to_string()),
|
edition: Some(cargo_toml::Edition::E2018),
|
||||||
edition: Some(cargo_toml::Edition::E2018),
|
..Default::default()
|
||||||
..Default::default()
|
},],
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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 log::{debug, error, info, warn, LevelFilter};
|
||||||
use simplelog::{TermLogger, ConfigBuilder, TerminalMode, ColorChoice};
|
use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode};
|
||||||
|
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
use cargo_binstall::*;
|
use cargo_binstall::{
|
||||||
|
bins,
|
||||||
|
fetchers::{Data, Fetcher, GhCrateMeta, MultiFetcher, QuickInstall},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, StructOpt)]
|
#[derive(Debug, StructOpt)]
|
||||||
struct Options {
|
struct Options {
|
||||||
|
@ -48,7 +51,7 @@ struct Options {
|
||||||
|
|
||||||
/// 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 and
|
/// the specified path directly, useful for debugging and
|
||||||
/// when adding `binstall` support.
|
/// when adding `binstall` support.
|
||||||
#[structopt(long)]
|
#[structopt(long)]
|
||||||
manifest_path: Option<PathBuf>,
|
manifest_path: Option<PathBuf>,
|
||||||
|
@ -58,10 +61,8 @@ struct Options {
|
||||||
log_level: LevelFilter,
|
log_level: LevelFilter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), anyhow::Error> {
|
async fn main() -> Result<(), anyhow::Error> {
|
||||||
|
|
||||||
// Filter extraneous arg when invoked by cargo
|
// Filter extraneous arg when invoked by cargo
|
||||||
// `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"]
|
// `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"]
|
||||||
// `cargo binstall --help` gives ["/home/ryan/.cargo/bin/cargo-binstall", "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("hyper".to_string());
|
||||||
log_config.add_filter_ignore("reqwest".to_string());
|
log_config.add_filter_ignore("reqwest".to_string());
|
||||||
log_config.set_location_level(LevelFilter::Off);
|
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.
|
// Create a temporary directory for downloads etc.
|
||||||
let temp_dir = TempDir::new("cargo-binstall")?;
|
let temp_dir = TempDir::new("cargo-binstall")?;
|
||||||
|
@ -92,13 +99,17 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => fetch_crate_cratesio(&opts.name, &opts.version, temp_dir.path()).await?,
|
None => fetch_crate_cratesio(&opts.name, &opts.version, temp_dir.path()).await?,
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Reading manifest: {}", manifest_path.display());
|
debug!("Reading manifest: {}", manifest_path.display());
|
||||||
let manifest = load_manifest_path(manifest_path.join("Cargo.toml"))?;
|
let manifest = load_manifest_path(manifest_path.join("Cargo.toml"))?;
|
||||||
let package = manifest.package.unwrap();
|
let package = manifest.package.unwrap();
|
||||||
|
|
||||||
let (mut meta, binaries) = (
|
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,
|
manifest.bin,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -109,42 +120,61 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
|
|
||||||
debug!("Found metadata: {:?}", meta);
|
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
|
// Compute install directory
|
||||||
let install_path = match get_install_path(opts.install_path.as_deref()) {
|
let install_path = get_install_path(opts.install_path.as_deref()).ok_or_else(|| {
|
||||||
Some(p) => p,
|
error!("No viable install path found of specified, try `--install-path`");
|
||||||
None => {
|
anyhow::anyhow!("No install path found or specified")
|
||||||
error!("No viable install path found of specified, try `--install-path`");
|
})?;
|
||||||
return Err(anyhow::anyhow!("No install path found or specified"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("Using install path: {}", install_path.display());
|
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
|
// Download package
|
||||||
let pkg_path = temp_dir.path().join(format!("pkg-{}.{}", opts.name, meta.pkg_fmt));
|
fetcher.fetch(&pkg_path).await?;
|
||||||
download(&rendered, pkg_path.to_str().unwrap()).await?;
|
|
||||||
|
|
||||||
#[cfg(incomplete)]
|
#[cfg(incomplete)]
|
||||||
{
|
{
|
||||||
// Fetch and check package signature if available
|
// 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);
|
debug!("Found public key: {}", pub_key);
|
||||||
|
|
||||||
// Generate signature file URL
|
// Generate signature file URL
|
||||||
|
@ -160,7 +190,6 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
|
|
||||||
// TODO: do the signature check
|
// TODO: do the signature check
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
warn!("No public key found, package signature could not be validated");
|
warn!("No public key found, package signature could not be validated");
|
||||||
}
|
}
|
||||||
|
@ -168,7 +197,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
|
|
||||||
// Extract files
|
// Extract files
|
||||||
let bin_path = temp_dir.path().join(format!("bin-{}", opts.name));
|
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
|
// Bypass cleanup if disabled
|
||||||
if opts.no_cleanup {
|
if opts.no_cleanup {
|
||||||
|
@ -177,94 +206,58 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
|
|
||||||
if binaries.len() == 0 {
|
if binaries.len() == 0 {
|
||||||
error!("No binaries specified (or inferred from file system)");
|
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
|
// List files to be installed
|
||||||
// based on those found via Cargo.toml
|
// based on those found via Cargo.toml
|
||||||
let bin_files = binaries.iter().map(|p| {
|
let bin_data = bins::Data {
|
||||||
// Fetch binary base name
|
name: package.name.clone(),
|
||||||
let base_name = p.name.clone().unwrap();
|
target: opts.target.clone(),
|
||||||
|
version: package.version.clone(),
|
||||||
|
repo: package.repository.clone(),
|
||||||
|
meta,
|
||||||
|
bin_path,
|
||||||
|
install_path,
|
||||||
|
};
|
||||||
|
|
||||||
// Generate binary path via interpolation
|
let bin_files = binaries
|
||||||
let mut bin_ctx = ctx.clone();
|
.iter()
|
||||||
bin_ctx.bin = Some(base_name.clone());
|
.map(|p| bins::BinFile::from_product(&bin_data, p))
|
||||||
|
.collect::<Result<Vec<_>, anyhow::Error>>()?;
|
||||||
// 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>>()?;
|
|
||||||
|
|
||||||
// Prompt user for confirmation
|
// Prompt user for confirmation
|
||||||
info!("This will install the following binaries:");
|
info!("This will install the following binaries:");
|
||||||
for (name, source, dest, _link) in &bin_files {
|
for file in &bin_files {
|
||||||
info!(" - {} ({} -> {})", name, source.file_name().unwrap().to_string_lossy(), dest.display());
|
info!(" - {}", file.preview_bin());
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.no_symlinks {
|
if !opts.no_symlinks {
|
||||||
info!("And create (or update) the following symlinks:");
|
info!("And create (or update) the following symlinks:");
|
||||||
for (name, _source, dest, link) in &bin_files {
|
for file in &bin_files {
|
||||||
info!(" - {} ({} -> {})", name, dest.display(), link.display());
|
info!(" - {}", file.preview_link());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.no_confirm && !confirm()? {
|
if !opts.no_confirm && !confirm()? {
|
||||||
warn!("Installation cancelled");
|
warn!("Installation cancelled");
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Installing binaries...");
|
info!("Installing binaries...");
|
||||||
|
for file in &bin_files {
|
||||||
// Install binaries
|
file.install_bin()?;
|
||||||
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))?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate symlinks
|
// Generate symlinks
|
||||||
if !opts.no_symlinks {
|
if !opts.no_symlinks {
|
||||||
for (_name, _source, dest, link) in &bin_files {
|
for file in &bin_files {
|
||||||
// Remove existing symlink
|
file.install_link()?;
|
||||||
// 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)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Installation complete!");
|
|
||||||
|
|
||||||
|
info!("Installation complete!");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue