Split crates and clean up structure of codebase (#294)

Co-authored-by: Jiahao XU <Jiahao_XU@outlook.com>
This commit is contained in:
Félix Saparelli 2022-08-20 23:24:12 +12:00 committed by GitHub
parent bf700f9012
commit 4b00f5f143
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 2989 additions and 1423 deletions

162
crates/lib/src/bins.rs Normal file
View file

@ -0,0 +1,162 @@
use std::path::{Path, PathBuf};
use cargo_toml::Product;
use compact_str::CompactString;
use log::debug;
use serde::Serialize;
use tinytemplate::TinyTemplate;
use crate::{
errors::BinstallError,
fs::{atomic_install, atomic_symlink_file},
manifests::cargo_toml_binstall::{PkgFmt, PkgMeta},
};
pub struct BinFile {
pub base_name: CompactString,
pub source: PathBuf,
pub dest: PathBuf,
pub link: PathBuf,
}
impl BinFile {
pub fn from_product(data: &Data, product: &Product) -> Result<Self, BinstallError> {
let base_name = CompactString::from(product.name.clone().unwrap());
let binary_ext = if data.target.contains("windows") {
".exe"
} else {
""
};
let ctx = Context {
name: &data.name,
repo: data.repo.as_ref().map(|s| &s[..]),
target: &data.target,
version: &data.version,
bin: &base_name,
format: binary_ext,
binary_ext,
};
// 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{.extension}
let dest_file_path = ctx.render("{ bin }-v{ version }{ binary-ext }")?;
let dest = data.install_path.join(dest_file_path);
// Link at install dir + base-name{.extension}
let link = data
.install_path
.join(&ctx.render("{ bin }{ binary-ext }")?);
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.link.display(),
self.link_dest().display()
)
}
pub fn install_bin(&self) -> Result<(), BinstallError> {
if !self.source.try_exists()? {
return Err(BinstallError::BinFileNotFound(self.source.clone()));
}
debug!(
"Atomically install file from '{}' to '{}'",
self.source.display(),
self.dest.display()
);
atomic_install(&self.source, &self.dest)?;
Ok(())
}
pub fn install_link(&self) -> Result<(), BinstallError> {
// Remove existing symlink
// TODO: check if existing symlink is correct
if self.link.exists() {
debug!("Remove link '{}'", self.link.display());
std::fs::remove_file(&self.link)?;
}
let dest = self.link_dest();
debug!(
"Create link '{}' pointing to '{}'",
self.link.display(),
dest.display()
);
atomic_symlink_file(dest, &self.link)?;
Ok(())
}
fn link_dest(&self) -> &Path {
if cfg!(target_family = "unix") {
Path::new(self.dest.file_name().unwrap())
} else {
&self.dest
}
}
}
/// 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 bin: &'c str,
/// Soft-deprecated alias for binary-ext
pub format: &'c str,
/// Filename extension on the binary, i.e. .exe on Windows, nothing otherwise
#[serde(rename = "binary-ext")]
pub binary_ext: &'c str,
}
impl<'c> Context<'c> {
fn render(&self, template: &str) -> Result<String, BinstallError> {
let mut tt = TinyTemplate::new();
tt.add_template("path", template)?;
Ok(tt.render("path", self)?)
}
}

View file

@ -0,0 +1,5 @@
mod version;
use version::find_version;
mod crates_io;
pub use crates_io::fetch_crate_cratesio;

View file

@ -0,0 +1,62 @@
use std::path::PathBuf;
use cargo_toml::Manifest;
use crates_io_api::AsyncClient;
use log::debug;
use reqwest::Client;
use semver::VersionReq;
use url::Url;
use crate::{
errors::BinstallError,
helpers::download::download_tar_based_and_visit,
manifests::cargo_toml_binstall::{Meta, TarBasedFmt},
};
use super::find_version;
mod vfs;
mod visitor;
use visitor::ManifestVisitor;
/// Fetch a crate Cargo.toml by name and version from crates.io
pub async fn fetch_crate_cratesio(
client: &Client,
crates_io_api_client: &AsyncClient,
name: &str,
version_req: &VersionReq,
) -> Result<Manifest<Meta>, BinstallError> {
// Fetch / update index
debug!("Looking up crate information");
// Fetch online crate information
let base_info = crates_io_api_client
.get_crate(name.as_ref())
.await
.map_err(|err| BinstallError::CratesIoApi {
crate_name: name.into(),
err,
})?;
// Locate matching version
let version_iter = base_info.versions.iter().filter(|v| !v.yanked);
let (version, version_name) = find_version(version_req, version_iter)?;
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);
debug!("Fetching crate from: {crate_url} and extracting Cargo.toml from it");
let manifest_dir_path: PathBuf = format!("{name}-{version_name}").into();
download_tar_based_and_visit(
client,
Url::parse(&crate_url)?,
TarBasedFmt::Tgz,
ManifestVisitor::new(manifest_dir_path),
)
.await
}

View file

@ -0,0 +1,53 @@
use std::{
collections::{hash_map::HashMap, hash_set::HashSet},
io,
path::Path,
};
use cargo_toml::AbstractFilesystem;
use normalize_path::NormalizePath;
/// This type stores the filesystem structure for the crate tarball
/// extracted in memory and can be passed to
/// `cargo_toml::Manifest::complete_from_abstract_filesystem`.
#[derive(Debug)]
pub(super) struct Vfs(HashMap<Box<Path>, HashSet<Box<str>>>);
impl Vfs {
pub(super) fn new() -> Self {
Self(HashMap::with_capacity(16))
}
/// * `path` - must be canonical, must not be empty.
pub(super) fn add_path(&mut self, mut path: &Path) {
while let Some(parent) = path.parent() {
// Since path has parent, it must have a filename
let filename = path.file_name().unwrap();
// `cargo_toml`'s implementation does the same thing.
// https://docs.rs/cargo_toml/0.11.5/src/cargo_toml/afs.rs.html#24
let filename = filename.to_string_lossy();
self.0
.entry(parent.into())
.or_insert_with(|| HashSet::with_capacity(4))
.insert(filename.into());
path = parent;
}
}
}
impl AbstractFilesystem for Vfs {
fn file_names_in(&self, rel_path: &str) -> io::Result<HashSet<Box<str>>> {
let rel_path = Path::new(rel_path).normalize();
Ok(self.0.get(&*rel_path).map(Clone::clone).unwrap_or_default())
}
}
impl AbstractFilesystem for &Vfs {
fn file_names_in(&self, rel_path: &str) -> io::Result<HashSet<Box<str>>> {
(*self).file_names_in(rel_path)
}
}

View file

@ -0,0 +1,86 @@
use std::{
io::Read,
path::{Path, PathBuf},
};
use cargo_toml::Manifest;
use log::debug;
use normalize_path::NormalizePath;
use tar::Entries;
use super::vfs::Vfs;
use crate::{
errors::BinstallError, helpers::async_extracter::TarEntriesVisitor,
manifests::cargo_toml_binstall::Meta,
};
#[derive(Debug)]
pub(super) struct ManifestVisitor {
cargo_toml_content: Vec<u8>,
/// manifest_dir_path is treated as the current dir.
manifest_dir_path: PathBuf,
vfs: Vfs,
}
impl ManifestVisitor {
pub(super) fn new(manifest_dir_path: PathBuf) -> Self {
Self {
// Cargo.toml is quite large usually.
cargo_toml_content: Vec::with_capacity(2000),
manifest_dir_path,
vfs: Vfs::new(),
}
}
}
impl TarEntriesVisitor for ManifestVisitor {
type Target = Manifest<Meta>;
fn visit<R: Read>(&mut self, entries: Entries<'_, R>) -> Result<(), BinstallError> {
for res in entries {
let mut entry = res?;
let path = entry.path()?;
let path = path.normalize();
let path = if let Ok(path) = path.strip_prefix(&self.manifest_dir_path) {
path
} else {
// The path is outside of the curr dir (manifest dir),
// ignore it.
continue;
};
if path == Path::new("Cargo.toml")
|| path == Path::new("src/main.rs")
|| path.starts_with("src/bin")
{
self.vfs.add_path(path);
}
if path == Path::new("Cargo.toml") {
// Since it is possible for the same Cargo.toml to appear
// multiple times using `tar --keep-old-files`, here we
// clear the buffer first before reading into it.
self.cargo_toml_content.clear();
entry.read_to_end(&mut self.cargo_toml_content)?;
}
}
Ok(())
}
/// Load binstall metadata using the extracted information stored in memory.
fn finish(self) -> Result<Self::Target, BinstallError> {
debug!("Loading manifest directly from extracted file");
// Load and parse manifest
let mut manifest = Manifest::from_slice_with_metadata(&self.cargo_toml_content)?;
// Checks vfs for binary output names
manifest.complete_from_abstract_filesystem(&self.vfs)?;
// Return metadata
Ok(manifest)
}
}

View file

@ -0,0 +1,52 @@
use log::debug;
use semver::VersionReq;
use crate::errors::BinstallError;
pub(super) trait Version {
/// Return `None` on error.
fn get_version(&self) -> Option<semver::Version>;
}
impl<T: Version> Version for &T {
fn get_version(&self) -> Option<semver::Version> {
(*self).get_version()
}
}
impl Version for crates_io_api::Version {
fn get_version(&self) -> Option<semver::Version> {
// Remove leading `v` for git tags
let ver_str = match self.num.strip_prefix('v') {
Some(v) => v,
None => &self.num,
};
// Parse out version
semver::Version::parse(ver_str).ok()
}
}
pub(super) fn find_version<Item: Version, VersionIter: Iterator<Item = Item>>(
version_req: &VersionReq,
version_iter: VersionIter,
) -> Result<(Item, semver::Version), BinstallError> {
version_iter
// Filter for matching versions
.filter_map(|item| {
let ver = item.get_version()?;
// Filter by version match
if version_req.matches(&ver) {
debug!("Version: {:?}", ver);
Some((item, ver))
} else {
None
}
})
// Return highest version
.max_by_key(|(_item, ver)| ver.clone())
.ok_or(BinstallError::VersionMismatch {
req: version_req.clone(),
})
}

373
crates/lib/src/errors.rs Normal file
View file

