use std::{
env, fs,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use atomic_file_install::atomic_install;
use binstalk::{
errors::{BinstallError, CrateContextError},
fetchers::{Fetcher, GhCrateMeta, QuickInstall, SignaturePolicy},
get_desired_targets,
helpers::{
jobserver_client::LazyJobserverClient,
lazy_gh_api_client::LazyGhApiClient,
remote::{Certificate, Client},
tasks::AutoAbortJoinHandle,
},
ops::{
self,
resolve::{CrateName, Resolution, ResolutionFetch, VersionReqExt},
CargoTomlFetchOverride, Options, Resolver,
},
TARGET,
};
use binstalk_manifests::{
cargo_toml_binstall::{PkgOverride, Strategy},
crate_info::{CrateInfo, CrateSource},
crates_manifests::Manifests,
};
use compact_str::CompactString;
use file_format::FileFormat;
use log::LevelFilter;
use miette::{Report, Result};
use semver::{Version, VersionReq};
use tokio::task::block_in_place;
use tracing::{debug, info, warn};
use crate::registry_auth::{get_registry_env_var, resolve_registry_auth};
use crate::{args::Args, gh_token, git_credentials, initialise::Init, ui::confirm};
pub fn install_crates(
args: Args,
cli_overrides: PkgOverride,
jobserver_client: LazyJobserverClient,
) -> Result<Option<AutoAbortJoinHandle<Result<()>>>> {
let Init {
mut cargo_config,
cargo_home,
settings,
cargo_root,
install_path,
manifests,
temp_dir,
} = crate::initialise::initialise(&args)?;
let mut cargo_install_fallback = false;
let resolvers: Vec<_> = settings
.strategies
.into_iter()
.filter_map(|strategy| match strategy.0 {
Strategy::CrateMetaData => Some(GhCrateMeta::new as Resolver),
Strategy::QuickInstall => Some(QuickInstall::new as Resolver),
Strategy::Compile => {
cargo_install_fallback = true;
None
}
})
.collect();
let mut crate_names = filter_out_installed_crates(
args.crate_names,
args.force,
manifests.as_ref(),
args.version_req,
)
.peekable();
if crate_names.peek().is_none() {
debug!("Nothing to do");
return Ok(None);
}
let desired_targets = get_desired_targets(settings.targets);
let rate_limit = args.rate_limit;
let mut http = cargo_config.http.clone();
let client = Client::new(
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
args.min_tls_version.map(|v| v.into()),
rate_limit.duration,
rate_limit.request_count,
read_root_certs(
args.root_certificates,
http.as_mut().and_then(|http| http.cainfo.take()),
),
)
.map_err(BinstallError::from)?;
let gh_api_client = args
.github_token
.map(|token| token.0)
.or_else(|| {
if args.no_discover_github_token {
None
} else {
git_credentials::try_from_home()
}
})
.map(|token| LazyGhApiClient::new(client.clone(), Some(token)))
.unwrap_or_else(|| {
if args.no_discover_github_token {
LazyGhApiClient::new(client.clone(), None)
} else {
LazyGhApiClient::with_get_gh_token_future(client.clone(), async {
match gh_token::get().await {
Ok(token) => Some(token),
Err(err) => {
debug!(?err, "Failed to retrieve token from `gh auth token`");
debug!("Failed to read git credential file");
None
}
}
})
}
});
let (resolved_registry, cargo_install_registry, cargo_install_index) = {
let cargo_home = cargo_home.as_deref().unwrap_or(&cargo_root);
if let Some(index) = args.index.clone() {
let resolved_registry = binstalk::registry::ResolvedRegistry::new(index, None);
let cargo_install_index = resolved_registry.cargo_install_index_arg().into();
(resolved_registry, None, Some(cargo_install_index))
} else {
let registry_name = args.registry.clone().or_else(|| {
cargo_config
.registry
.take()
.and_then(|registry| registry.default)
});
let registry = if let Some(registry_name) = registry_name.as_deref() {
let index = get_registry_env_var(registry_name, "INDEX");
let index = index
.as_deref()
.or_else(|| cargo_config.get_registry_index(registry_name));
if let Some(index) = index {
index.parse().map_err(BinstallError::from)?
} else if registry_name.eq_ignore_ascii_case("crates-io") {
Default::default()
} else {
return Err(BinstallError::UnknownRegistryName(registry_name.into()).into());
}
} else {
Default::default()
};
let registry_auth = resolve_registry_auth(
&cargo_config,
cargo_home,
registry_name.as_deref(),
®istry,
);
(
binstalk::registry::ResolvedRegistry::new(registry, registry_auth),
registry_name.filter(|name| !name.eq_ignore_ascii_case("crates-io")),
None,
)
}
};
let binstall_opts = Arc::new(Options {
no_symlinks: args.no_symlinks,
dry_run: args.dry_run,
force: args.force,
quiet: args.log_level == Some(LevelFilter::Off),
locked: args.locked,
no_track: !settings.track_installs,
#[cfg(feature = "git")]
cargo_toml_fetch_override: match (args.manifest_path, args.git) {
(Some(manifest_path), None) => Some(CargoTomlFetchOverride::Path(manifest_path)),
(None, Some(git_url)) => Some(CargoTomlFetchOverride::Git(git_url)),
(None, None) => None,
_ => unreachable!("manifest_path and git cannot be specified at the same time"),
},
#[cfg(not(feature = "git"))]
cargo_toml_fetch_override: args.manifest_path.map(CargoTomlFetchOverride::Path),
cli_overrides,
desired_targets,
resolvers,
cargo_install_fallback,
bins: args.bin.map(|mut bins| {
bins.sort_unstable();
bins
}),
temp_dir: temp_dir.path().to_owned(),
install_path,
has_overriden_install_path: args.install_path.is_some(),
cargo_root: Some(cargo_root),
cargo_install_registry,
cargo_install_index,
client,
gh_api_client,
jobserver_client,
registry: resolved_registry,
signature_policy: if args.only_signed {
SignaturePolicy::Require
} else if args.skip_signatures {
SignaturePolicy::Ignore
} else {
SignaturePolicy::IfPresent
},
disable_telemetry: !settings.telemetry.enabled,
maximum_resolution_timeout: Duration::from_secs(
args.maximum_resolution_timeout.get().into(),
),
});
let dry_run = args.dry_run;
let no_confirm = !settings.confirm;
let no_cleanup = args.no_cleanup;
let continue_on_failure = settings.continue_on_failure;
let tasks = crate_names
.map(|res| {
res.map(|(crate_name, current_version)| {
AutoAbortJoinHandle::spawn(ops::resolve::resolve(
binstall_opts.clone(),
crate_name,
current_version,
))
})
})
.collect::<Result<Vec<_>, BinstallError>>()?;
Ok(Some(if continue_on_failure {
AutoAbortJoinHandle::spawn(async move {
let mut resolution_fetches = Vec::new();
let mut resolution_sources = Vec::new();
let mut errors = Vec::new();
for task in tasks {
match task.flattened_join().await {
Ok(Resolution::AlreadyUpToDate) => {}
Ok(Resolution::Fetch(fetch)) => {
fetch.print(&binstall_opts);
resolution_fetches.push(fetch)
}
Ok(Resolution::InstallFromSource(source)) => {
source.print();
resolution_sources.push(source)
}
Err(BinstallError::CrateContext(err)) => errors.push(err),
Err(e) => panic!("Expected BinstallError::CrateContext(_), got {e}"),
}
}
if resolution_fetches.is_empty() && resolution_sources.is_empty() {
return if let Some(err) = BinstallError::crate_errors(errors) {
Err(err.into())
} else {
debug!("Nothing to do");
Ok(())
};
}
if !dry_run && !no_confirm {
if let Err(abort_err) = confirm().await {
return if let Some(err) = BinstallError::crate_errors(errors) {
Err(Report::new(abort_err).wrap_err(err))
} else {
Err(abort_err.into())
};
}
}
let manifest_update_res = do_install_fetches_continue_on_failure(
resolution_fetches,
manifests,
&binstall_opts,
dry_run,
temp_dir,
no_cleanup,
&mut errors,
);
let tasks: Vec<_> = resolution_sources
.into_iter()
.map(|source| AutoAbortJoinHandle::spawn(source.install(binstall_opts.clone())))
.collect();
for task in tasks {
match task.flattened_join().await {
Ok(_) => (),
Err(BinstallError::CrateContext(err)) => errors.push(err),
Err(e) => panic!("Expected BinstallError::CrateContext(_), got {e}"),
}
}
match (BinstallError::crate_errors(errors), manifest_update_res) {
(None, Ok(())) => Ok(()),
(None, Err(err)) => Err(err),
(Some(err), Ok(())) => Err(err.into()),
(Some(err), Err(manifest_update_err)) => {
Err(Report::new(err).wrap_err(manifest_update_err))
}
}
})
} else {
AutoAbortJoinHandle::spawn(async move {
let mut resolution_fetches = Vec::new();
let mut resolution_sources = Vec::new();
for task in tasks {
match task.await?? {
Resolution::AlreadyUpToDate => {}
Resolution::Fetch(fetch) => {
fetch.print(&binstall_opts);
resolution_fetches.push(fetch)
}
Resolution::InstallFromSource(source) => {
source.print();
resolution_sources.push(source)
}
}
}
if resolution_fetches.is_empty() && resolution_sources.is_empty() {
debug!("Nothing to do");
return Ok(());
}
if !dry_run && !no_confirm {
confirm().await?;
}
do_install_fetches(
resolution_fetches,
manifests,
&binstall_opts,
dry_run,
temp_dir,
no_cleanup,
)?;
let tasks: Vec<_> = resolution_sources
.into_iter()
.map(|source| AutoAbortJoinHandle::spawn(source.install(binstall_opts.clone())))
.collect();
for task in tasks {
task.await??;
}
Ok(())
})
}))
}
fn do_read_root_cert(path: &Path) -> Result<Option<Certificate>, BinstallError> {
use std::io::{Read, Seek};
let mut file = fs::File::open(path)?;
let file_format = FileFormat::from_reader(&mut file)?;
let open_cert = match file_format {
FileFormat::PemCertificate => Certificate::from_pem,
FileFormat::DerCertificate => Certificate::from_der,
_ => {
warn!(
"Unable to load {}: Expected pem or der certificate but found {file_format}",
path.display()
);
return Ok(None);
}
};
file.rewind()?;
let mut buffer = Vec::with_capacity(200);
file.read_to_end(&mut buffer)?;
open_cert(&buffer).map_err(From::from).map(Some)
}
fn read_root_certs(
root_certificate_paths: Vec<PathBuf>,
config_cainfo: Option<PathBuf>,
) -> impl Iterator<Item = Certificate> {
root_certificate_paths
.into_iter()
.chain(config_cainfo)
.filter_map(|path| match do_read_root_cert(&path) {
Ok(optional_cert) => optional_cert,
Err(err) => {
warn!(
"Failed to load root certificate at {}: {err}",
path.display()
);
None
}
})
}
fn filter_out_installed_crates<'a>(
crate_names: Vec<CrateName>,
force: bool,
manifests: Option<&'a Manifests>,
version_req: Option<VersionReq>,
) -> impl Iterator<Item = Result<(CrateName, Option<semver::Version>), BinstallError>> + 'a {
let installed_crates = manifests.map(|m| m.installed_crates());
CrateName::dedup(crate_names)
.filter_map(move |mut crate_name| {
let name = &crate_name.name;
let curr_version = installed_crates
.and_then(|crates| crates.get(name));
match (crate_name.version_req.is_some(), version_req.is_some()) {
(false, true) => crate_name.version_req = version_req.clone(),
(true, true) => return Some(Err(BinstallError::SuperfluousVersionOption)),
_ => (),
};
match (
force,
curr_version,
&crate_name.version_req,
) {
(false, Some(curr_version), Some(version_req))
if version_req.is_latest_compatible(curr_version) =>
{
debug!("Bailing out early because we can assume wanted is already installed from metafile");
info!("{name} v{curr_version} is already installed, use --force to override");
None
}
(false, Some(curr_version), None) => {
Some(Ok((crate_name, Some(curr_version.clone()))))
}
_ => Some(Ok((crate_name, None))),
}
})
}
#[allow(clippy::vec_box)]
fn do_install_fetches(
resolution_fetches: Vec<Box<ResolutionFetch>>,
manifests: Option<Manifests>,
binstall_opts: &Options,
dry_run: bool,
temp_dir: tempfile::TempDir,
no_cleanup: bool,
) -> Result<()> {
if resolution_fetches.is_empty() {
return Ok(());
}
if dry_run {
info!("Dry-run: Not proceeding to install fetched binaries");
return Ok(());
}
block_in_place(|| {
let metadata_vec = resolution_fetches
.into_iter()
.map(|fetch| fetch.install(binstall_opts))
.collect::<Result<Vec<_>, BinstallError>>()?;
update_manifest(manifests, temp_dir, no_cleanup, metadata_vec)
})
}
#[allow(clippy::vec_box)]
fn do_install_fetches_continue_on_failure(
resolution_fetches: Vec<Box<ResolutionFetch>>,
manifests: Option<Manifests>,
binstall_opts: &Options,
dry_run: bool,
temp_dir: tempfile::TempDir,
no_cleanup: bool,
errors: &mut Vec<Box<CrateContextError>>,
) -> Result<()> {
if resolution_fetches.is_empty() {
return Ok(());
}
if dry_run {
info!("Dry-run: Not proceeding to install fetched binaries");
return Ok(());
}
block_in_place(|| {
let metadata_vec = resolution_fetches
.into_iter()
.filter_map(|fetch| match fetch.install(binstall_opts) {
Ok(crate_info) => Some(crate_info),
Err(BinstallError::CrateContext(err)) => {
errors.push(err);
None
}
Err(e) => panic!("Expected BinstallError::CrateContext(_), got {e}"),
})
.collect::<Vec<_>>();
update_manifest(manifests, temp_dir, no_cleanup, metadata_vec)
})
}
fn update_manifest(
manifests: Option<Manifests>,
temp_dir: tempfile::TempDir,
no_cleanup: bool,
metadata_vec: Vec<CrateInfo>,
) -> Result<()> {
if let Some(manifests) = manifests {
manifests.update(metadata_vec)?;
}
if no_cleanup {
let _ = temp_dir.keep();
} else {
temp_dir.close().unwrap_or_else(|err| {
warn!("Failed to clean up some resources: {err}");
});
}
Ok(())
}
pub fn self_install(args: Args) -> Result<()> {
let Init {
install_path,
manifests,
..
} = crate::initialise::initialise(&args)?;
let mut dest = install_path.join("cargo-binstall");
if cfg!(windows) {
assert!(dest.set_extension("exe"));
}
atomic_install(&env::current_exe().map_err(BinstallError::from)?, &dest)
.map_err(BinstallError::from)?;
if let Some(manifests) = manifests {
manifests.update(vec![CrateInfo {
name: CompactString::const_new("cargo-binstall"),
version_req: CompactString::const_new("*"),
current_version: Version::new(
env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(),
env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(),
env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
),
source: CrateSource::cratesio_registry(),
target: CompactString::const_new(TARGET),
bins: vec![CompactString::const_new("cargo-binstall")],
}])?;
}
Ok(())
}