blob: a5ebba0262bde97adb7537ca5688df8bf7d796c2 [file] [log] [blame]
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(())
}