@ -0,0 +1,373 @@
use std::{
path::PathBuf,
process::{ExitCode, ExitStatus, Termination},
};
use compact_str::CompactString;
use log::{error, warn};
use miette::{Diagnostic, Report};
use thiserror::Error;
use tokio::task;
/// Error kinds emitted by cargo-binstall.
#[derive(Error, Diagnostic, Debug)]
#[non_exhaustive]
pub enum BinstallError {
/// Internal: a task could not be joined.
///
/// - Code: `binstall::internal::task_join`
/// - Exit: 17
#[error(transparent)]
#[diagnostic(severity(error), code(binstall::internal::task_join))]
TaskJoinError(#[from] task::JoinError),
/// The installation was cancelled by a user at a confirmation prompt,
/// or user send a ctrl_c on all platforms or
/// `SIGINT`, `SIGHUP`, `SIGTERM` or `SIGQUIT` on unix to the program.
///
/// - Code: `binstall::user_abort`
/// - Exit: 32
#[error("installation cancelled by user")]
#[diagnostic(severity(info), code(binstall::user_abort))]
UserAbort,
/// A URL is invalid.
///
/// This may be the result of a template in a Cargo manifest.
///
/// - Code: `binstall::url_parse`
/// - Exit: 65
#[error(transparent)]
#[diagnostic(severity(error), code(binstall::url_parse))]
UrlParse(#[from] url::ParseError),
/// An error while unzipping a file.
///
/// - Code: `binstall::unzip`
/// - Exit: 66
#[error(transparent)]
#[diagnostic(severity(error), code(binstall::unzip))]
Unzip(#[from] zip::result::ZipError),
/// A rendering error in a template.
///
/// - Code: `binstall::template`
/// - Exit: 67
#[error(transparent)]
#[diagnostic(severity(error), code(binstall::template))]
Template(#[from] tinytemplate::error::Error),
/// A generic error from our HTTP client, reqwest.
///
/// Errors resulting from HTTP fetches are handled with [`BinstallError::Http`] instead.
///
/// - Code: `binstall::reqwest`
/// - Exit: 68
#[error(transparent)]
#[diagnostic(severity(error), code(binstall::reqwest))]
Reqwest(#[from] reqwest::Error),
/// An HTTP request failed.
///
/// This includes both connection/transport failures and when the HTTP status of the response
/// is not as expected.
///
/// - Code: `binstall::http`
/// - Exit: 69
#[error("could not {method} {url}")]
#[diagnostic(severity(error), code(binstall::http))]
Http {
method: reqwest::Method,
url: url::Url,
#[source]
err: reqwest::Error,
},
/// A subprocess failed.
///
/// This is often about cargo-install calls.
///
/// - Code: `binstall::subprocess`
/// - Exit: 70
#[error("subprocess {command} errored with {status}")]
#[diagnostic(severity(error), code(binstall::subprocess))]
SubProcess { command: String, status: ExitStatus },
/// A generic I/O error.
///
/// - Code: `binstall::io`
/// - Exit: 74
#[error(transparent)]
#[diagnostic(severity(error), code(binstall::io))]
Io(std::io::Error),
/// An error interacting with the crates.io API.
///
/// This could either be a "not found" or a server/transport error.
///
/// - Code: `binstall::crates_io_api`
/// - Exit: 76
#[error("crates.io API error")]
#[diagnostic(
severity(error),
code(binstall::crates_io_api),
help("Check that the crate name you provided is correct.\nYou can also search for a matching crate at: https://lib.rs/search?q={crate_name}")
)]
CratesIoApi {
crate_name: String,
#[source]
err: crates_io_api::Error,
},
/// The override path to the cargo manifest is invalid or cannot be resolved.
///
/// - Code: `binstall::cargo_manifest_path`
/// - Exit: 77
#[error("the --manifest-path is invalid or cannot be resolved")]
#[diagnostic(severity(error), code(binstall::cargo_manifest_path))]
CargoManifestPath,
/// A parsing or validation error in a cargo manifest.
///
/// This should be rare, as manifests are generally fetched from crates.io, which does its own
/// validation upstream. The most common failure will therefore be for direct repository access
/// and with the `--manifest-path` option.
///
/// - Code: `binstall::cargo_manifest`
/// - Exit: 78
#[error(transparent)]
#[diagnostic(
severity(error),
code(binstall::cargo_manifest),
help("If you used --manifest-path, check the Cargo.toml syntax.")
)]
CargoManifest(#[from] cargo_toml::Error),
/// A version is not valid semver.
///
/// Note that we use the [`semver`] crate, which parses Cargo version syntax; this may be
/// somewhat stricter or very slightly different from other semver implementations.
///
/// - Code: `binstall::version::parse`
/// - Exit: 80
#[error("version string '{v}' is not semver")]
#[diagnostic(severity(error), code(binstall::version::parse))]
VersionParse {
v: String,
#[source]
err: semver::Error,
},
/// A version requirement is not valid.
///
/// This is usually provided via the `--version` option.
///
/// Note that we use the [`semver`] crate, which parses Cargo version requirement syntax; they
/// may be slightly different from other semver requirements expressions implementations.
///
/// - Code: `binstall::version::requirement`
/// - Exit: 81
#[error("version requirement '{req}' is not semver")]
#[diagnostic(severity(error), code(binstall::version::requirement))]
VersionReq {
req: String,
#[source]
err: semver::Error,
},
/// No available version matches the requirements.
///
/// This may be the case when using the `--version` option.
///
/// Note that using `--version 1.2.3` is interpreted as the requirement `=1.2.3`.
///
/// - Code: `binstall::version::mismatch`
/// - Exit: 82
#[error("no version matching requirement '{req}'")]
#[diagnostic(severity(error), code(binstall::version::mismatch))]
VersionMismatch { req: semver::VersionReq },
/// The crates.io API doesn't have manifest metadata for the given version.
///
/// - Code: `binstall::version::unavailable`
/// - Exit: 83
#[error("no crate information available for '{crate_name}' version '{v}'")]
#[diagnostic(severity(error), code(binstall::version::unavailable))]
VersionUnavailable {
crate_name: String,
v: semver::Version,
},
/// The crate@version syntax was used at the same time as the --version option.
///
/// You can't do that as it's ambiguous which should apply.
///
/// - Code: `binstall::conflict::version`
/// - Exit: 84
#[error("superfluous version specification")]
#[diagnostic(
severity(error),
code(binstall::conflict::version),
help("You cannot use both crate@version and the --version option. Remove one.")
)]
SuperfluousVersionOption,
/// An override option is used when multiple packages are to be installed.
///
/// This is raised when more than one package name is provided and any of:
///
/// - `--version`
/// - `--manifest-path`
/// - `--bin-dir`
/// - `--pkg-fmt`
/// - `--pkg-url`
///
/// is provided.
///
/// - Code: `binstall::conflict::overrides`
/// - Exit: 85
#[error("override option used with multi package syntax")]
#[diagnostic(
severity(error),
code(binstall::conflict::overrides),
help("You cannot use --{option} and specify multiple packages at the same time. Do one or the other.")
)]
OverrideOptionUsedWithMultiInstall { option: &'static str },
/// No binaries were found for the crate.
///
/// When installing, either the binaries are specified in the crate's Cargo.toml, or they're
/// inferred from the crate layout (e.g. src/main.rs or src/bins/name.rs). If no binaries are
/// found through these methods, we can't know what to install!
///
/// - Code: `binstall::resolve::binaries`
/// - Exit: 86
#[error("no binaries specified nor inferred")]
#[diagnostic(
severity(error),
code(binstall::resolve::binaries),
help("This crate doesn't specify any binaries, so there's nothing to install.")
)]
UnspecifiedBinaries,
/// No viable targets were found.
///
/// When installing, we attempt to find which targets the host (your computer) supports, and
/// discover builds for these targets from the remote binary source. This error occurs when we
/// fail to discover the host's target.
///
/// You should in this case specify --target manually.
///
/// - Code: `binstall::targets::none_host`
/// - Exit: 87
#[error("failed to discovered a viable target from the host")]
#[diagnostic(
severity(error),
code(binstall::targets::none_host),
help("Try to specify --target")
)]
NoViableTargets,
/// Bin file is not found.
///
/// - Code: `binstall::binfile`
/// - Exit: 88
#[error("bin file {0} not found")]
#[diagnostic(severity(error), code(binstall::binfile))]
BinFileNotFound(PathBuf),
/// A wrapped error providing the context of which crate the error is about.
#[error("for crate {crate_name}")]
CrateContext {
#[source]
error: Box<BinstallError>,
crate_name: CompactString,
},
}
impl BinstallError {
fn exit_number(&self) -> u8 {
use BinstallError::*;
let code: u8 = match self {
TaskJoinError(_) => 17,
UserAbort => 32,
UrlParse(_) => 65,
Unzip(_) => 66,
Template(_) => 67,
Reqwest(_) => 68,
Http { .. } => 69,
SubProcess { .. } => 70,
Io(_) => 74,
CratesIoApi { .. } => 76,
CargoManifestPath => 77,
CargoManifest { .. } => 78,
VersionParse { .. } => 80,
VersionReq { .. } => 81,
VersionMismatch { .. } => 82,
VersionUnavailable { .. } => 83,
SuperfluousVersionOption => 84,
OverrideOptionUsedWithMultiInstall { .. } => 85,
UnspecifiedBinaries => 86,
NoViableTargets => 87,
BinFileNotFound(_) => 88,
CrateContext { error, .. } => error.exit_number(),
};
// reserved codes
debug_assert!(code != 64 && code != 16 && code != 1 && code != 2 && code != 0);
code
}
/// The recommended exit code for this error.
///
/// This will never output:
/// - 0 (success)
/// - 1 and 2 (catchall and shell)
/// - 16 (binstall errors not handled here)
/// - 64 (generic error)
pub fn exit_code(&self) -> ExitCode {
self.exit_number().into()
}
/// Add crate context to the error
pub fn crate_context(self, crate_name: impl Into<CompactString>) -> Self {
Self::CrateContext {
error: Box::new(self),
crate_name: crate_name.into(),
}
}
}
impl Termination for BinstallError {
fn report(self) -> ExitCode {
let code = self.exit_code();
if let BinstallError::UserAbort = self {
warn!("Installation cancelled");
} else {
error!("Fatal error:");
eprintln!("{:?}", Report::new(self));
}
code
}
}
impl From<std::io::Error> for BinstallError {
fn from(err: std::io::Error) -> Self {
if err.get_ref().is_some() {
let kind = err.kind();
let inner = err
.into_inner()
.expect("err.get_ref() returns Some, so err.into_inner() should also return Some");
inner
.downcast()
.map(|b| *b)
.unwrap_or_else(|err| BinstallError::Io(std::io::Error::new(kind, err)))
} else {
BinstallError::Io(err)
}
}
}

View file

@ -0,0 +1,98 @@
use std::{path::Path, sync::Arc};
use compact_str::CompactString;
pub use gh_crate_meta::*;
pub use log::debug;
pub use quickinstall::*;
use reqwest::Client;
use crate::{
errors::BinstallError,
helpers::tasks::AutoAbortJoinHandle,
manifests::cargo_toml_binstall::{PkgFmt, PkgMeta},
};
mod gh_crate_meta;
mod quickinstall;
#[async_trait::async_trait]
pub trait Fetcher: Send + Sync {
/// Create a new fetcher from some data
async fn new(client: &Client, data: &Data) -> Arc<Self>
where
Self: Sized;
/// Fetch a package and extract
async fn fetch_and_extract(&self, dst: &Path) -> Result<(), BinstallError>;
/// Find the package, if it is available for download
///
/// This may look for multiple remote targets, but must write (using some form of interior
/// mutability) the best one to the implementing struct in some way so `fetch_and_extract` can
/// proceed without additional work.
///
/// Must return `true` if a package is available, `false` if none is, and reserve errors to
/// fatal conditions only.
async fn find(&self) -> Result<bool, BinstallError>;
/// Return the package format
fn pkg_fmt(&self) -> PkgFmt;
/// A short human-readable name or descriptor for the package source
fn source_name(&self) -> CompactString;
/// Should return true if the remote is from a third-party source
fn is_third_party(&self) -> bool;
/// Return the target for this fetcher
fn target(&self) -> &str;
}
/// Data required to fetch a package
#[derive(Clone, Debug)]
pub struct Data {
pub name: String,
pub target: String,
pub version: String,
pub repo: Option<String>,
pub meta: PkgMeta,
}
type FetcherJoinHandle = AutoAbortJoinHandle<Result<bool, BinstallError>>;
#[derive(Default)]
pub struct MultiFetcher(Vec<(Arc<dyn Fetcher>, FetcherJoinHandle)>);
impl MultiFetcher {
pub fn add(&mut self, fetcher: Arc<dyn Fetcher>) {
self.0.push((
fetcher.clone(),
AutoAbortJoinHandle::spawn(async move { fetcher.find().await }),
));
}
pub async fn first_available(self) -> Option<Arc<dyn Fetcher>> {
for (fetcher, handle) in self.0 {
match handle.await {
Ok(Ok(true)) => return Some(fetcher),
Ok(Ok(false)) => (),
Ok(Err(err)) => {
debug!(
"Error while checking fetcher {}: {}",
fetcher.source_name(),
err
);
}
Err(join_err) => {
debug!(
"Error while joining the task that checks the fetcher {}: {}",
fetcher.source_name(),
join_err
);
}
}
}
None
}
}

View file

