use std::{fs, path::PathBuf, sync::Arc, time::Duration}; use binstalk::{ errors::BinstallError, fetchers::{Fetcher, GhCrateMeta, QuickInstall}, get_desired_targets, helpers::{jobserver_client::LazyJobserverClient, remote::Client, tasks::AutoAbortJoinHandle}, ops::{ self, resolve::{CrateName, Resolution, ResolutionFetch, VersionReqExt}, Resolver, }, }; use binstalk_manifests::cargo_toml_binstall::PkgOverride; use crates_io_api::AsyncClient as CratesIoApiClient; use log::LevelFilter; use miette::{miette, Result, WrapErr}; use tokio::task::block_in_place; use tracing::{debug, error, info, warn}; use crate::{ args::{Args, Strategy}, install_path, manifests::Manifests, ui::confirm, }; pub async fn install_crates(args: Args, jobserver_client: LazyJobserverClient) -> Result<()> { // Compute Resolvers let mut cargo_install_fallback = false; let resolvers: Vec<_> = args .strategies .into_iter() .filter_map(|strategy| match strategy { Strategy::CrateMetaData => Some(GhCrateMeta::new as Resolver), Strategy::QuickInstall => Some(QuickInstall::new as Resolver), Strategy::Compile => { cargo_install_fallback = true; None } }) .collect(); // Compute paths let (install_path, mut manifests, temp_dir) = compute_paths_and_load_manifests(args.roots, args.install_path)?; // Remove installed crates let mut crate_names = filter_out_installed_crates(args.crate_names, args.force, manifests.as_mut())?.peekable(); if crate_names.peek().is_none() { debug!("Nothing to do"); return Ok(()); } // Launch target detection let desired_targets = get_desired_targets(args.targets); // Computer cli_overrides let cli_overrides = PkgOverride { pkg_url: args.pkg_url, pkg_fmt: args.pkg_fmt, bin_dir: args.bin_dir, }; // Initialize reqwest client let rate_limit = args.rate_limit; let client = Client::new( concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")), args.min_tls_version.map(|v| v.into()), Duration::from_millis(rate_limit.duration.get()), rate_limit.request_count, ) .map_err(BinstallError::from)?; // Build crates.io api client let crates_io_api_client = CratesIoApiClient::with_http_client(client.get_inner().clone(), Duration::from_millis(100)); // Create binstall_opts let binstall_opts = Arc::new(ops::Options { no_symlinks: args.no_symlinks, dry_run: args.dry_run, force: args.force, quiet: args.log_level == Some(LevelFilter::Off), version_req: args.version_req, manifest_path: args.manifest_path, cli_overrides, desired_targets, resolvers, cargo_install_fallback, temp_dir: temp_dir.path().to_owned(), install_path, client, crates_io_api_client, jobserver_client, }); // Destruct args before any async function to reduce size of the future let dry_run = args.dry_run; let no_confirm = args.no_confirm; let no_cleanup = args.no_cleanup; // Resolve crates let tasks: Vec<_> = crate_names .map(|(crate_name, current_version)| { AutoAbortJoinHandle::spawn(ops::resolve::resolve( binstall_opts.clone(), crate_name, current_version, )) }) .collect(); // Collect results let mut resolution_fetchs = 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_fetchs.push(fetch) } Resolution::InstallFromSource(source) => { source.print(); resolution_sources.push(source) } } } if resolution_fetchs.is_empty() && resolution_sources.is_empty() { debug!("Nothing to do"); return Ok(()); } // Confirm if !dry_run && !no_confirm { confirm().await?; } do_install_fetches( resolution_fetchs, 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(()) } /// Return (install_path, manifests, temp_dir) fn compute_paths_and_load_manifests( roots: Option, install_path: Option, ) -> Result<(PathBuf, Option, tempfile::TempDir)> { block_in_place(|| { // Compute cargo_roots let cargo_roots = install_path::get_cargo_roots_path(roots).ok_or_else(|| { error!("No viable cargo roots path found of specified, try `--roots`"); miette!("No cargo roots path found or specified") })?; // Compute install directory let (install_path, custom_install_path) = install_path::get_install_path(install_path, Some(&cargo_roots)); let install_path = install_path.ok_or_else(|| { error!("No viable install path found of specified, try `--install-path`"); miette!("No install path found or specified") })?; fs::create_dir_all(&install_path).map_err(BinstallError::Io)?; debug!("Using install path: {}", install_path.display()); // Load manifests let manifests = if !custom_install_path { Some(Manifests::open_exclusive(&cargo_roots)?) } else { None }; // Create a temporary directory for downloads etc. // // Put all binaries to a temporary directory under `dst` first, catching // some failure modes (e.g., out of space) before touching the existing // binaries. This directory will get cleaned up via RAII. let temp_dir = tempfile::Builder::new() .prefix("cargo-binstall") .tempdir_in(&install_path) .map_err(BinstallError::from) .wrap_err("Creating a temporary directory failed.")?; Ok((install_path, manifests, temp_dir)) }) } /// Return vec of (crate_name, current_version) fn filter_out_installed_crates( crate_names: Vec, force: bool, manifests: Option<&mut Manifests>, ) -> Result)> + '_> { let mut installed_crates = manifests .map(Manifests::load_installed_crates) .transpose()?; Ok(CrateName::dedup(crate_names) .filter_map(move |crate_name| { let name = &crate_name.name; let curr_version = installed_crates .as_mut() // Since crate_name is deduped, every entry of installed_crates // can be visited at most once. // // So here we take ownership of the version stored to avoid cloning. .and_then(|crates| crates.remove(name)); 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 } // The version req is "*" thus a remote upgraded version could exist (false, Some(curr_version), None) => { Some((crate_name, Some(curr_version))) } _ => Some((crate_name, None)), } })) } #[allow(clippy::vec_box)] fn do_install_fetches( resolution_fetchs: Vec>, // Take manifests by value to drop the `FileLock`. manifests: Option, binstall_opts: &ops::Options, dry_run: bool, temp_dir: tempfile::TempDir, no_cleanup: bool, ) -> Result<()> { if resolution_fetchs.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_fetchs .into_iter() .map(|fetch| fetch.install(binstall_opts)) .collect::, BinstallError>>()?; if let Some(manifests) = manifests { manifests.update(metadata_vec)?; } if no_cleanup { // Consume temp_dir without removing it from fs. temp_dir.into_path(); } else { temp_dir.close().unwrap_or_else(|err| { warn!("Failed to clean up some resources: {err}"); }); } Ok(()) }) }