]> git.proxmox.com Git - cargo.git/blame - src/cargo/ops/cargo_install.rs
Refactor `Kind` to carry target name in `Target`
[cargo.git] / src / cargo / ops / cargo_install.rs
CommitLineData
5a59b809 1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
bc60f64b 2use std::path::{Path, PathBuf};
1ab19a5a 3use std::sync::Arc;
e5a11190 4use std::{env, fs};
f8308bc5 5
e023a667 6use failure::{bail, format_err};
8daf81e1 7use tempfile::Builder as TempFileBuilder;
bc60f64b 8
e9428cba 9use crate::core::compiler::Freshness;
04ddd4d0 10use crate::core::compiler::{DefaultExecutor, Executor};
e26ef017 11use crate::core::resolver::ResolveOpts;
5a59b809 12use crate::core::{Edition, Package, PackageId, PackageIdSpec, Source, SourceId, Workspace};
e023a667 13use crate::ops;
6a197fb3 14use crate::ops::common_for_install_and_uninstall::*;
f51ac74f 15use crate::sources::{GitSource, SourceConfigMap};
04ddd4d0 16use crate::util::errors::{CargoResult, CargoResultExt};
e9428cba 17use crate::util::{paths, Config, Filesystem};
bc60f64b
AC
18
19struct Transaction {
20 bins: Vec<PathBuf>,
21}
22
9ea1f2af
GK
23impl Transaction {
24 fn success(mut self) {
25 self.bins.clear();
26 }
27}
28
bc60f64b
AC
29impl Drop for Transaction {
30 fn drop(&mut self) {
31 for bin in self.bins.iter() {
c933673e 32 let _ = paths::remove_file(bin);
bc60f64b
AC
33 }
34 }
35}
36
1e682848
AC
37pub fn install(
38 root: Option<&str>,
39 krates: Vec<&str>,
e5a11190 40 source_id: SourceId,
d6786057 41 from_cwd: bool,
1e682848 42 vers: Option<&str>,
b8b7faee 43 opts: &ops::CompileOptions<'_>,
1e682848 44 force: bool,
e023a667 45 no_track: bool,
1e682848 46) -> CargoResult<()> {
497c2975
AB
47 let root = resolve_root(root, opts.config)?;
48 let map = SourceConfigMap::new(opts.config)?;
49
ce2d69d2 50 let (installed_anything, scheduled_error) = if krates.len() <= 1 {
1e682848
AC
51 install_one(
52 &root,
53 &map,
54 krates.into_iter().next(),
55 source_id,
d6786057 56 from_cwd,
1e682848
AC
57 vers,
58 opts,
59 force,
e023a667 60 no_track,
1e682848
AC
61 true,
62 )?;
ce2d69d2 63 (true, false)
497c2975 64 } else {
aa201ab7
AB
65 let mut succeeded = vec![];
66 let mut failed = vec![];
daf4eabb 67 let mut first = true;
497c2975
AB
68 for krate in krates {
69 let root = root.clone();
70 let map = map.clone();
1e682848
AC
71 match install_one(
72 &root,
73 &map,
74 Some(krate),
75 source_id,
d6786057 76 from_cwd,
1e682848
AC
77 vers,
78 opts,
79 force,
e023a667 80 no_track,
1e682848
AC
81 first,
82 ) {
aa201ab7
AB
83 Ok(()) => succeeded.push(krate),
84 Err(e) => {
04ddd4d0 85 crate::handle_error(&e, &mut opts.config.shell());
aa201ab7
AB
86 failed.push(krate)
87 }
497c2975 88 }
daf4eabb 89 first = false;
497c2975
AB
90 }
91
f78fc7c2 92 let mut summary = vec![];
aa201ab7 93 if !succeeded.is_empty() {
f78fc7c2 94 summary.push(format!("Successfully installed {}!", succeeded.join(", ")));
aa201ab7
AB
95 }
96 if !failed.is_empty() {
1e682848
AC
97 summary.push(format!(
98 "Failed to install {} (see error(s) above).",
99 failed.join(", ")
100 ));
f78fc7c2
AB
101 }
102 if !succeeded.is_empty() || !failed.is_empty() {
37cffbe0 103 opts.config.shell().status("Summary", summary.join(" "))?;
aa201ab7 104 }
497c2975 105
ce2d69d2 106 (!succeeded.is_empty(), !failed.is_empty())
f78fc7c2
AB
107 };
108
109 if installed_anything {
110 // Print a warning that if this directory isn't in PATH that they won't be
111 // able to run these commands.
e023a667 112 let dst = root.join("bin").into_path_unlocked();
23591fe5 113 let path = env::var_os("PATH").unwrap_or_default();
f78fc7c2
AB
114 for path in env::split_paths(&path) {
115 if path == dst {
1e682848 116 return Ok(());
f78fc7c2 117 }
daf4eabb 118 }
daf4eabb 119
1e682848
AC
120 opts.config.shell().warn(&format!(
121 "be sure to add `{}` to your PATH to be \
122 able to run the installed binaries",
123 dst.display()
124 ))?;
f78fc7c2 125 }
daf4eabb 126
ce2d69d2 127 if scheduled_error {
e023a667 128 bail!("some crates failed to install");
ce2d69d2
AB
129 }
130
daf4eabb 131 Ok(())
497c2975
AB
132}
133
1e682848
AC
134fn install_one(
135 root: &Filesystem,
b8b7faee 136 map: &SourceConfigMap<'_>,
1e682848 137 krate: Option<&str>,
e5a11190 138 source_id: SourceId,
d6786057 139 from_cwd: bool,
1e682848 140 vers: Option<&str>,
b8b7faee 141 opts: &ops::CompileOptions<'_>,
1e682848 142 force: bool,
e023a667 143 no_track: bool,
1e682848
AC
144 is_first_install: bool,
145) -> CargoResult<()> {
f8308bc5 146 let config = opts.config;
497c2975 147
8a301589 148 let pkg = if source_id.is_git() {
1e682848
AC
149 select_pkg(
150 GitSource::new(source_id, config)?,
151 krate,
152 vers,
153 config,
494fdabe 154 true,
1e682848
AC
155 &mut |git| git.read_packages(),
156 )?
bc60f64b 157 } else if source_id.is_path() {
8921abd7 158 let mut src = path_source(source_id, config)?;
3378a5a1 159 if !src.path().is_dir() {
e023a667 160 bail!(
3378a5a1
EH
161 "`{}` is not a directory. \
162 --path must point to a directory containing a Cargo.toml file.",
8921abd7 163 src.path().display()
1e682848 164 )
3378a5a1
EH
165 }
166 if !src.path().join("Cargo.toml").exists() {
167 if from_cwd {
e023a667 168 bail!(
3378a5a1
EH
169 "`{}` is not a crate root; specify a crate to \
170 install from crates.io, or use --path or --git to \
171 specify an alternate source",
172 src.path().display()
173 );
174 } else {
e023a667 175 bail!(
3378a5a1
EH
176 "`{}` does not contain a Cargo.toml file. \
177 --path must point to a directory containing a Cargo.toml file.",
178 src.path().display()
179 )
180 }
181 }
182 src.update()?;
e5a11190
E
183 select_pkg(src, krate, vers, config, false, &mut |path| {
184 path.read_packages()
185 })?
bc60f64b 186 } else {
1e682848 187 select_pkg(
192073bf 188 map.load(source_id, &HashSet::new())?,
1e682848
AC
189 krate,
190 vers,
191 config,
192 is_first_install,
193 &mut |_| {
e023a667 194 bail!(
1e682848
AC
195 "must specify a crate to install from \
196 crates.io, or use --path or --git to \
197 specify alternate source"
198 )
199 },
200 )?
bc60f64b 201 };
3c9b362b 202
3c9b362b 203 let mut td_opt = None;
a81b1437 204 let mut needs_cleanup = false;
3c9b362b
AK
205 let overidden_target_dir = if source_id.is_path() {
206 None
695bc5e4 207 } else if let Some(dir) = config.target_dir()? {
a81b1437 208 Some(dir)
8daf81e1 209 } else if let Ok(td) = TempFileBuilder::new().prefix("cargo-install").tempdir() {
3c9b362b
AK
210 let p = td.path().to_owned();
211 td_opt = Some(td);
212 Some(Filesystem::new(p))
213 } else {
a81b1437 214 needs_cleanup = true;
3c9b362b
AK
215 Some(Filesystem::new(config.cwd().join("target-install")))
216 };
217
eae89007
EH
218 let mut ws = match overidden_target_dir {
219 Some(dir) => Workspace::ephemeral(pkg, config, Some(dir), false)?,
d1ef031a
AC
220 None => {
221 let mut ws = Workspace::new(pkg.manifest_path(), config)?;
222 ws.set_require_optional_deps(false);
223 ws
224 }
225 };
eae89007 226 ws.set_ignore_lock(config.lock_update_allowed());
82655b46 227 let pkg = ws.current()?;
bc60f64b 228
d6786057 229 if from_cwd {
b765fad1
DW
230 if pkg.manifest().edition() == Edition::Edition2015 {
231 config.shell().warn(
fc93b0f9 232 "Using `cargo install` to install the binaries for the \
3492a390 233 package in current working directory is deprecated, \
fc93b0f9
DW
234 use `cargo install --path .` instead. \
235 Use `cargo build` if you want to simply build the package.",
b765fad1
DW
236 )?
237 } else {
e023a667 238 bail!(
fc93b0f9 239 "Using `cargo install` to install the binaries for the \
3492a390 240 package in current working directory is no longer supported, \
fc93b0f9 241 use `cargo install --path .` instead. \
3e6de57a 242 Use `cargo build` if you want to simply build the package."
b765fad1 243 )
4676820f
DW
244 }
245 };
246
9ecdfe04
EH
247 // For bare `cargo install` (no `--bin` or `--example`), check if there is
248 // *something* to install. Explicit `--bin` or `--example` flags will be
249 // checked at the start of `compile_ws`.
e023a667
EH
250 if !opts.filter.is_specific() && !pkg.targets().iter().any(|t| t.is_bin()) {
251 bail!("specified package `{}` has no binaries", pkg);
252 }
82a8f6f7 253
8eac1d62
AC
254 // Preflight checks to check up front whether we'll overwrite something.
255 // We have to check this again afterwards, but may as well avoid building
256 // anything if we're gonna throw it away anyway.
e023a667
EH
257 let dst = root.join("bin").into_path_unlocked();
258 let rustc = config.load_global_rustc(Some(&ws))?;
259 let target = opts
260 .build_config
261 .requested_target
262 .as_ref()
263 .unwrap_or(&rustc.host)
264 .clone();
265
266 // Helper for --no-track flag to make sure it doesn't overwrite anything.
267 let no_track_duplicates = || -> CargoResult<BTreeMap<String, Option<PackageId>>> {
268 let duplicates: BTreeMap<String, Option<PackageId>> = exe_names(pkg, &opts.filter)
269 .into_iter()
270 .filter(|name| dst.join(name).exists())
271 .map(|name| (name, None))
272 .collect();
273 if !force && !duplicates.is_empty() {
274 let mut msg: Vec<String> = duplicates
275 .iter()
276 .map(|(name, _)| format!("binary `{}` already exists in destination", name))
277 .collect();
278 msg.push("Add --force to overwrite".to_string());
279 bail!("{}", msg.join("\n"));
280 }
281 Ok(duplicates)
282 };
283
5a9ff8a6
EH
284 // WARNING: no_track does not perform locking, so there is no protection
285 // of concurrent installs.
e023a667
EH
286 if no_track {
287 // Check for conflicts.
288 no_track_duplicates()?;
289 } else {
290 let tracker = InstallTracker::load(config, root)?;
291 let (freshness, _duplicates) =
292 tracker.check_upgrade(&dst, pkg, force, opts, &target, &rustc.verbose_version)?;
293 if freshness == Freshness::Fresh {
294 let msg = format!(
295 "package `{}` is already installed, use --force to override",
296 pkg
297 );
298 config.shell().status("Ignored", &msg)?;
299 return Ok(());
300 }
301 // Unlock while building.
302 drop(tracker);
8eac1d62 303 }
bc60f64b 304
e023a667
EH
305 config.shell().status("Installing", pkg)?;
306
5f616eb1
EH
307 check_yanked_install(&ws)?;
308
b8b7faee 309 let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
8a301589 310 let compile = ops::compile_ws(&ws, opts, &exec).chain_err(|| {
e5a11190
E
311 if let Some(td) = td_opt.take() {
312 // preserve the temporary directory, so the user can inspect it
313 td.into_path();
314 }
1a6bad8e 315
e023a667 316 format_err!(
e5a11190
E
317 "failed to compile `{}`, intermediate artifacts can be \
318 found at `{}`",
319 pkg,
320 ws.target_dir().display()
321 )
322 })?;
e023a667 323 let mut binaries: Vec<(&str, &Path)> = compile
1e682848
AC
324 .binaries
325 .iter()
326 .map(|bin| {
327 let name = bin.file_name().unwrap();
328 if let Some(s) = name.to_str() {
329 Ok((s, bin.as_ref()))
330 } else {
e023a667 331 bail!("Binary `{:?}` name can't be serialized into string", name)
1e682848
AC
332 }
333 })
334 .collect::<CargoResult<_>>()?;
d43fd2ea 335 if binaries.is_empty() {
e023a667 336 bail!("no binaries are available for install using the selected features");
d43fd2ea 337 }
e023a667
EH
338 // This is primarily to make testing easier.
339 binaries.sort_unstable();
bc60f64b 340
e023a667
EH
341 let (tracker, duplicates) = if no_track {
342 (None, no_track_duplicates()?)
343 } else {
344 let tracker = InstallTracker::load(config, root)?;
345 let (_freshness, duplicates) =
346 tracker.check_upgrade(&dst, pkg, force, opts, &target, &rustc.verbose_version)?;
347 (Some(tracker), duplicates)
348 };
8eac1d62 349
5102de2b 350 paths::create_dir_all(&dst)?;
9ea1f2af
GK
351
352 // Copy all binaries to a temporary directory under `dst` first, catching
f7c91ba6 353 // some failure modes (e.g., out of space) before touching the existing
9ea1f2af 354 // binaries. This directory will get cleaned up via RAII.
8daf81e1
BO
355 let staging_dir = TempFileBuilder::new()
356 .prefix("cargo-install")
357 .tempdir_in(&dst)?;
9ea1f2af
GK
358 for &(bin, src) in binaries.iter() {
359 let dst = staging_dir.path().join(bin);
9f0fa249 360 // Try to move if `target_dir` is transient.
23591fe5 361 if !source_id.is_path() && fs::rename(src, &dst).is_ok() {
1e682848 362 continue;
9f0fa249 363 }
e95044e3 364 fs::copy(src, &dst).chain_err(|| {
e023a667 365 format_err!("failed to copy `{}` to `{}`", src.display(), dst.display())
82655b46 366 })?;
9ea1f2af
GK
367 }
368
1e682848
AC
369 let (to_replace, to_install): (Vec<&str>, Vec<&str>) = binaries
370 .iter()
371 .map(|&(bin, _)| bin)
372 .partition(|&bin| duplicates.contains_key(bin));
9ea1f2af
GK
373
374 let mut installed = Transaction { bins: Vec::new() };
e023a667 375 let mut successful_bins = BTreeSet::new();
9ea1f2af
GK
376
377 // Move the temporary copies into `dst` starting with new binaries.
378 for bin in to_install.iter() {
379 let src = staging_dir.path().join(bin);
380 let dst = dst.join(bin);
82655b46 381 config.shell().status("Installing", dst.display())?;
e95044e3 382 fs::rename(&src, &dst).chain_err(|| {
e023a667 383 format_err!("failed to move `{}` to `{}`", src.display(), dst.display())
82655b46 384 })?;
9ea1f2af 385 installed.bins.push(dst);
e023a667 386 successful_bins.insert(bin.to_string());
9ea1f2af
GK
387 }
388
389 // Repeat for binaries which replace existing ones but don't pop the error
390 // up until after updating metadata.
e023a667 391 let replace_result = {
9ea1f2af
GK
392 let mut try_install = || -> CargoResult<()> {
393 for &bin in to_replace.iter() {
394 let src = staging_dir.path().join(bin);
395 let dst = dst.join(bin);
82655b46 396 config.shell().status("Replacing", dst.display())?;
e95044e3 397 fs::rename(&src, &dst).chain_err(|| {
e023a667 398 format_err!("failed to move `{}` to `{}`", src.display(), dst.display())
82655b46 399 })?;
e023a667 400 successful_bins.insert(bin.to_string());
9ea1f2af
GK
401 }
402 Ok(())
403 };
404 try_install()
405 };
406
9ecdfe04 407 if let Some(mut tracker) = tracker {
e023a667
EH
408 tracker.mark_installed(
409 pkg,
410 &successful_bins,
411 vers.map(|s| s.to_string()),
412 opts,
593a02f2
AC
413 &target,
414 &rustc.verbose_version,
e023a667 415 );
9ea1f2af 416
5a59b809
EH
417 if let Err(e) = remove_orphaned_bins(&ws, &mut tracker, &duplicates, pkg, &dst) {
418 // Don't hard error on remove.
419 config
420 .shell()
421 .warn(format!("failed to remove orphan: {:?}", e))?;
422 }
423
e023a667
EH
424 match tracker.save() {
425 Err(err) => replace_result.chain_err(|| err)?,
426 Ok(_) => replace_result?,
427 }
bc60f64b 428 }
958f9d92 429
9ea1f2af
GK
430 // Reaching here means all actions have succeeded. Clean up.
431 installed.success();
a81b1437 432 if needs_cleanup {
a9fd1c2c
AC
433 // Don't bother grabbing a lock as we're going to blow it all away
434 // anyway.
3c9b362b 435 let target_dir = ws.target_dir().into_path_unlocked();
c933673e 436 paths::remove_dir_all(&target_dir)?;
958f9d92 437 }
bc60f64b 438
e023a667
EH
439 // Helper for creating status messages.
440 fn executables<T: AsRef<str>>(mut names: impl Iterator<Item = T> + Clone) -> String {
441 if names.clone().count() == 1 {
442 format!("(executable `{}`)", names.next().unwrap().as_ref())
9ea1f2af 443 } else {
e023a667
EH
444 format!(
445 "(executables {})",
446 names
447 .map(|b| format!("`{}`", b.as_ref()))
448 .collect::<Vec<_>>()
449 .join(", ")
450 )
9ea1f2af
GK
451 }
452 }
9ea1f2af 453
e023a667
EH
454 if duplicates.is_empty() {
455 config.shell().status(
456 "Installed",
457 format!("package `{}` {}", pkg, executables(successful_bins.iter())),
458 )?;
459 Ok(())
460 } else {
461 if !to_install.is_empty() {
462 config.shell().status(
463 "Installed",
464 format!("package `{}` {}", pkg, executables(to_install.iter())),
465 )?;
bc60f64b 466 }
e023a667
EH
467 // Invert the duplicate map.
468 let mut pkg_map = BTreeMap::new();
469 for (bin_name, opt_pkg_id) in &duplicates {
470 let key = opt_pkg_id.map_or_else(|| "unknown".to_string(), |pkg_id| pkg_id.to_string());
ea40fc4f 471 pkg_map.entry(key).or_insert_with(Vec::new).push(bin_name);
e023a667
EH
472 }
473 for (pkg_descr, bin_names) in &pkg_map {
474 config.shell().status(
475 "Replaced",
476 format!(
477 "package `{}` with `{}` {}",
478 pkg_descr,
479 pkg,
480 executables(bin_names.iter())
481 ),
482 )?;
bc60f64b 483 }
e023a667 484 Ok(())
bc60f64b 485 }
bc60f64b
AC
486}
487
5f616eb1
EH
488fn check_yanked_install(ws: &Workspace<'_>) -> CargoResult<()> {
489 if ws.ignore_lock() || !ws.root().join("Cargo.lock").exists() {
490 return Ok(());
491 }
492 let specs = vec![PackageIdSpec::from_package_id(ws.current()?.package_id())];
493 // It would be best if `source` could be passed in here to avoid a
494 // duplicate "Updating", but since `source` is taken by value, then it
495 // wouldn't be available for `compile_ws`.
e26ef017 496 let (pkg_set, resolve) = ops::resolve_ws_with_opts(ws, ResolveOpts::everything(), &specs)?;
5f616eb1 497 let mut sources = pkg_set.sources_mut();
5217280e 498
e26ef017 499 // Checking the yanked status involves taking a look at the registry and
5217280e
AC
500 // maybe updating files, so be sure to lock it here.
501 let _lock = ws.config().acquire_package_cache_lock()?;
502
5f616eb1
EH
503 for pkg_id in resolve.iter() {
504 if let Some(source) = sources.get_mut(pkg_id.source_id()) {
505 if source.is_yanked(pkg_id)? {
506 ws.config().shell().warn(format!(
507 "package `{}` in Cargo.lock is yanked in registry `{}`, \
508 consider running without --locked",
509 pkg_id,
510 pkg_id.source_id().display_registry_name()
511 ))?;
512 }
513 }
514 }
515
516 Ok(())
517}
518
e023a667 519/// Display a list of installed binaries.
bc60f64b 520pub fn install_list(dst: Option<&str>, config: &Config) -> CargoResult<()> {
e023a667
EH
521 let root = resolve_root(dst, config)?;
522 let tracker = InstallTracker::load(config, &root)?;
523 for (k, v) in tracker.all_installed_bins() {
f8fb0a02 524 println!("{}:", k);
bc60f64b 525 for bin in v {
f8fb0a02 526 println!(" {}", bin);
bc60f64b
AC
527 }
528 }
529 Ok(())
530}
5a59b809
EH
531
532/// Removes executables that are no longer part of a package that was
533/// previously installed.
534fn remove_orphaned_bins(
535 ws: &Workspace<'_>,
536 tracker: &mut InstallTracker,
537 duplicates: &BTreeMap<String, Option<PackageId>>,
538 pkg: &Package,
539 dst: &Path,
540) -> CargoResult<()> {
541 let filter = ops::CompileFilter::new_all_targets();
542 let all_self_names = exe_names(pkg, &filter);
543 let mut to_remove: HashMap<PackageId, BTreeSet<String>> = HashMap::new();
544 // For each package that we stomped on.
545 for other_pkg in duplicates.values() {
546 // Only for packages with the same name.
547 if let Some(other_pkg) = other_pkg {
548 if other_pkg.name() == pkg.name() {
549 // Check what the old package had installed.
550 if let Some(installed) = tracker.installed_bins(*other_pkg) {
551 // If the old install has any names that no longer exist,
552 // add them to the list to remove.
553 for installed_name in installed {
554 if !all_self_names.contains(installed_name.as_str()) {
555 to_remove
556 .entry(*other_pkg)
557 .or_default()
558 .insert(installed_name.clone());
559 }
560 }
561 }
562 }
563 }
564 }
565
566 for (old_pkg, bins) in to_remove {
567 tracker.remove(old_pkg, &bins);
568 for bin in bins {
569 let full_path = dst.join(bin);
570 if full_path.exists() {
571 ws.config().shell().status(
572 "Removing",
573 format!(
574 "executable `{}` from previous version {}",
575 full_path.display(),
576 old_pkg
577 ),
578 )?;
579 paths::remove_file(&full_path)
580 .chain_err(|| format!("failed to remove {:?}", full_path))?;
581 }
582 }
583 }
584 Ok(())
585}