@ -0,0 +1,315 @@
use std::{path::Path, sync::Arc};
use compact_str::{CompactString, ToCompactString};
use log::{debug, warn};
use once_cell::sync::OnceCell;
use reqwest::{Client, Method};
use serde::Serialize;
use tinytemplate::TinyTemplate;
use url::Url;
use crate::{
errors::BinstallError,
helpers::{download::download_and_extract, remote::remote_exists, tasks::AutoAbortJoinHandle},
manifests::cargo_toml_binstall::PkgFmt,
};
use super::Data;
pub struct GhCrateMeta {
client: Client,
data: Data,
url: OnceCell<Url>,
}
#[async_trait::async_trait]
impl super::Fetcher for GhCrateMeta {
async fn new(client: &Client, data: &Data) -> Arc<Self> {
Arc::new(Self {
client: client.clone(),
data: data.clone(),
url: OnceCell::new(),
})
}
async fn find(&self) -> Result<bool, BinstallError> {
// build up list of potential URLs
let urls = self.data.meta.pkg_fmt.extensions().iter().map(|ext| {
let ctx = Context::from_data(&self.data, ext);
ctx.render_url(&self.data.meta.pkg_url)
});
// go check all potential URLs at once
let checks = urls
.map(|url| {
let client = self.client.clone();
AutoAbortJoinHandle::spawn(async move {
let url = url?;
debug!("Checking for package at: '{url}'");
remote_exists(client, url.clone(), Method::HEAD)
.await
.map(|exists| (url.clone(), exists))
})
})
.collect::<Vec<_>>();
// get the first URL that exists
for check in checks {
let (url, exists) = check.await??;
if exists {
if url.scheme() != "https" {
warn!(
"URL is not HTTPS! This may become a hard error in the future, tell the upstream!"
);
}
debug!("Winning URL is {url}");
self.url.set(url).unwrap(); // find() is called first
return Ok(true);
}
}
Ok(false)
}
async fn fetch_and_extract(&self, dst: &Path) -> Result<(), BinstallError> {
let url = self.url.get().unwrap(); // find() is called first
debug!("Downloading package from: '{url}'");
download_and_extract(&self.client, url, self.pkg_fmt(), dst).await
}
fn pkg_fmt(&self) -> PkgFmt {
self.data.meta.pkg_fmt
}
fn source_name(&self) -> CompactString {
self.url
.get()
.map(|url| {
if let Some(domain) = url.domain() {
domain.to_compact_string()
} else if let Some(host) = url.host_str() {
host.to_compact_string()
} else {
url.to_compact_string()
}
})
.unwrap_or_else(|| "invalid url".into())
}
fn is_third_party(&self) -> bool {
false
}
fn target(&self) -> &str {
&self.data.target
}
}
/// 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,
/// Soft-deprecated alias for archive-format
pub format: &'c str,
/// Archive format e.g. tar.gz, zip
#[serde(rename = "archive-format")]
pub archive_format: &'c str,
/// Filename extension on the binary, i.e. .exe on Windows, nothing otherwise
#[serde(rename = "binary-ext")]
pub binary_ext: &'c str,
}
impl<'c> Context<'c> {
pub(self) fn from_data(data: &'c Data, archive_format: &'c str) -> Self {
Self {
name: &data.name,
repo: data.repo.as_ref().map(|s| &s[..]),
target: &data.target,
version: &data.version,
format: archive_format,
archive_format,
binary_ext: if data.target.contains("windows") {
".exe"
} else {
""
},
}
}
pub(self) fn render_url(&self, template: &str) -> Result<Url, BinstallError> {
debug!("Render {template:?} using context: {:?}", self);
let mut tt = TinyTemplate::new();
tt.add_template("path", template)?;
Ok(Url::parse(&tt.render("path", self)?)?)
}
}
#[cfg(test)]
mod test {
use crate::manifests::cargo_toml_binstall::{PkgFmt, PkgMeta};
use super::{super::Data, Context};
use url::Url;
fn url(s: &str) -> Url {
Url::parse(s).unwrap()
}
#[test]
fn defaults() {
let meta = PkgMeta::default();
let data = Data {
name: "cargo-binstall".to_string(),
target: "x86_64-unknown-linux-gnu".to_string(),
version: "1.2.3".to_string(),
repo: Some("https://github.com/ryankurte/cargo-binstall".to_string()),
meta,
};
let ctx = Context::from_data(&data, "tgz");
assert_eq!(
ctx.render_url(&data.meta.pkg_url).unwrap(),
url("https://github.com/ryankurte/cargo-binstall/releases/download/v1.2.3/cargo-binstall-x86_64-unknown-linux-gnu-v1.2.3.tgz")
);
}
#[test]
#[should_panic]
fn no_repo() {
let meta = PkgMeta::default();
let data = Data {
name: "cargo-binstall".to_string(),
target: "x86_64-unknown-linux-gnu".to_string(),
version: "1.2.3".to_string(),
repo: None,
meta,
};
let ctx = Context::from_data(&data, "tgz");
ctx.render_url(&data.meta.pkg_url).unwrap();
}
#[test]
fn no_repo_but_full_url() {
let meta = PkgMeta {
pkg_url: format!("https://example.com{}", PkgMeta::default().pkg_url),
..Default::default()
};
let data = Data {
name: "cargo-binstall".to_string(),
target: "x86_64-unknown-linux-gnu".to_string(),
version: "1.2.3".to_string(),
repo: None,
meta,
};
let ctx = Context::from_data(&data, "tgz");
assert_eq!(
ctx.render_url(&data.meta.pkg_url).unwrap(),
url("https://example.com/releases/download/v1.2.3/cargo-binstall-x86_64-unknown-linux-gnu-v1.2.3.tgz")
);
}
#[test]
fn different_url() {
let meta = PkgMeta {
pkg_url:
"{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ archive-format }"
.into(),
..Default::default()
};
let data = Data {
name: "radio-sx128x".to_string(),
target: "x86_64-unknown-linux-gnu".to_string(),
version: "0.14.1-alpha.5".to_string(),
repo: Some("https://github.com/rust-iot/rust-radio-sx128x".to_string()),
meta,
};
let ctx = Context::from_data(&data, "tgz");
assert_eq!(
ctx.render_url(&data.meta.pkg_url).unwrap(),
url("https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/sx128x-util-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz")
);
}
#[test]
fn deprecated_format() {
let meta = PkgMeta {
pkg_url: "{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ format }".into(),
..Default::default()
};
let data = Data {
name: "radio-sx128x".to_string(),
target: "x86_64-unknown-linux-gnu".to_string(),
version: "0.14.1-alpha.5".to_string(),
repo: Some("https://github.com/rust-iot/rust-radio-sx128x".to_string()),
meta,
};
let ctx = Context::from_data(&data, "tgz");
assert_eq!(
ctx.render_url(&data.meta.pkg_url).unwrap(),
url("https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/sx128x-util-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz")
);
}
#[test]
fn different_ext() {
let meta = PkgMeta {
pkg_url:
"{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }.tar.xz"
.into(),
pkg_fmt: PkgFmt::Txz,
..Default::default()
};
let data = Data {
name: "cargo-watch".to_string(),
target: "aarch64-apple-darwin".to_string(),
version: "9.0.0".to_string(),
repo: Some("https://github.com/watchexec/cargo-watch".to_string()),
meta,
};
let ctx = Context::from_data(&data, "txz");
assert_eq!(
ctx.render_url(&data.meta.pkg_url).unwrap(),
url("https://github.com/watchexec/cargo-watch/releases/download/v9.0.0/cargo-watch-v9.0.0-aarch64-apple-darwin.tar.xz")
);
}
#[test]
fn no_archive() {
let meta = PkgMeta {
pkg_url: "{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }{ binary-ext }".into(),
pkg_fmt: PkgFmt::Bin,
..Default::default()
};
let data = Data {
name: "cargo-watch".to_string(),
target: "aarch64-pc-windows-msvc".to_string(),
version: "9.0.0".to_string(),
repo: Some("https://github.com/watchexec/cargo-watch".to_string()),
meta,
};
let ctx = Context::from_data(&data, "bin");
assert_eq!(
ctx.render_url(&data.meta.pkg_url).unwrap(),
url("https://github.com/watchexec/cargo-watch/releases/download/v9.0.0/cargo-watch-v9.0.0-aarch64-pc-windows-msvc.exe")
);
}
}

View file

@ -0,0 +1,113 @@
use std::{path::Path, sync::Arc};
use compact_str::CompactString;
use log::debug;
use reqwest::Client;
use reqwest::Method;
use tokio::task::JoinHandle;
use url::Url;
use crate::{
errors::BinstallError,
helpers::{download::download_and_extract, remote::remote_exists},
manifests::cargo_toml_binstall::PkgFmt,
};
use super::Data;
const BASE_URL: &str = "https://github.com/alsuren/cargo-quickinstall/releases/download";
const STATS_URL: &str = "https://warehouse-clerk-tmp.vercel.app/api/crate";
pub struct QuickInstall {
client: Client,
package: String,
target: String,
}
#[async_trait::async_trait]
impl super::Fetcher for QuickInstall {
async fn new(client: &Client, data: &Data) -> Arc<Self> {
let crate_name = &data.name;
let version = &data.version;
let target = data.target.clone();
Arc::new(Self {
client: client.clone(),
package: format!("{crate_name}-{version}-{target}"),
target,
})
}
async fn find(&self) -> Result<bool, BinstallError> {
let url = self.package_url();
self.report();
debug!("Checking for package at: '{url}'");
remote_exists(self.client.clone(), Url::parse(&url)?, Method::HEAD).await
}
async fn fetch_and_extract(&self, dst: &Path) -> Result<(), BinstallError> {
let url = self.package_url();
debug!("Downloading package from: '{url}'");
download_and_extract(&self.client, &Url::parse(&url)?, self.pkg_fmt(), dst).await
}
fn pkg_fmt(&self) -> PkgFmt {
PkgFmt::Tgz
}
fn source_name(&self) -> CompactString {
CompactString::from("QuickInstall")
}
fn is_third_party(&self) -> bool {
true
}
fn target(&self) -> &str {
&self.target
}
}
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 fn report(&self) -> JoinHandle<Result<(), BinstallError>> {
let stats_url = self.stats_url();
let client = self.client.clone();
tokio::spawn(async move {
if cfg!(debug_assertions) {
debug!("Not sending quickinstall report in debug mode");
return Ok(());
}
let url = Url::parse(&stats_url)?;
debug!("Sending installation report to quickinstall ({url})");
client
.request(Method::HEAD, url.clone())
.send()
.await
.map_err(|err| BinstallError::Http {
method: Method::HEAD,
url,
err,
})?;
Ok(())
})
}
}

103
crates/lib/src/fs.rs Normal file
View file

@ -0,0 +1,103 @@
use std::{fs, io, path::Path};
use log::debug;
use tempfile::NamedTempFile;
/// Returned file is readable and writable.
pub fn create_if_not_exist(path: impl AsRef<Path>) -> io::Result<fs::File> {
let path = path.as_ref();
let mut options = fs::File::options();
options.read(true).write(true);
options
.clone()
.create_new(true)
.open(path)
.or_else(|_| options.open(path))
}
/// Atomically install a file.
///
/// This is a blocking function, must be called in `block_in_place` mode.
pub fn atomic_install(src: &Path, dst: &Path) -> io::Result<()> {
debug!(
"Attempting to atomically rename from '{}' to '{}'",
src.display(),
dst.display()
);
if let Err(err) = fs::rename(src, dst) {
debug!("Attempting at atomic rename failed: {err:#?}, fallback to creating tempfile.");
// src and dst is not on the same filesystem/mountpoint.
// Fallback to creating NamedTempFile on the parent dir of
// dst.
let mut src_file = fs::File::open(src)?;
let parent = dst.parent().unwrap();
debug!("Creating named tempfile at '{}'", parent.display());
let mut tempfile = NamedTempFile::new_in(parent)?;
debug!(
"Copying from '{}' to '{}'",
src.display(),
tempfile.path().display()
);
io::copy(&mut src_file, tempfile.as_file_mut())?;
debug!("Retrieving permissions of '{}'", src.display());
let permissions = src_file.metadata()?.permissions();
debug!(
"Setting permissions of '{}' to '{permissions:#?}'",
tempfile.path().display()
);
tempfile.as_file().set_permissions(permissions)?;
debug!(
"Persisting '{}' to '{}'",
tempfile.path().display(),
dst.display()
);
tempfile.persist(dst).map_err(io::Error::from)?;
} else {
debug!("Attempting at atomically succeeded.");
}
Ok(())
}
fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
#[cfg(target_family = "unix")]
let f = std::os::unix::fs::symlink;
#[cfg(target_family = "windows")]
let f = std::os::windows::fs::symlink_file;
f(original, link)
}
/// Atomically install symlink "link" to a file "dst".
///
/// This is a blocking function, must be called in `block_in_place` mode.
pub fn atomic_symlink_file(dest: &Path, link: &Path) -> io::Result<()> {
let parent = link.parent().unwrap();
debug!("Creating tempPath at '{}'", parent.display());
let temp_path = NamedTempFile::new_in(parent)?.into_temp_path();
fs::remove_file(&temp_path)?;
debug!(
"Creating symlink '{}' to file '{}'",
temp_path.display(),
dest.display()
);
symlink_file(dest, &temp_path)?;
debug!(
"Persisting '{}' to '{}'",
temp_path.display(),
link.display()
);
temp_path.persist(link).map_err(io::Error::from)
}

