| use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; |
| use std::path::{Path, PathBuf}; |
| use std::sync::Arc; |
| use std::{env, fs}; |
| |
| use failure::{bail, format_err}; |
| use tempfile::Builder as TempFileBuilder; |
| |
| use crate::core::compiler::Freshness; |
| use crate::core::compiler::{CompileKind, DefaultExecutor, Executor}; |
| use crate::core::resolver::ResolveOpts; |
| use crate::core::{Edition, Package, PackageId, PackageIdSpec, Source, SourceId, Workspace}; |
| use crate::ops; |
| use crate::ops::common_for_install_and_uninstall::*; |
| use crate::sources::{GitSource, SourceConfigMap}; |
| use crate::util::errors::{CargoResult, CargoResultExt}; |
| use crate::util::{paths, Config, Filesystem}; |
| |
| struct Transaction { |
| bins: Vec<PathBuf>, |
| } |
| |
| impl Transaction { |
| fn success(mut self) { |
| self.bins.clear(); |
| } |
| } |
| |
| impl Drop for Transaction { |
| fn drop(&mut self) { |
| for bin in self.bins.iter() { |
| let _ = paths::remove_file(bin); |
| } |
| } |
| } |
| |
| pub fn install( |
| root: Option<&str>, |
| krates: Vec<&str>, |
| source_id: SourceId, |
| from_cwd: bool, |
| vers: Option<&str>, |
| opts: &ops::CompileOptions<'_>, |
| force: bool, |
| no_track: bool, |
| ) -> CargoResult<()> { |
| let root = resolve_root(root, opts.config)?; |
| let map = SourceConfigMap::new(opts.config)?; |
| |
| let (installed_anything, scheduled_error) = if krates.len() <= 1 { |
| install_one( |
| &root, |
| &map, |
| krates.into_iter().next(), |
| source_id, |
| from_cwd, |
| vers, |
| opts, |
| force, |
| no_track, |
| true, |
| )?; |
| (true, false) |
| } else { |
| let mut succeeded = vec![]; |
| let mut failed = vec![]; |
| let mut first = true; |
| for krate in krates { |
| let root = root.clone(); |
| let map = map.clone(); |
| match install_one( |
| &root, |
| &map, |
| Some(krate), |
| source_id, |
| from_cwd, |
| vers, |
| opts, |
| force, |
| no_track, |
| first, |
| ) { |
| Ok(()) => succeeded.push(krate), |
| Err(e) => { |
| crate::handle_error(&e, &mut opts.config.shell()); |
| failed.push(krate) |
| } |
| } |
| first = false; |
| } |
| |
| let mut summary = vec![]; |
| if !succeeded.is_empty() { |
| summary.push(format!("Successfully installed {}!", succeeded.join(", "))); |
| } |
| if !failed.is_empty() { |
| summary.push(format!( |
| "Failed to install {} (see error(s) above).", |
| failed.join(", ") |
| )); |
| } |
| if !succeeded.is_empty() || !failed.is_empty() { |
| opts.config.shell().status("Summary", summary.join(" "))?; |
| } |
| |
| (!succeeded.is_empty(), !failed.is_empty()) |
| }; |
| |
| if installed_anything { |
| // Print a warning that if this directory isn't in PATH that they won't be |
| // able to run these commands. |
| let dst = root.join("bin").into_path_unlocked(); |
| let path = env::var_os("PATH").unwrap_or_default(); |
| for path in env::split_paths(&path) { |
| if path == dst { |
| return Ok(()); |
| } |
| } |
| |
| opts.config.shell().warn(&format!( |
| "be sure to add `{}` to your PATH to be \ |
| able to run the installed binaries", |
| dst.display() |
| ))?; |
| } |
| |
| if scheduled_error { |
| bail!("some crates failed to install"); |
| } |
| |
| Ok(()) |
| } |
| |
| fn install_one( |
| root: &Filesystem, |
| map: &SourceConfigMap<'_>, |
| krate: Option<&str>, |
| source_id: SourceId, |
| from_cwd: bool, |
| vers: Option<&str>, |
| opts: &ops::CompileOptions<'_>, |
| force: bool, |
| no_track: bool, |
| is_first_install: bool, |
| ) -> CargoResult<()> { |
| let config = opts.config; |
| |
| let pkg = if source_id.is_git() { |
| select_pkg( |
| GitSource::new(source_id, config)?, |
| krate, |
| vers, |
| config, |
| true, |
| &mut |git| git.read_packages(), |
| )? |
| } else if source_id.is_path() { |
| let mut src = path_source(source_id, config)?; |
| if !src.path().is_dir() { |
| bail!( |
| "`{}` is not a directory. \ |
| --path must point to a directory containing a Cargo.toml file.", |
| src.path().display() |
| ) |
| } |
| if !src.path().join("Cargo.toml").exists() { |
| if from_cwd { |
| bail!( |
| "`{}` is not a crate root; specify a crate to \ |
| install from crates.io, or use --path or --git to \ |
| specify an alternate source", |
| src.path().display() |
| ); |
| } else { |
| bail!( |
| "`{}` does not contain a Cargo.toml file. \ |
| --path must point to a directory containing a Cargo.toml file.", |
| src.path().display() |
| ) |
| } |
| } |
| src.update()?; |
| select_pkg(src, krate, vers, config, false, &mut |path| { |
| path.read_packages() |
| })? |
| } else { |
| select_pkg( |
| map.load(source_id, &HashSet::new())?, |
| krate, |
| vers, |
| config, |
| is_first_install, |
| &mut |_| { |
| bail!( |
| "must specify a crate to install from \ |
| crates.io, or use --path or --git to \ |
| specify alternate source" |
| ) |
| }, |
| )? |
| }; |
| |
| let mut td_opt = None; |
| let mut needs_cleanup = false; |
| let overidden_target_dir = if source_id.is_path() { |
| None |
| } else if let Some(dir) = config.target_dir()? { |
| Some(dir) |
| } else if let Ok(td) = TempFileBuilder::new().prefix("cargo-install").tempdir() { |
| let p = td.path().to_owned(); |
| td_opt = Some(td); |
| Some(Filesystem::new(p)) |
| } else { |
| needs_cleanup = true; |
| Some(Filesystem::new(config.cwd().join("target-install"))) |
| }; |
| |
| let mut ws = match overidden_target_dir { |
| Some(dir) => Workspace::ephemeral(pkg, config, Some(dir), false)?, |
| None => { |
| let mut ws = Workspace::new(pkg.manifest_path(), config)?; |
| ws.set_require_optional_deps(false); |
| ws |
| } |
| }; |
| ws.set_ignore_lock(config.lock_update_allowed()); |
| let pkg = ws.current()?; |
| |
| if from_cwd { |
| if pkg.manifest().edition() == Edition::Edition2015 { |
| config.shell().warn( |
| "Using `cargo install` to install the binaries for the \ |
| package in current working directory is deprecated, \ |
| use `cargo install --path .` instead. \ |
| Use `cargo build` if you want to simply build the package.", |
| )? |
| } else { |
| bail!( |
| "Using `cargo install` to install the binaries for the \ |
| package in current working directory is no longer supported, \ |
| use `cargo install --path .` instead. \ |
| Use `cargo build` if you want to simply build the package." |
| ) |
| } |
| }; |
| |
| // For bare `cargo install` (no `--bin` or `--example`), check if there is |
| // *something* to install. Explicit `--bin` or `--example` flags will be |
| // checked at the start of `compile_ws`. |
| if !opts.filter.is_specific() && !pkg.targets().iter().any(|t| t.is_bin()) { |
| bail!("specified package `{}` has no binaries", pkg); |
| } |
| |
| // Preflight checks to check up front whether we'll overwrite something. |
| // We have to check this again afterwards, but may as well avoid building |
| // anything if we're gonna throw it away anyway. |
| let dst = root.join("bin").into_path_unlocked(); |
| let rustc = config.load_global_rustc(Some(&ws))?; |
| let target = match &opts.build_config.requested_kind { |
| CompileKind::Host => rustc.host.as_str(), |
| CompileKind::Target(target) => target.short_name(), |
| }; |
| |
| // Helper for --no-track flag to make sure it doesn't overwrite anything. |
| let no_track_duplicates = || -> CargoResult<BTreeMap<String, Option<PackageId>>> { |
| let duplicates: BTreeMap<String, Option<PackageId>> = exe_names(pkg, &opts.filter) |
| .into_iter() |
| .filter(|name| dst.join(name).exists()) |
| .map(|name| (name, None)) |
| .collect(); |
| if !force && !duplicates.is_empty() { |
| let mut msg: Vec<String> = duplicates |
| .iter() |
| .map(|(name, _)| format!("binary `{}` already exists in destination", name)) |
| .collect(); |
| msg.push("Add --force to overwrite".to_string()); |
| bail!("{}", msg.join("\n")); |
| } |
| Ok(duplicates) |
| }; |
| |
| // WARNING: no_track does not perform locking, so there is no protection |
| // of concurrent installs. |
| if no_track { |
| // Check for conflicts. |
| no_track_duplicates()?; |
| } else { |
| let tracker = InstallTracker::load(config, root)?; |
| let (freshness, _duplicates) = |
| tracker.check_upgrade(&dst, pkg, force, opts, target, &rustc.verbose_version)?; |
| if freshness == Freshness::Fresh { |
| let msg = format!( |
| "package `{}` is already installed, use --force to override", |
| pkg |
| ); |
| config.shell().status("Ignored", &msg)?; |
| return Ok(()); |
| } |
| // Unlock while building. |
| drop(tracker); |
| } |
| |
| config.shell().status("Installing", pkg)?; |
| |
| check_yanked_install(&ws)?; |
| |
| let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor); |
| let compile = ops::compile_ws(&ws, opts, &exec).chain_err(|| { |
| if let Some(td) = td_opt.take() { |
| // preserve the temporary directory, so the user can inspect it |
| td.into_path(); |
| } |
| |
| format_err!( |
| "failed to compile `{}`, intermediate artifacts can be \ |
| found at `{}`", |
| pkg, |
| ws.target_dir().display() |
| ) |
| })?; |
| let mut binaries: Vec<(&str, &Path)> = compile |
| .binaries |
| .iter() |
| .map(|bin| { |
| let name = bin.file_name().unwrap(); |
| if let Some(s) = name.to_str() { |
| Ok((s, bin.as_ref())) |
| } else { |
| bail!("Binary `{:?}` name can't be serialized into string", name) |
| } |
| }) |
| .collect::<CargoResult<_>>()?; |
| if binaries.is_empty() { |
| bail!("no binaries are available for install using the selected features"); |
| } |
| // This is primarily to make testing easier. |
| binaries.sort_unstable(); |
| |
| let (tracker, duplicates) = if no_track { |
| (None, no_track_duplicates()?) |
| } else { |
| let tracker = InstallTracker::load(config, root)?; |
| let (_freshness, duplicates) = |
| tracker.check_upgrade(&dst, pkg, force, opts, target, &rustc.verbose_version)?; |
| (Some(tracker), duplicates) |
| }; |
| |
| paths::create_dir_all(&dst)?; |
| |
| // Copy 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 staging_dir = TempFileBuilder::new() |
| .prefix("cargo-install") |
| .tempdir_in(&dst)?; |
| for &(bin, src) in binaries.iter() { |
| let dst = staging_dir.path().join(bin); |
| // Try to move if `target_dir` is transient. |
| if !source_id.is_path() && fs::rename(src, &dst).is_ok() { |
| continue; |
| } |
| fs::copy(src, &dst).chain_err(|| { |
| format_err!("failed to copy `{}` to `{}`", src.display(), dst.display()) |
| })?; |
| } |
| |
| let (to_replace, to_install): (Vec<&str>, Vec<&str>) = binaries |
| .iter() |
| .map(|&(bin, _)| bin) |
| .partition(|&bin| duplicates.contains_key(bin)); |
| |
| let mut installed = Transaction { bins: Vec::new() }; |
| let mut successful_bins = BTreeSet::new(); |
| |
| // Move the temporary copies into `dst` starting with new binaries. |
| for bin in to_install.iter() { |
| let src = staging_dir.path().join(bin); |
| let dst = dst.join(bin); |
| config.shell().status("Installing", dst.display())?; |
| fs::rename(&src, &dst).chain_err(|| { |
| format_err!("failed to move `{}` to `{}`", src.display(), dst.display()) |
| })?; |
| installed.bins.push(dst); |
| successful_bins.insert(bin.to_string()); |
| } |
| |
| // Repeat for binaries which replace existing ones but don't pop the error |
| // up until after updating metadata. |
| let replace_result = { |
| let mut try_install = || -> CargoResult<()> { |
| for &bin in to_replace.iter() { |
| let src = staging_dir.path().join(bin); |
| let dst = dst.join(bin); |
| config.shell().status("Replacing", dst.display())?; |
| fs::rename(&src, &dst).chain_err(|| { |
| format_err!("failed to move `{}` to `{}`", src.display(), dst.display()) |
| })?; |
| successful_bins.insert(bin.to_string()); |
| } |
| Ok(()) |
| }; |
| try_install() |
| }; |
| |
| if let Some(mut tracker) = tracker { |
| tracker.mark_installed( |
| pkg, |
| &successful_bins, |
| vers.map(|s| s.to_string()), |
| opts, |
| target, |
| &rustc.verbose_version, |
| ); |
| |
| if let Err(e) = remove_orphaned_bins(&ws, &mut tracker, &duplicates, pkg, &dst) { |
| // Don't hard error on remove. |
| config |
| .shell() |
| .warn(format!("failed to remove orphan: {:?}", e))?; |
| } |
| |
| match tracker.save() { |
| Err(err) => replace_result.chain_err(|| err)?, |
| Ok(_) => replace_result?, |
| } |
| } |
| |
| // Reaching here means all actions have succeeded. Clean up. |
| installed.success(); |
| if needs_cleanup { |
| // Don't bother grabbing a lock as we're going to blow it all away |
| // anyway. |
| let target_dir = ws.target_dir().into_path_unlocked(); |
| paths::remove_dir_all(&target_dir)?; |
| } |
| |
| // Helper for creating status messages. |
| fn executables<T: AsRef<str>>(mut names: impl Iterator<Item = T> + Clone) -> String { |
| if names.clone().count() == 1 { |
| format!("(executable `{}`)", names.next().unwrap().as_ref()) |
| } else { |
| format!( |
| "(executables {})", |
| names |
| .map(|b| format!("`{}`", b.as_ref())) |
| .collect::<Vec<_>>() |
| .join(", ") |
| ) |
| } |
| } |
| |
| if duplicates.is_empty() { |
| config.shell().status( |
| "Installed", |
| format!("package `{}` {}", pkg, executables(successful_bins.iter())), |
| )?; |
| Ok(()) |
| } else { |
| if !to_install.is_empty() { |
| config.shell().status( |
| "Installed", |
| format!("package `{}` {}", pkg, executables(to_install.iter())), |
| )?; |
| } |
| // Invert the duplicate map. |
| let mut pkg_map = BTreeMap::new(); |
| for (bin_name, opt_pkg_id) in &duplicates { |
| let key = opt_pkg_id.map_or_else(|| "unknown".to_string(), |pkg_id| pkg_id.to_string()); |
| pkg_map.entry(key).or_insert_with(Vec::new).push(bin_name); |
| } |
| for (pkg_descr, bin_names) in &pkg_map { |
| config.shell().status( |
| "Replaced", |
| format!( |
| "package `{}` with `{}` {}", |
| pkg_descr, |
| pkg, |
| executables(bin_names.iter()) |
| ), |
| )?; |
| } |
| Ok(()) |
| } |
| } |
| |
| fn check_yanked_install(ws: &Workspace<'_>) -> CargoResult<()> { |
| if ws.ignore_lock() || !ws.root().join("Cargo.lock").exists() { |
| return Ok(()); |
| } |
| let specs = vec![PackageIdSpec::from_package_id(ws.current()?.package_id())]; |
| // It would be best if `source` could be passed in here to avoid a |
| // duplicate "Updating", but since `source` is taken by value, then it |
| // wouldn't be available for `compile_ws`. |
| let ws_resolve = ops::resolve_ws_with_opts(ws, ResolveOpts::everything(), &specs)?; |
| let mut sources = ws_resolve.pkg_set.sources_mut(); |
| |
| // Checking the yanked status involves taking a look at the registry and |
| // maybe updating files, so be sure to lock it here. |
| let _lock = ws.config().acquire_package_cache_lock()?; |
| |
| for pkg_id in ws_resolve.targeted_resolve.iter() { |
| if let Some(source) = sources.get_mut(pkg_id.source_id()) { |
| if source.is_yanked(pkg_id)? { |
| ws.config().shell().warn(format!( |
| "package `{}` in Cargo.lock is yanked in registry `{}`, \ |
| consider running without --locked", |
| pkg_id, |
| pkg_id.source_id().display_registry_name() |
| ))?; |
| } |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Display a list of installed binaries. |
| pub fn install_list(dst: Option<&str>, config: &Config) -> CargoResult<()> { |
| let root = resolve_root(dst, config)?; |
| let tracker = InstallTracker::load(config, &root)?; |
| for (k, v) in tracker.all_installed_bins() { |
| println!("{}:", k); |
| for bin in v { |
| println!(" {}", bin); |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Removes executables that are no longer part of a package that was |
| /// previously installed. |
| fn remove_orphaned_bins( |
| ws: &Workspace<'_>, |
| tracker: &mut InstallTracker, |
| duplicates: &BTreeMap<String, Option<PackageId>>, |
| pkg: &Package, |
| dst: &Path, |
| ) -> CargoResult<()> { |
| let filter = ops::CompileFilter::new_all_targets(); |
| let all_self_names = exe_names(pkg, &filter); |
| let mut to_remove: HashMap<PackageId, BTreeSet<String>> = HashMap::new(); |
| // For each package that we stomped on. |
| for other_pkg in duplicates.values() { |
| // Only for packages with the same name. |
| if let Some(other_pkg) = other_pkg { |
| if other_pkg.name() == pkg.name() { |
| // Check what the old package had installed. |
| if let Some(installed) = tracker.installed_bins(*other_pkg) { |
| // If the old install has any names that no longer exist, |
| // add them to the list to remove. |
| for installed_name in installed { |
| if !all_self_names.contains(installed_name.as_str()) { |
| to_remove |
| .entry(*other_pkg) |
| .or_default() |
| .insert(installed_name.clone()); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| for (old_pkg, bins) in to_remove { |
| tracker.remove(old_pkg, &bins); |
| for bin in bins { |
| let full_path = dst.join(bin); |
| if full_path.exists() { |
| ws.config().shell().status( |
| "Removing", |
| format!( |
| "executable `{}` from previous version {}", |
| full_path.display(), |
| old_pkg |
| ), |
| )?; |
| paths::remove_file(&full_path) |
| .chain_err(|| format!("failed to remove {:?}", full_path))?; |
| } |
| } |
| } |
| Ok(()) |
| } |