View file

@ -0,0 +1,9 @@
pub mod async_extracter;
pub mod download;
pub mod extracter;
pub mod jobserver_client;
pub mod remote;
pub mod signal;
pub mod statics;
pub mod stream_readable;
pub mod tasks;

View file

@ -0,0 +1,115 @@
use std::{
fmt::Debug,
fs,
io::{copy, Read, Seek},
path::Path,
};
use bytes::Bytes;
use futures_util::stream::Stream;
use log::debug;
use scopeguard::{guard, ScopeGuard};
use tar::Entries;
use tempfile::tempfile;
use tokio::task::block_in_place;
use crate::{errors::BinstallError, manifests::cargo_toml_binstall::TarBasedFmt};
use super::{extracter::*, stream_readable::StreamReadable};
pub async fn extract_bin<S, E>(stream: S, path: &Path) -> Result<(), BinstallError>
where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
BinstallError: From<E>,
{
let mut reader = StreamReadable::new(stream).await;
block_in_place(move || {
fs::create_dir_all(path.parent().unwrap())?;
let mut file = fs::File::create(&path)?;
// remove it unless the operation isn't aborted and no write
// fails.
let remove_guard = guard(&path, |path| {
fs::remove_file(path).ok();
});
copy(&mut reader, &mut file)?;
// Operation isn't aborted and all writes succeed,
// disarm the remove_guard.
ScopeGuard::into_inner(remove_guard);
Ok(())
})
}
pub async fn extract_zip<S, E>(stream: S, path: &Path) -> Result<(), BinstallError>
where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
BinstallError: From<E>,
{
let mut reader = StreamReadable::new(stream).await;
block_in_place(move || {
fs::create_dir_all(path.parent().unwrap())?;
let mut file = tempfile()?;
copy(&mut reader, &mut file)?;
// rewind it so that we can pass it to unzip
file.rewind()?;
unzip(file, path)
})
}
pub async fn extract_tar_based_stream<S, E>(
stream: S,
path: &Path,
fmt: TarBasedFmt,
) -> Result<(), BinstallError>
where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
BinstallError: From<E>,
{
let reader = StreamReadable::new(stream).await;
block_in_place(move || {
fs::create_dir_all(path.parent().unwrap())?;
debug!("Extracting from {fmt} archive to {path:#?}");
create_tar_decoder(reader, fmt)?.unpack(path)?;
Ok(())
})
}
/// Visitor must iterate over all entries.
/// Entires can be in arbitary order.
pub trait TarEntriesVisitor {
type Target;
fn visit<R: Read>(&mut self, entries: Entries<'_, R>) -> Result<(), BinstallError>;
fn finish(self) -> Result<Self::Target, BinstallError>;
}
pub async fn extract_tar_based_stream_and_visit<S, V, E>(
stream: S,
fmt: TarBasedFmt,
mut visitor: V,
) -> Result<V::Target, BinstallError>
where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
V: TarEntriesVisitor + Debug + Send + 'static,
BinstallError: From<E>,
{
let reader = StreamReadable::new(stream).await;
block_in_place(move || {
debug!("Extracting from {fmt} archive to process it in memory");
let mut tar = create_tar_decoder(reader, fmt)?;
visitor.visit(tar.entries()?)?;
visitor.finish()
})
}

View file

@ -0,0 +1,62 @@
use std::{fmt::Debug, path::Path};
use log::debug;
use reqwest::{Client, Url};
use crate::{
errors::BinstallError,
helpers::{
async_extracter::{
extract_bin, extract_tar_based_stream, extract_tar_based_stream_and_visit, extract_zip,
},
remote::create_request,
},
manifests::cargo_toml_binstall::{PkgFmt, PkgFmtDecomposed, TarBasedFmt},
};
use super::async_extracter::TarEntriesVisitor;
/// Download a file from the provided URL and extract it to the provided path.
pub async fn download_and_extract<P: AsRef<Path>>(
client: &Client,
url: &Url,
fmt: PkgFmt,
path: P,
) -> Result<(), BinstallError> {
let stream = create_request(client, url.clone()).await?;
let path = path.as_ref();
debug!("Downloading and extracting to: '{}'", path.display());
match fmt.decompose() {
PkgFmtDecomposed::Tar(fmt) => extract_tar_based_stream(stream, path, fmt).await?,
PkgFmtDecomposed::Bin => extract_bin(stream, path).await?,
PkgFmtDecomposed::Zip => extract_zip(stream, path).await?,
}
debug!("Download OK, extracted to: '{}'", path.display());
Ok(())
}
/// Download a file from the provided URL and extract part of it to
/// the provided path.
///
/// * `filter` - If Some, then it will pass the path of the file to it
/// and only extract ones which filter returns `true`.
pub async fn download_tar_based_and_visit<V: TarEntriesVisitor + Debug + Send + 'static>(
client: &Client,
url: Url,
fmt: TarBasedFmt,
visitor: V,
) -> Result<V::Target, BinstallError> {
let stream = create_request(client, url).await?;
debug!("Downloading and extracting then in-memory processing");
let ret = extract_tar_based_stream_and_visit(stream, fmt, visitor).await?;
debug!("Download, extraction and in-memory procession OK");
Ok(ret)
}

View file

@ -0,0 +1,46 @@
use std::{
fs::File,
io::{self, BufRead, Read},
path::Path,
};
use bzip2::bufread::BzDecoder;
use flate2::bufread::GzDecoder;
use log::debug;
use tar::Archive;
use xz2::bufread::XzDecoder;
use zip::read::ZipArchive;
use zstd::stream::Decoder as ZstdDecoder;
use crate::{errors::BinstallError, manifests::cargo_toml_binstall::TarBasedFmt};
pub(super) fn create_tar_decoder(
dat: impl BufRead + 'static,
fmt: TarBasedFmt,
) -> io::Result<Archive<Box<dyn Read>>> {
use TarBasedFmt::*;
let r: Box<dyn Read> = match fmt {
Tar => Box::new(dat),
Tbz2 => Box::new(BzDecoder::new(dat)),
Tgz => Box::new(GzDecoder::new(dat)),
Txz => Box::new(XzDecoder::new(dat)),
Tzstd => {
// The error can only come from raw::Decoder::with_dictionary as of zstd 0.10.2 and
// 0.11.2, which is specified as `&[]` by `ZstdDecoder::new`, thus `ZstdDecoder::new`
// should not return any error.
Box::new(ZstdDecoder::with_buffer(dat)?)
}
};
Ok(Archive::new(r))
}
pub(super) fn unzip(dat: File, dst: &Path) -> Result<(), BinstallError> {
debug!("Decompressing from zip archive to `{dst:?}`");
let mut zip = ZipArchive::new(dat)?;
zip.extract(dst)?;
Ok(())
}

View file

@ -0,0 +1,33 @@
use std::{num::NonZeroUsize, sync::Arc, thread::available_parallelism};
use jobserver::Client;
use tokio::sync::OnceCell;
use crate::errors::BinstallError;
#[derive(Clone)]
pub struct LazyJobserverClient(Arc<OnceCell<Client>>);
impl LazyJobserverClient {
/// This must be called at the start of the program since
/// `Client::from_env` requires that.
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
// Safety:
//
// Client::from_env is unsafe because from_raw_fd is unsafe.
// It doesn't do anything that is actually unsafe, like
// dereferencing pointer.
let opt = unsafe { Client::from_env() };
Self(Arc::new(OnceCell::new_with(opt)))
}
pub async fn get(&self) -> Result<&Client, BinstallError> {
self.0
.get_or_try_init(|| async {
let ncore = available_parallelism().map(NonZeroUsize::get).unwrap_or(1);
Ok(Client::new(ncore)?)
})
.await
}
}

View file

@ -0,0 +1,62 @@
use std::env;
use bytes::Bytes;
use futures_util::stream::Stream;
use log::debug;
use reqwest::{tls, Client, ClientBuilder, Method, Response};
use url::Url;
use crate::errors::BinstallError;
pub fn create_reqwest_client(
secure: bool,
min_tls: Option<tls::Version>,
) -> Result<Client, BinstallError> {
const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
let mut builder = ClientBuilder::new().user_agent(USER_AGENT);
if secure {
builder = builder
.https_only(true)
.min_tls_version(tls::Version::TLS_1_2);
}
if let Some(ver) = min_tls {
builder = builder.min_tls_version(ver);
}
Ok(builder.build()?)
}
pub async fn remote_exists(
client: Client,
url: Url,
method: Method,
) -> Result<bool, BinstallError> {
let req = client
.request(method.clone(), url.clone())
.send()
.await
.map_err(|err| BinstallError::Http { method, url, err })?;
Ok(req.status().is_success())
}
pub(crate) async fn create_request(
client: &Client,
url: Url,
) -> Result<impl Stream<Item = reqwest::Result<Bytes>>, BinstallError> {
debug!("Downloading from: '{url}'");
client
.get(url.clone())
.send()
.await
.and_then(|r| r.error_for_status())
.map_err(|err| BinstallError::Http {
method: Method::GET,
url,
err,
})
.map(Response::bytes_stream)
}

View file

@ -0,0 +1,82 @@
use std::io;
use futures_util::future::pending;
use tokio::signal;
use super::tasks::AutoAbortJoinHandle;
use crate::errors::BinstallError;
/// This function will poll the handle while listening for ctrl_c,
/// `SIGINT`, `SIGHUP`, `SIGTERM` and `SIGQUIT`.
///
/// When signal is received, [`BinstallError::UserAbort`] will be returned.
///
/// It would also ignore `SIGUSER1` and `SIGUSER2` on unix.
///
/// This function uses [`tokio::signal`] and once exit, does not reset the default
/// signal handler, so be careful when using it.
pub async fn cancel_on_user_sig_term<T>(
handle: AutoAbortJoinHandle<T>,
) -> Result<T, BinstallError> {
#[cfg(unix)]
unix::ignore_signals_on_unix()?;
tokio::select! {
res = handle => res,
res = wait_on_cancellation_signal() => {
res.map_err(BinstallError::Io).and(Err(BinstallError::UserAbort))
}
}
}
async fn wait_on_cancellation_signal() -> Result<(), io::Error> {
#[cfg(unix)]
async fn inner() -> Result<(), io::Error> {
unix::wait_on_cancellation_signal_unix().await
}
#[cfg(not(unix))]
async fn inner() -> Result<(), io::Error> {
// Use pending here so that tokio::select! would just skip this branch.
pending().await
}
tokio::select! {
res = signal::ctrl_c() => res,
res = inner() => res,
}
}
#[cfg(unix)]
mod unix {
use super::*;
use signal::unix::{signal, SignalKind};
/// Same as [`wait_on_cancellation_signal`] but is only available on unix.
pub async fn wait_on_cancellation_signal_unix() -> Result<(), io::Error> {
tokio::select! {
res = wait_for_signal_unix(SignalKind::interrupt()) => res,
res = wait_for_signal_unix(SignalKind::hangup()) => res,
res = wait_for_signal_unix(SignalKind::terminate()) => res,
res = wait_for_signal_unix(SignalKind::quit()) => res,
}
}
/// Wait for first arrival of signal.
pub async fn wait_for_signal_unix(kind: signal::unix::SignalKind) -> Result<(), io::Error> {
let mut sig_listener = signal::unix::signal(kind)?;
if sig_listener.recv().await.is_some() {
Ok(())
} else {
// Use pending() here for the same reason as above.
pending().await
}
}
pub fn ignore_signals_on_unix() -> Result<(), BinstallError> {
drop(signal(SignalKind::user_defined1())?);
drop(signal(SignalKind::user_defined2())?);
Ok(())
}
}

View file

@ -0,0 +1,23 @@
use std::{
io::Error,
ops::Deref,
path::{Path, PathBuf},
};
use once_cell::sync::{Lazy, OnceCell};
use url::Url;
pub fn cargo_home() -> Result<&'static Path, Error> {
static CARGO_HOME: OnceCell<PathBuf> = OnceCell::new();
CARGO_HOME
.get_or_try_init(home::cargo_home)
.map(Deref::deref)
}
pub fn cratesio_url() -> &'static Url {
static CRATESIO: Lazy<Url, fn() -> Url> =
Lazy::new(|| Url::parse("https://github.com/rust-lang/crates.io-index").unwrap());
&*CRATESIO
}

View file

@ -0,0 +1,83 @@
use std::{
cmp::min,
io::{self, BufRead, Read},
};
use bytes::{Buf, Bytes};
use futures_util::stream::{Stream, StreamExt};
use tokio::runtime::Handle;
use crate::errors::BinstallError;
/// This wraps an AsyncIterator as a `Read`able.
/// It must be used in non-async context only,
/// meaning you have to use it with
/// `tokio::task::{block_in_place, spawn_blocking}` or
/// `std::thread::spawn`.
#[derive(Debug)]
pub(super) struct StreamReadable<S> {
stream: S,
handle: Handle,
bytes: Bytes,
}
impl<S> StreamReadable<S> {
pub(super) async fn new(stream: S) -> Self {
Self {
stream,
handle: Handle::current(),
bytes: Bytes::new(),
}
}
}
impl<S, E> Read for StreamReadable<S>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
BinstallError: From<E>,
{
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
if self.fill_buf()?.is_empty() {
return Ok(0);
}
let bytes = &mut self.bytes;
// copy_to_slice requires the bytes to have enough remaining bytes
// to fill buf.
let n = min(buf.len(), bytes.remaining());
bytes.copy_to_slice(&mut buf[..n]);
Ok(n)
}
}
impl<S, E> BufRead for StreamReadable<S>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
BinstallError: From<E>,
{
fn fill_buf(&mut self) -> io::Result<&[u8]> {
let bytes = &mut self.bytes;
if !bytes.has_remaining() {
match self.handle.block_on(async { self.stream.next().await }) {
Some(Ok(new_bytes)) => *bytes = new_bytes,
Some(Err(e)) => {
let e: BinstallError = e.into();
return Err(io::Error::new(io::ErrorKind::Other, e));
}
None => (),
}
}
Ok(&*bytes)
}
fn consume(&mut self, amt: usize) {
self.bytes.advance(amt);
}
}

View file

@ -0,0 +1,61 @@
use std::{
future::Future,
ops::{Deref, DerefMut},
pin::Pin,
task::{Context, Poll},
};
use tokio::task::JoinHandle;
use crate::errors::BinstallError;
#[derive(Debug)]
pub struct AutoAbortJoinHandle<T>(JoinHandle<T>);
impl<T> AutoAbortJoinHandle<T> {
pub fn new(handle: JoinHandle<T>) -> Self {
Self(handle)
}
}
impl<T> AutoAbortJoinHandle<T>
where
T: Send + 'static,
{
pub fn spawn<F>(future: F) -> Self
where
F: Future<Output = T> + Send + 'static,
{
Self(tokio::spawn(future))
}
}
impl<T> Drop for AutoAbortJoinHandle<T> {
fn drop(&mut self) {
self.0.abort();
}
}
impl<T> Deref for AutoAbortJoinHandle<T> {
type Target = JoinHandle<T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for AutoAbortJoinHandle<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T> Future for AutoAbortJoinHandle<T> {
type Output = Result<T, BinstallError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut Pin::into_inner(self).0)
.poll(cx)
.map(|res| res.map_err(BinstallError::TaskJoinError))
}
}

9
crates/lib/src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
pub mod bins;
pub mod drivers;
pub mod errors;
pub mod fetchers;
pub mod fs;
pub mod helpers;
pub mod manifests;
pub mod ops;
pub mod targets;

View file

@ -0,0 +1,14 @@
//! Manifest formats and utilities.
//!
//! There are three types of manifests Binstall may deal with:
//! - manifests that define how to fetch and install a package
//! ([Cargo.toml's `[metadata.binstall]`][cargo_toml_binstall]);
//! - manifests that record which packages _are_ installed
//! ([Cargo's `.crates.toml`][cargo_crates_v1] and
//! [Binstall's `.crates-v1.json`][binstall_crates_v1]);
//! - manifests that specify which packages _to_ install (currently none).
pub mod binstall_crates_v1;
pub mod cargo_crates_v1;
pub mod cargo_toml_binstall;
pub mod crate_info;

View file

@ -0,0 +1,260 @@
//! Binstall's `crates-v1.json` manifest.
//!
//! This manifest is used by Binstall to record which crates were installed, and may be used by
//! other (third party) tooling to act upon these crates (e.g. upgrade them, list them, etc).
//!
//! The format is a series of JSON object concatenated together. It is _not_ NLJSON, though writing
//! NLJSON to the file will be understood fine.
use std::{
collections::{btree_set, BTreeSet},
fs,
io::{self, Seek, Write},
iter::{IntoIterator, Iterator},
path::{Path, PathBuf},
};
use flock::FileLock;
use miette::Diagnostic;
use serde::Serialize;
use thiserror::Error;
use crate::{fs::create_if_not_exist, helpers::statics::cargo_home};
use super::crate_info::CrateInfo;
#[derive(Debug, Diagnostic, Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
SerdeJsonParse(#[from] serde_json::Error),
}
pub fn append_to_path<Iter>(path: impl AsRef<Path>, iter: Iter) -> Result<(), Error>
where
Iter: IntoIterator<Item = CrateInfo>,
{
let mut file = FileLock::new_exclusive(create_if_not_exist(path.as_ref())?)?;
// Move the cursor to EOF
file.seek(io::SeekFrom::End(0))?;
write_to(&mut file, &mut iter.into_iter())
}
pub fn append<Iter>(iter: Iter) -> Result<(), Error>
where
Iter: IntoIterator<Item = CrateInfo>,
{
append_to_path(default_path()?, iter)
}
pub fn write_to(
file: &mut FileLock,
iter: &mut dyn Iterator<Item = CrateInfo>,
) -> Result<(), Error> {
let writer = io::BufWriter::with_capacity(512, file);
let mut ser = serde_json::Serializer::new(writer);
for item in iter {
item.serialize(&mut ser)?;
}
ser.into_inner().flush()?;
Ok(())
}
pub fn default_path() -> Result<PathBuf, Error> {
let dir = cargo_home()?.join("binstall");
fs::create_dir_all(&dir)?;
Ok(dir.join("crates-v1.json"))
}
#[derive(Debug)]
pub struct Records {
file: FileLock,
/// Use BTreeSet to dedup the metadata
data: BTreeSet<CrateInfo>,
}
impl Records {
fn load_impl(&mut self) -> Result<(), Error> {
let reader = io::BufReader::with_capacity(1024, &mut self.file);
let stream_deser = serde_json::Deserializer::from_reader(reader).into_iter();
for res in stream_deser {
let item = res?;
self.data.replace(item);
}
Ok(())
}
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
let mut this = Self {
file: FileLock::new_exclusive(create_if_not_exist(path.as_ref())?)?,
data: BTreeSet::default(),
};
this.load_impl()?;
Ok(this)
}
pub fn load() -> Result<Self, Error> {
Self::load_from_path(default_path()?)
}
/// **Warning: This will overwrite all existing records!**
pub fn overwrite(mut self) -> Result<(), Error> {
self.file.rewind()?;
write_to(&mut self.file, &mut self.data.into_iter())?;
let len = self.file.stream_position()?;
self.file.set_len(len)?;
Ok(())
}
pub fn get(&self, value: impl AsRef<str>) -> Option<&CrateInfo> {
self.data.get(value.as_ref())
}
pub fn contains(&self, value: impl AsRef<str>) -> bool {
self.data.contains(value.as_ref())
}
/// Adds a value to the set.
/// If the set did not have an equal element present, true is returned.
/// If the set did have an equal element present, false is returned,
/// and the entry is not updated.
pub fn insert(&mut self, value: CrateInfo) -> bool {
self.data.insert(value)
}
pub fn replace(&mut self, value: CrateInfo) -> Option<CrateInfo> {
self.data.replace(value)
}
pub fn remove(&mut self, value: impl AsRef<str>) -> bool {
self.data.remove(value.as_ref())
}
pub fn take(&mut self, value: impl AsRef<str>) -> Option<CrateInfo> {
self.data.take(value.as_ref())
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
impl<'a> IntoIterator for &'a Records {
type Item = &'a CrateInfo;
type IntoIter = btree_set::Iter<'a, CrateInfo>;
fn into_iter(self) -> Self::IntoIter {
self.data.iter()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{manifests::crate_info::CrateSource, targets::TARGET};
use compact_str::CompactString;
use semver::Version;
use tempfile::NamedTempFile;
macro_rules! assert_records_eq {
($records:expr, $metadata_set:expr) => {
assert_eq!($records.len(), $metadata_set.len());
for (record, metadata) in $records.into_iter().zip($metadata_set.iter()) {
assert_eq!(record, metadata);
}
};
}
#[test]
fn rw_test() {
let target = CompactString::from(TARGET);
let named_tempfile = NamedTempFile::new().unwrap();
let path = named_tempfile.path();
let metadata_vec = [
CrateInfo {
name: "a".into(),
version_req: "*".into(),
current_version: Version::new(0, 1, 0),
source: CrateSource::cratesio_registry(),
target: target.clone(),
bins: vec!["1".into(), "2".into()],
other: Default::default(),
},
CrateInfo {
name: "b".into(),
version_req: "0.1.0".into(),
current_version: Version::new(0, 1, 0),
source: CrateSource::cratesio_registry(),
target: target.clone(),
bins: vec!["1".into(), "2".into()],
other: Default::default(),
},
CrateInfo {
name: "a".into(),
version_req: "*".into(),
current_version: Version::new(0, 2, 0),
source: CrateSource::cratesio_registry(),
target: target.clone(),
bins: vec!["1".into()],
other: Default::default(),
},
];
append_to_path(&path, metadata_vec.clone()).unwrap();
let mut iter = metadata_vec.into_iter();
iter.next().unwrap();
let mut metadata_set: BTreeSet<_> = iter.collect();
let mut records = Records::load_from_path(&path).unwrap();
assert_records_eq!(&records, &metadata_set);
records.remove("b");
assert_eq!(records.len(), metadata_set.len() - 1);
records.overwrite().unwrap();
metadata_set.remove("b");
let records = Records::load_from_path(&path).unwrap();
assert_records_eq!(&records, &metadata_set);
// Drop the exclusive file lock
drop(records);
let new_metadata = CrateInfo {
name: "b".into(),
version_req: "0.1.0".into(),
current_version: Version::new(0, 1, 1),
source: CrateSource::cratesio_registry(),
target,
bins: vec!["1".into(), "2".into()],
other: Default::default(),
};
append_to_path(&path, [new_metadata.clone()]).unwrap();
metadata_set.insert(new_metadata);
let records = Records::load_from_path(&path).unwrap();
assert_records_eq!(&records, &metadata_set);
}
}

View file

@ -0,0 +1,157 @@
//! Cargo's `.crates.toml` manifest.
//!
//! This manifest is used by Cargo to record which crates were installed by `cargo-install` and by
//! other Cargo (first and third party) tooling to act upon these crates (e.g. upgrade them, list
//! them, etc).
//!
//! Binstall writes to this manifest when installing a crate, for interoperability with the Cargo
//! ecosystem.
use std::{
collections::BTreeMap,
fs::File,
io::{self, Seek},
iter::IntoIterator,
path::{Path, PathBuf},
};
use compact_str::CompactString;
use flock::FileLock;
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{fs::create_if_not_exist, helpers::statics::cargo_home};
use super::crate_info::CrateInfo;
mod crate_version_source;
use crate_version_source::*;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct CratesToml {
v1: BTreeMap<String, Vec<CompactString>>,
}
impl CratesToml {
pub fn default_path() -> Result<PathBuf, CratesTomlParseError> {
Ok(cargo_home()?.join(".crates.toml"))
}
pub fn load() -> Result<Self, CratesTomlParseError> {
Self::load_from_path(Self::default_path()?)
}
pub fn load_from_reader<R: io::Read>(mut reader: R) -> Result<Self, CratesTomlParseError> {
let mut vec = Vec::new();
reader.read_to_end(&mut vec)?;
Ok(toml_edit::easy::from_slice(&vec)?)
}
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, CratesTomlParseError> {
let file = File::open(path)?;
Self::load_from_reader(file)
}
pub fn insert(&mut self, cvs: &CrateVersionSource, bins: Vec<CompactString>) {
self.v1.insert(cvs.to_string(), bins);
}
pub fn write(&self) -> Result<(), CratesTomlParseError> {
self.write_to_path(Self::default_path()?)
}
pub fn write_to_writer<W: io::Write>(&self, mut writer: W) -> Result<(), CratesTomlParseError> {
let data = toml_edit::easy::to_vec(&self)?;
writer.write_all(&data)?;
Ok(())
}
pub fn write_to_file(&self, file: &mut File) -> Result<(), CratesTomlParseError> {
self.write_to_writer(&mut *file)?;
let pos = file.stream_position()?;
file.set_len(pos)?;
Ok(())
}
pub fn write_to_path(&self, path: impl AsRef<Path>) -> Result<(), CratesTomlParseError> {
let mut file = File::create(path)?;
self.write_to_file(&mut file)
}
pub fn append_to_path<'a, Iter>(
path: impl AsRef<Path>,
iter: Iter,
) -> Result<(), CratesTomlParseError>
where
Iter: IntoIterator<Item = &'a CrateInfo>,
{
let mut file = FileLock::new_exclusive(create_if_not_exist(path.as_ref())?)?;
let mut c1 = if file.metadata()?.len() != 0 {
Self::load_from_reader(&mut *file)?
} else {
Self::default()
};
for metadata in iter {
c1.insert(&CrateVersionSource::from(metadata), metadata.bins.clone());
}
file.rewind()?;
c1.write_to_file(&mut *file)?;
Ok(())
}
pub fn append<'a, Iter>(iter: Iter) -> Result<(), CratesTomlParseError>
where
Iter: IntoIterator<Item = &'a CrateInfo>,
{
Self::append_to_path(Self::default_path()?, iter)
}
}
#[derive(Debug, Diagnostic, Error)]
pub enum CratesTomlParseError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
TomlParse(#[from] toml_edit::easy::de::Error),
#[error(transparent)]
TomlWrite(#[from] toml_edit::easy::ser::Error),
#[error(transparent)]
CvsParse(#[from] CvsParseError),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{manifests::crate_info::CrateSource, targets::TARGET};
use semver::Version;
use tempfile::TempDir;
#[test]
fn test_empty() {
let tempdir = TempDir::new().unwrap();
let path = tempdir.path().join("crates-v1.toml");
CratesToml::append_to_path(
&path,
&[CrateInfo {
name: "cargo-binstall".into(),
version_req: "*".into(),
current_version: Version::new(0, 11, 1),
source: CrateSource::cratesio_registry(),
target: TARGET.into(),
bins: vec!["cargo-binstall".into()],
other: Default::default(),
}],
)
.unwrap();
}
}

View file

@ -0,0 +1,148 @@
use std::{borrow::Cow, fmt, str::FromStr};
use compact_str::CompactString;
use miette::Diagnostic;
use semver::Version;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use url::Url;
use crate::{
helpers::statics::cratesio_url,
manifests::crate_info::{CrateInfo, CrateSource, SourceType},
};
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub struct CrateVersionSource {
pub name: CompactString,
pub version: Version,
pub source: Source,
}
impl From<&CrateInfo> for CrateVersionSource {
fn from(metadata: &CrateInfo) -> Self {
super::CrateVersionSource {
name: metadata.name.clone(),
version: metadata.current_version.clone(),
source: Source::from(&metadata.source),
}
}
}
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub enum Source {
Git(Url),
Path(Url),
Registry(Url),
}
impl Source {
pub fn cratesio_registry() -> Source {
Self::Registry(cratesio_url().clone())
}
}
impl From<&CrateSource> for Source {
fn from(source: &CrateSource) -> Self {
use SourceType::*;
let url = source.url.clone();
match source.source_type {
Git => Self::Git(url),
Path => Self::Path(url),
Registry => Self::Registry(url),
}
}
}
impl FromStr for CrateVersionSource {
type Err = CvsParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.splitn(3, ' ').collect::<Vec<_>>()[..] {
[name, version, source] => {
let version = version.parse()?;
let source = match source
.trim_matches(&['(', ')'][..])
.splitn(2, '+')
.collect::<Vec<_>>()[..]
{
["git", url] => Source::Git(Url::parse(url)?),
["path", url] => Source::Path(Url::parse(url)?),
["registry", url] => Source::Registry(Url::parse(url)?),
[kind, arg] => {
return Err(CvsParseError::UnknownSourceType {
kind: kind.to_string(),
arg: arg.to_string(),
})
}
_ => return Err(CvsParseError::BadSource),
};
Ok(Self {
name: name.into(),
version,
source,
})
}
_ => Err(CvsParseError::BadFormat),
}
}
}
#[derive(Debug, Diagnostic, Error)]
pub enum CvsParseError {
#[error(transparent)]
UrlParse(#[from] url::ParseError),
#[error(transparent)]
VersionParse(#[from] semver::Error),
#[error("unknown source type {kind}+{arg}")]
UnknownSourceType { kind: String, arg: String },
#[error("bad source format")]
BadSource,
#[error("bad CVS format")]
BadFormat,
}
impl fmt::Display for CrateVersionSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
name,
version,
source,
} = &self;
write!(f, "{name} {version} ({source})")
}
}
impl fmt::Display for Source {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Source::Git(url) => write!(f, "git+{url}"),
Source::Path(url) => write!(f, "path+{url}"),
Source::Registry(url) => write!(f, "registry+{url}"),
}
}
}
impl Serialize for CrateVersionSource {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for CrateVersionSource {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = Cow::<'_, str>::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}

View file

@ -0,0 +1,102 @@
//! The format of the `[package.metadata.binstall]` manifest.
//!
//! This manifest defines how a particular binary crate may be installed by Binstall.
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[doc(inline)]
pub use package_formats::*;
mod package_formats;
/// Default package path template (may be overridden in package Cargo.toml)
pub const DEFAULT_PKG_URL: &str =
"{ repo }/releases/download/v{ version }/{ name }-{ target }-v{ version }.{ archive-format }";
/// Default binary name template (may be overridden in package Cargo.toml)
pub const DEFAULT_BIN_DIR: &str = "{ name }-{ target }-v{ version }/{ bin }{ binary-ext }";
/// `binstall` metadata container
///
/// Required to nest metadata under `package.metadata.binstall`
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Meta {
pub binstall: Option<PkgMeta>,
}
/// Metadata for binary installation use.
///
/// Exposed via `[package.metadata]` in `Cargo.toml`
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default)]
pub struct PkgMeta {
/// URL template for package downloads
pub pkg_url: String,
/// Format for package downloads
pub pkg_fmt: PkgFmt,
/// Path template for binary files in packages
pub bin_dir: String,
/// Public key for package verification (base64 encoded)
pub pub_key: Option<String>,
/// Target specific overrides
pub overrides: HashMap<String, PkgOverride>,
}
impl Default for PkgMeta {
fn default() -> Self {
Self {
pkg_url: DEFAULT_PKG_URL.to_string(),
pkg_fmt: PkgFmt::default(),
bin_dir: DEFAULT_BIN_DIR.to_string(),
pub_key: None,
overrides: HashMap::new(),
}
}
}
impl PkgMeta {
/// Merge configuration overrides into object
pub fn merge(&mut self, pkg_override: &PkgOverride) {
if let Some(o) = &pkg_override.pkg_url {
self.pkg_url = o.clone();
}
if let Some(o) = &pkg_override.pkg_fmt {
self.pkg_fmt = *o;
}
if let Some(o) = &pkg_override.bin_dir {
self.bin_dir = o.clone();
}
}
}
/// Target specific overrides for binary installation
///
/// Exposed via `[package.metadata.TARGET]` in `Cargo.toml`
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default)]
pub struct PkgOverride {
/// URL template override for package downloads
pub pkg_url: Option<String>,
/// Format override for package downloads
pub pkg_fmt: Option<PkgFmt>,
/// Path template override for binary files in packages
pub bin_dir: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct BinMeta {
/// Binary name
pub name: String,
/// Binary template path (within package)
pub path: String,
}

View file

@ -0,0 +1,89 @@
use serde::{Deserialize, Serialize};
use strum_macros::{Display, EnumString};
/// Binary format enumeration
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize, EnumString)]
#[serde(rename_all = "snake_case")]
pub enum PkgFmt {
/// Download format is TAR (uncompressed)
Tar,
/// Download format is TAR + Bzip2
Tbz2,
/// Download format is TGZ (TAR + GZip)
Tgz,
/// Download format is TAR + XZ
Txz,
/// Download format is TAR + Zstd
Tzstd,
/// Download format is Zip
Zip,
/// Download format is raw / binary
Bin,
}
impl Default for PkgFmt {
fn default() -> Self {
Self::Tgz
}
}
impl PkgFmt {
/// If self is one of the tar based formats, return Some.
pub fn decompose(self) -> PkgFmtDecomposed {
match self {
PkgFmt::Tar => PkgFmtDecomposed::Tar(TarBasedFmt::Tar),
PkgFmt::Tbz2 => PkgFmtDecomposed::Tar(TarBasedFmt::Tbz2),
PkgFmt::Tgz => PkgFmtDecomposed::Tar(TarBasedFmt::Tgz),
PkgFmt::Txz => PkgFmtDecomposed::Tar(TarBasedFmt::Txz),
PkgFmt::Tzstd => PkgFmtDecomposed::Tar(TarBasedFmt::Tzstd),
PkgFmt::Bin => PkgFmtDecomposed::Bin,
PkgFmt::Zip => PkgFmtDecomposed::Zip,
}
}
/// List of possible file extensions for the format.
pub fn extensions(self) -> &'static [&'static str] {
match self {
PkgFmt::Tar => &["tar"],
PkgFmt::Tbz2 => &["tbz2", "tar.bz2"],
PkgFmt::Tgz => &["tgz", "tar.gz"],
PkgFmt::Txz => &["txz", "tar.xz"],
PkgFmt::Tzstd => &["tzstd", "tzst", "tar.zst"],
PkgFmt::Bin => &["bin", "exe"],
PkgFmt::Zip => &["zip"],
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum PkgFmtDecomposed {
Tar(TarBasedFmt),
Bin,
Zip,
}
#[derive(Debug, Display, Copy, Clone, PartialEq)]
pub enum TarBasedFmt {
/// Download format is TAR (uncompressed)
Tar,
/// Download format is TAR + Bzip2
Tbz2,
/// Download format is TGZ (TAR + GZip)
Tgz,
/// Download format is TAR + XZ
Txz,
/// Download format is TAR + Zstd
Tzstd,
}
impl From<TarBasedFmt> for PkgFmt {
fn from(fmt: TarBasedFmt) -> Self {
match fmt {
TarBasedFmt::Tar => PkgFmt::Tar,
TarBasedFmt::Tbz2 => PkgFmt::Tbz2,
TarBasedFmt::Tgz => PkgFmt::Tgz,
TarBasedFmt::Txz => PkgFmt::Txz,
TarBasedFmt::Tzstd => PkgFmt::Tzstd,
}
}
}

View file

@ -0,0 +1,83 @@
//! Common structure for crate information for post-install manifests.
use std::{borrow, cmp, hash};
use compact_str::CompactString;
use semver::Version;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::helpers::statics::cratesio_url;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CrateInfo {
pub name: CompactString,
pub version_req: CompactString,
pub current_version: Version,
pub source: CrateSource,
pub target: CompactString,
pub bins: Vec<CompactString>,
/// Forwards compatibility. Unknown keys from future versions
/// will be stored here and retained when the file is saved.
///
/// We use an `Vec` here since it is never accessed in Rust.
#[serde(flatten, with = "tuple_vec_map")]
pub other: Vec<(CompactString, serde_json::Value)>,
}
impl borrow::Borrow<str> for CrateInfo {
fn borrow(&self) -> &str {
&self.name
}
}
impl PartialEq for CrateInfo {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for CrateInfo {}
impl PartialOrd for CrateInfo {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
self.name.partial_cmp(&other.name)
}
}
impl Ord for CrateInfo {
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.name.cmp(&other.name)
}
}
impl hash::Hash for CrateInfo {
fn hash<H>(&self, state: &mut H)
where
H: hash::Hasher,
{
self.name.hash(state)
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum SourceType {
Git,
Path,
Registry,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CrateSource {
pub source_type: SourceType,
pub url: Url,
}
impl CrateSource {
pub fn cratesio_registry() -> CrateSource {
Self {
source_type: SourceType::Registry,
url: cratesio_url().clone(),
}
}
}

21
crates/lib/src/ops.rs Normal file
View file

@ -0,0 +1,21 @@
//! Concrete Binstall operations.
use std::path::PathBuf;
use semver::VersionReq;
use crate::{manifests::cargo_toml_binstall::PkgOverride, targets::DesiredTargets};
pub mod install;
pub mod resolve;
pub struct Options {
pub no_symlinks: bool,
pub dry_run: bool,
pub force: bool,
pub version_req: Option<VersionReq>,
pub manifest_path: Option<PathBuf>,
pub cli_overrides: PkgOverride,
pub desired_targets: DesiredTargets,
pub quiet: bool,
}

View file

@ -0,0 +1,190 @@
use std::{path::PathBuf, process, sync::Arc};
use cargo_toml::Package;
use compact_str::CompactString;
use log::{debug, error, info};
use tokio::{process::Command, task::block_in_place};
use super::{resolve::Resolution, Options};
use crate::{
bins,
errors::BinstallError,
fetchers::Fetcher,
helpers::jobserver_client::LazyJobserverClient,
manifests::{
cargo_toml_binstall::Meta,
crate_info::{CrateInfo, CrateSource},
},
};
pub async fn install(
resolution: Resolution,
opts: Arc<Options>,
jobserver_client: LazyJobserverClient,
) -> Result<Option<CrateInfo>, BinstallError> {
match resolution {
Resolution::AlreadyUpToDate => Ok(None),
Resolution::Fetch {
fetcher,
package,
name,
version_req,
bin_path,
bin_files,
} => {
let current_version =
package
.version
.parse()
.map_err(|err| BinstallError::VersionParse {
v: package.version,
err,
})?;
let target = fetcher.target().into();
install_from_package(fetcher, opts, bin_path, bin_files)
.await
.map(|option| {
option.map(|bins| CrateInfo {
name,
version_req,
current_version,
source: CrateSource::cratesio_registry(),
target,
bins,
other: Default::default(),
})
})
}
Resolution::InstallFromSource { package } => {
let desired_targets = opts.desired_targets.get().await;
let target = desired_targets
.first()
.ok_or(BinstallError::NoViableTargets)?;
if !opts.dry_run {
install_from_source(package, target, jobserver_client, opts.quiet, opts.force)
.await
.map(|_| None)
} else {
info!(
"Dry-run: running `cargo install {} --version {} --target {target}`",
package.name, package.version
);
Ok(None)
}
}
}
}
async fn install_from_package(
fetcher: Arc<dyn Fetcher>,
opts: Arc<Options>,
bin_path: PathBuf,
bin_files: Vec<bins::BinFile>,
) -> Result<Option<Vec<CompactString>>, BinstallError> {
// Download package
if opts.dry_run {
info!("Dry run, not downloading package");
} else {
fetcher.fetch_and_extract(&bin_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() {
debug!("Found public key: {pub_key}");
// Generate signature file URL
let mut sig_ctx = ctx.clone();
sig_ctx.format = "sig".to_string();
let sig_url = sig_ctx.render(&pkg_url)?;
debug!("Fetching signature file: {sig_url}");
// Download signature file
let sig_path = temp_dir.join(format!("{pkg_name}.sig"));
download(&sig_url, &sig_path).await?;
// TODO: do the signature check
unimplemented!()
} else {
warn!("No public key found, package signature could not be validated");
}
}
if opts.dry_run {
info!("Dry run, not proceeding");
return Ok(None);
}
info!("Installing binaries...");
block_in_place(|| {
for file in &bin_files {
file.install_bin()?;
}
// Generate symlinks
if !opts.no_symlinks {
for file in &bin_files {
file.install_link()?;
}
}
Ok(Some(
bin_files.into_iter().map(|bin| bin.base_name).collect(),
))
})
}
async fn install_from_source(
package: Package<Meta>,
target: &str,
lazy_jobserver_client: LazyJobserverClient,
quiet: bool,
force: bool,
) -> Result<(), BinstallError> {
let jobserver_client = lazy_jobserver_client.get().await?;
debug!(
"Running `cargo install {} --version {} --target {target}`",
package.name, package.version
);
let mut command = process::Command::new("cargo");
jobserver_client.configure(&mut command);
let mut cmd = Command::from(command);
cmd.arg("install")
.arg(package.name)
.arg("--version")
.arg(package.version)
.arg("--target")
.arg(&*target);
if quiet {
cmd.arg("--quiet");
}
if force {
cmd.arg("--force");
}
let command_string = format!("{:?}", cmd);
let mut child = cmd.spawn()?;
debug!("Spawned command pid={:?}", child.id());
let status = child.wait().await?;
if status.success() {
info!("Cargo finished successfully");
Ok(())
} else {
error!("Cargo errored! {status:?}");
Err(BinstallError::SubProcess {
command: command_string,
status,
})
}
}

View file

@ -0,0 +1,304 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use cargo_toml::{Manifest, Package, Product};
use compact_str::{CompactString, ToCompactString};
use log::{debug, info, warn};
use reqwest::Client;
use semver::{Version, VersionReq};
use tokio::task::block_in_place;
use super::Options;
use crate::{
bins,
drivers::fetch_crate_cratesio,
errors::BinstallError,
fetchers::{Data, Fetcher, GhCrateMeta, MultiFetcher, QuickInstall},
manifests::cargo_toml_binstall::{Meta, PkgMeta},
};
mod crate_name;
#[doc(inline)]
pub use crate_name::CrateName;
mod version_ext;
#[doc(inline)]
pub use version_ext::VersionReqExt;
pub enum Resolution {
Fetch {
fetcher: Arc<dyn Fetcher>,
package: Package<Meta>,
name: CompactString,
version_req: CompactString,
bin_path: PathBuf,
bin_files: Vec<bins::BinFile>,
},
InstallFromSource {
package: Package<Meta>,
},
AlreadyUpToDate,
}
impl Resolution {
fn print(&self, opts: &Options) {
match self {
Resolution::Fetch {
fetcher, bin_files, ..
} => {
let fetcher_target = fetcher.target();
// Prompt user for confirmation
debug!(
"Found a binary install source: {} ({fetcher_target})",
fetcher.source_name()
);
if fetcher.is_third_party() {
warn!(
"The package will be downloaded from third-party source {}",
fetcher.source_name()
);
} else {
info!(
"The package will be downloaded from {}",
fetcher.source_name()
);
}
info!("This will install the following binaries:");
for file in bin_files {
info!(" - {}", file.preview_bin());
}
if !opts.no_symlinks {
info!("And create (or update) the following symlinks:");
for file in bin_files {
info!(" - {}", file.preview_link());
}
}
}
Resolution::InstallFromSource { .. } => {
warn!("The package will be installed from source (with cargo)",)
}
Resolution::AlreadyUpToDate => (),
}
}
}
pub async fn resolve(
opts: Arc<Options>,
crate_name: CrateName,
curr_version: Option<Version>,
temp_dir: Arc<Path>,
install_path: Arc<Path>,
client: Client,
crates_io_api_client: crates_io_api::AsyncClient,
) -> Result<Resolution, BinstallError> {
let crate_name_name = crate_name.name.clone();
resolve_inner(
opts,
crate_name,
curr_version,
temp_dir,
install_path,
client,
crates_io_api_client,
)
.await
.map_err(|err| err.crate_context(crate_name_name))
}
async fn resolve_inner(
opts: Arc<Options>,
crate_name: CrateName,
curr_version: Option<Version>,
temp_dir: Arc<Path>,
install_path: Arc<Path>,
client: Client,
crates_io_api_client: crates_io_api::AsyncClient,
) -> Result<Resolution, BinstallError> {
info!("Resolving package: '{}'", crate_name);
let version_req: VersionReq = match (&crate_name.version_req, &opts.version_req) {
(Some(version), None) => version.clone(),
(None, Some(version)) => version.clone(),
(Some(_), Some(_)) => Err(BinstallError::SuperfluousVersionOption)?,
(None, None) => VersionReq::STAR,
};
// 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: support git-based fetches (whole repo name rather than just crate name)
let manifest = match opts.manifest_path.clone() {
Some(manifest_path) => load_manifest_path(manifest_path)?,
None => {
fetch_crate_cratesio(
&client,
&crates_io_api_client,
&crate_name.name,
&version_req,
)
.await?
}
};
let package = manifest.package.unwrap();
if let Some(curr_version) = curr_version {
let new_version =
Version::parse(&package.version).map_err(|err| BinstallError::VersionParse {
v: package.version.clone(),
err,
})?;
if new_version == curr_version {
info!(
"{} v{curr_version} is already installed, use --force to override",
crate_name.name
);
return Ok(Resolution::AlreadyUpToDate);
}
}
let (mut meta, binaries) = (
package
.metadata
.as_ref()
.and_then(|m| m.binstall.clone())
.unwrap_or_default(),
manifest.bin,
);
let mut fetchers = MultiFetcher::default();
let desired_targets = opts.desired_targets.get().await;
for target in desired_targets {
debug!("Building metadata for target: {target}");
let mut target_meta = meta.clone();
// Merge any overrides
if let Some(o) = target_meta.overrides.get(target).cloned() {
target_meta.merge(&o);
}
target_meta.merge(&opts.cli_overrides);
debug!("Found metadata: {target_meta:?}");
let fetcher_data = Data {
name: package.name.clone(),
target: target.clone(),
version: package.version.clone(),
repo: package.repository.clone(),
meta: target_meta,
};
fetchers.add(GhCrateMeta::new(&client, &fetcher_data).await);
fetchers.add(QuickInstall::new(&client, &fetcher_data).await);
}
let resolution = match fetchers.first_available().await {
Some(fetcher) => {
// Build final metadata
let fetcher_target = fetcher.target();
if let Some(o) = meta.overrides.get(&fetcher_target.to_owned()).cloned() {
meta.merge(&o);
}
meta.merge(&opts.cli_overrides);
// Generate temporary binary path
let bin_path = temp_dir.join(format!("bin-{}", crate_name.name));
debug!("Using temporary binary path: {}", bin_path.display());
let bin_files = collect_bin_files(
fetcher.as_ref(),
&package,
meta,
binaries,
bin_path.clone(),
install_path.to_path_buf(),
)?;
Resolution::Fetch {
fetcher,
package,
name: crate_name.name,
version_req: version_req.to_compact_string(),
bin_path,
bin_files,
}
}
None => Resolution::InstallFromSource { package },
};
resolution.print(&opts);
Ok(resolution)
}
fn collect_bin_files(
fetcher: &dyn Fetcher,
package: &Package<Meta>,
mut meta: PkgMeta,
binaries: Vec<Product>,
bin_path: PathBuf,
install_path: PathBuf,
) -> Result<Vec<bins::BinFile>, BinstallError> {
// Update meta
if fetcher.source_name() == "QuickInstall" {
// TODO: less of a hack?
meta.bin_dir = "{ bin }{ binary-ext }".to_string();
}
// Check binaries
if binaries.is_empty() {
return Err(BinstallError::UnspecifiedBinaries);
}
// List files to be installed
// based on those found via Cargo.toml
let bin_data = bins::Data {
name: package.name.clone(),
target: fetcher.target().to_string(),
version: package.version.clone(),
repo: package.repository.clone(),
meta,
bin_path,
install_path,
};
// Create bin_files
let bin_files = binaries
.iter()
.map(|p| bins::BinFile::from_product(&bin_data, p))
.collect::<Result<Vec<_>, BinstallError>>()?;
Ok(bin_files)
}
/// 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>, BinstallError> {
block_in_place(|| {
let manifest_path = manifest_path.as_ref();
let manifest_path = if manifest_path.is_dir() {
manifest_path.join("Cargo.toml")
} else if manifest_path.is_file() {
manifest_path.into()
} else {
return Err(BinstallError::CargoManifestPath);
};
debug!(
"Reading manifest at local path: {}",
manifest_path.display()
);
// Load and parse manifest (this checks file system for binary output names)
let manifest = Manifest::<Meta>::from_path_with_metadata(manifest_path)?;
// Return metadata
Ok(manifest)
})
}

View file

@ -0,0 +1,112 @@
use std::{fmt, str::FromStr};
use compact_str::CompactString;
use itertools::Itertools;
use semver::{Error, VersionReq};
use super::version_ext::VersionReqExt;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct CrateName {
pub name: CompactString,
pub version_req: Option<VersionReq>,
}
impl fmt::Display for CrateName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)?;
if let Some(version) = &self.version_req {
write!(f, "@{version}")?;
}
Ok(())
}
}
impl FromStr for CrateName {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(if let Some((name, version)) = s.split_once('@') {
CrateName {
name: name.into(),
version_req: Some(VersionReq::parse_from_cli(version)?),
}
} else {
CrateName {
name: s.into(),
version_req: None,
}
})
}
}
impl CrateName {
pub fn dedup(crate_names: &[Self]) -> impl Iterator<Item = Self> {
let mut crate_names = crate_names.to_vec();
crate_names.sort_by(|x, y| x.name.cmp(&y.name));
crate_names.into_iter().coalesce(|previous, current| {
if previous.name == current.name {
Ok(current)
} else {
Err((previous, current))
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_dedup {
([ $( ( $input_name:expr, $input_version:expr ) ),* ], [ $( ( $output_name:expr, $output_version:expr ) ),* ]) => {
let input_crate_names = [$( CrateName {
name: $input_name.into(),
version_req: Some($input_version.parse().unwrap())
}, )*];
let mut output_crate_names: Vec<CrateName> = vec![$( CrateName {
name: $output_name.into(), version_req: Some($output_version.parse().unwrap())
}, )*];
output_crate_names.sort_by(|x, y| x.name.cmp(&y.name));
let crate_names: Vec<_> = CrateName::dedup(&input_crate_names).collect();
assert_eq!(crate_names, output_crate_names);
};
}
#[test]
fn test_dedup() {
// Base case 0: Empty input
assert_dedup!([], []);
// Base case 1: With only one input
assert_dedup!([("a", "1")], [("a", "1")]);
// Base Case 2: Only has duplicate names
assert_dedup!([("a", "1"), ("a", "2")], [("a", "2")]);
// Complex Case 0: Having two crates
assert_dedup!(
[("a", "10"), ("b", "3"), ("a", "0"), ("b", "0"), ("a", "1")],
[("a", "1"), ("b", "0")]
);
// Complex Case 1: Having three crates
assert_dedup!(
[
("d", "1.1"),
("a", "10"),
("b", "3"),
("d", "230"),
("a", "0"),
("b", "0"),
("a", "1"),
("d", "23")
],
[("a", "1"), ("b", "0"), ("d", "23")]
);
}
}

View file

@ -0,0 +1,99 @@
use compact_str::format_compact;
use semver::{Prerelease, Version, VersionReq};
/// Extension trait for [`VersionReq`].
pub trait VersionReqExt {
/// Return `true` if `self.matches(version)` returns `true`
/// and the `version` is the latest one acceptable by `self`.
fn is_latest_compatible(&self, version: &Version) -> bool;
/// Parse from CLI option.
///
/// Notably, a bare version is treated as if preceded by `=`, not by `^` as in Cargo.toml
/// dependencies.
fn parse_from_cli(str: &str) -> Result<Self, semver::Error>
where
Self: Sized;
}
impl VersionReqExt for VersionReq {
fn is_latest_compatible(&self, version: &Version) -> bool {
if !self.matches(version) {
return false;
}
// Test if bumping patch will be accepted
let bumped_version = Version::new(version.major, version.minor, version.patch + 1);
if self.matches(&bumped_version) {
return false;
}
// Test if bumping prerelease will be accepted if version has one.
let pre = &version.pre;
if !pre.is_empty() {
// Bump pre by appending random number to the end.
let bumped_pre = format_compact!("{}.1", pre.as_str());
let bumped_version = Version {
major: version.major,
minor: version.minor,
patch: version.patch,
pre: Prerelease::new(&bumped_pre).unwrap(),
build: Default::default(),
};
if self.matches(&bumped_version) {
return false;
}
}
true
}
fn parse_from_cli(version: &str) -> Result<Self, semver::Error> {
if version
.chars()
.next()
.map(|ch| ch.is_ascii_digit())
.unwrap_or(false)
{
format_compact!("={version}").parse()
} else {
version.parse()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test() {
// Test star
assert!(!VersionReq::STAR.is_latest_compatible(&Version::parse("0.0.1").unwrap()));
assert!(!VersionReq::STAR.is_latest_compatible(&Version::parse("0.1.1").unwrap()));
assert!(!VersionReq::STAR.is_latest_compatible(&Version::parse("0.1.1-alpha").unwrap()));
// Test ^x.y.z
assert!(!VersionReq::parse("^0.1")
.unwrap()
.is_latest_compatible(&Version::parse("0.1.99").unwrap()));
// Test =x.y.z
assert!(VersionReq::parse("=0.1.0")
.unwrap()
.is_latest_compatible(&Version::parse("0.1.0").unwrap()));
// Test =x.y.z-alpha
assert!(VersionReq::parse("=0.1.0-alpha")
.unwrap()
.is_latest_compatible(&Version::parse("0.1.0-alpha").unwrap()));
// Test >=x.y.z-alpha
assert!(!VersionReq::parse(">=0.1.0-alpha")
.unwrap()
.is_latest_compatible(&Version::parse("0.1.0-alpha").unwrap()));
}
}

254
crates/lib/src/targets.rs Normal file
View file

@ -0,0 +1,254 @@
use std::{
io::{BufRead, Cursor},
process::Output,
sync::Arc,
};
use tokio::{process::Command, sync::OnceCell};
/// Compiled target triple, used as default for binary fetching
pub const TARGET: &str = env!("TARGET");
#[derive(Debug)]
enum DesiredTargetsInner {
AutoDetect(Arc<OnceCell<Vec<String>>>),
Initialized(Vec<String>),
}
#[derive(Debug)]
pub struct DesiredTargets(DesiredTargetsInner);
impl DesiredTargets {
fn initialized(targets: Vec<String>) -> Self {
Self(DesiredTargetsInner::Initialized(targets))
}
fn auto_detect() -> Self {
let arc = Arc::new(OnceCell::new());
let once_cell = arc.clone();
tokio::spawn(async move {
once_cell.get_or_init(detect_targets).await;
});
Self(DesiredTargetsInner::AutoDetect(arc))
}
pub async fn get(&self) -> &[String] {
use DesiredTargetsInner::*;
match &self.0 {
Initialized(targets) => targets,
// This will mostly just wait for the spawned task,
// on rare occausion though, it will poll the future
// returned by `detect_targets`.
AutoDetect(once_cell) => once_cell.get_or_init(detect_targets).await,
}
}
}
/// If opts_targets is `Some`, then it will be used.
/// Otherwise, call `detect_targets` using `tokio::spawn` to detect targets.
///
/// Since `detect_targets` internally spawns a process and wait for it,
/// it's pretty costy.
///
/// Calling it through `tokio::spawn` would enable other tasks, such as
/// fetching the crate tarballs, to be executed concurrently.
pub fn get_desired_targets(opts_targets: &Option<String>) -> DesiredTargets {
if let Some(targets) = opts_targets.as_ref() {
DesiredTargets::initialized(targets.split(',').map(|t| t.to_string()).collect())
} else {
DesiredTargets::auto_detect()
}
}
/// Detect the targets supported at runtime,
/// which might be different from `TARGET` which is detected
/// at compile-time.
///
/// Return targets supported in the order of preference.
/// If target_os is linux and it support gnu, then it is preferred
/// to musl.
///
/// If target_os is mac and it is aarch64, then aarch64 is preferred
/// to x86_64.
///
/// Check [this issue](https://github.com/ryankurte/cargo-binstall/issues/155)
/// for more information.
pub async fn detect_targets() -> Vec<String> {
if let Some(target) = get_target_from_rustc().await {
let mut v = vec![target];
#[cfg(target_os = "linux")]
if v[0].contains("gnu") {
v.push(v[0].replace("gnu", "musl"));
}
#[cfg(target_os = "macos")]
if &*v[0] == macos::AARCH64 {
v.push(macos::X86.into());
}
#[cfg(target_os = "windows")]
v.extend(windows::detect_alternative_targets(&v[0]));
v
} else {
#[cfg(target_os = "linux")]
{
linux::detect_targets_linux().await
}
#[cfg(target_os = "macos")]
{
macos::detect_targets_macos()
}
#[cfg(target_os = "windows")]
{
windows::detect_targets_windows()
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
vec![TARGET.into()]
}
}
}
/// Figure out what the host target is using `rustc`.
/// If `rustc` is absent, then it would return `None`.
async fn get_target_from_rustc() -> Option<String> {
let Output { status, stdout, .. } = Command::new("rustc").arg("-vV").output().await.ok()?;
if !status.success() {
return None;
}
Cursor::new(stdout)
.lines()
.filter_map(|line| line.ok())
.find_map(|line| line.strip_prefix("host: ").map(|host| host.to_owned()))
}
#[cfg(target_os = "linux")]
mod linux {
use super::{Command, Output, TARGET};
pub(super) async fn detect_targets_linux() -> Vec<String> {
let (abi, libc) = parse_abi_and_libc();
match libc {
// Glibc can only be dynamically linked.
// If we can run this binary, then it means that the target
// supports both glibc and musl.
Libc::Glibc => return create_targets_str(&["gnu", "musl"], abi),
_ => (),
}
if let Ok(Output {
status: _,
stdout,
stderr,
}) = Command::new("ldd").arg("--version").output().await
{
let libc_version =
if let Some(libc_version) = parse_libc_version_from_ldd_output(&stdout) {
libc_version
} else if let Some(libc_version) = parse_libc_version_from_ldd_output(&stderr) {
libc_version
} else {
return vec![create_target_str("musl", abi)];
};
if libc_version == "gnu" {
return create_targets_str(&["gnu", "musl"], abi);
}
}
// Fallback to using musl
vec![create_target_str("musl", abi)]
}
fn parse_libc_version_from_ldd_output(output: &[u8]) -> Option<&'static str> {
let s = String::from_utf8_lossy(output);
if s.contains("musl libc") {
Some("musl")
} else if s.contains("GLIBC") {
Some("gnu")
} else {
None
}
}
enum Libc {
Glibc,
Musl,
}
fn parse_abi_and_libc() -> (&'static str, Libc) {
let last = TARGET.rsplit_once('-').unwrap().1;
if let Some(libc_version) = last.strip_prefix("musl") {
(libc_version, Libc::Musl)
} else if let Some(libc_version) = last.strip_prefix("gnu") {
(libc_version, Libc::Glibc)
} else {
panic!("Unrecognized libc")
}
}
fn create_target_str(libc_version: &str, abi: &str) -> String {
let prefix = TARGET
.rsplit_once('-')
.expect("unwrap: TARGET always has a -")
.0;
format!("{prefix}-{libc_version}{abi}")
}
fn create_targets_str(libc_versions: &[&str], abi: &str) -> Vec<String> {
libc_versions
.iter()
.map(|libc_version| create_target_str(libc_version, abi))
.collect()
}
}
#[cfg(target_os = "macos")]
mod macos {
use super::TARGET;
use guess_host_triple::guess_host_triple;
pub(super) const AARCH64: &str = "aarch64-apple-darwin";
pub(super) const X86: &str = "x86_64-apple-darwin";
pub(super) fn detect_targets_macos() -> Vec<String> {
let mut targets = vec![guess_host_triple().unwrap_or(TARGET).to_string()];
if targets[0] == AARCH64 {
targets.push(X86.into());
}
targets
}
}
#[cfg(target_os = "windows")]
mod windows {
use super::TARGET;
use guess_host_triple::guess_host_triple;
pub(super) fn detect_alternative_targets(target: &str) -> Option<String> {
let (prefix, abi) = target.rsplit_once('-').expect("Invalid target triple");
// detect abi in ["gnu", "gnullvm", ...]
(abi != "msvc").then(|| format!("{prefix}-msvc"))
}
pub(super) fn detect_targets_windows() -> Vec<String> {
let mut targets = vec![guess_host_triple().unwrap_or(TARGET).to_string()];
targets.extend(detect_alternative_targets(&targets[0]));
targets
}
}