1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4use std::{env, fs};
5
6use anyhow::{bail, format_err};
7use tempfile::Builder as TempFileBuilder;
8
9use crate::core::compiler::Freshness;
10use crate::core::compiler::{CompileKind, DefaultExecutor, Executor};
11use crate::core::{Edition, Package, PackageId, Source, SourceId, Workspace};
12use crate::ops;
13use crate::ops::common_for_install_and_uninstall::*;
14use crate::sources::{GitSource, SourceConfigMap};
15use crate::util::errors::{CargoResult, CargoResultExt};
16use crate::util::{paths, Config, Filesystem};
17
18struct Transaction {
19 bins: Vec<PathBuf>,
20}
21
22impl Transaction {
23 fn success(mut self) {
24 self.bins.clear();
25 }
26}
27
28impl Drop for Transaction {
29 fn drop(&mut self) {
30 for bin in self.bins.iter() {
31 let _ = paths::remove_file(bin);
32 }
33 }
34}
35
36pub fn install(
37 config: &Config,
38 root: Option<&str>,
39 krates: Vec<&str>,
40 source_id: SourceId,
41 from_cwd: bool,
42 vers: Option<&str>,
43 opts: &ops::CompileOptions,
44 force: bool,
45 no_track: bool,
46) -> CargoResult<()> {
47 let root = resolve_root(root, config)?;
48 let map = SourceConfigMap::new(config)?;
49
50 let (installed_anything, scheduled_error) = if krates.len() <= 1 {
51 install_one(
52 config,
53 &root,
54 &map,
55 krates.into_iter().next(),
56 source_id,
57 from_cwd,
58 vers,
59 opts,
60 force,
61 no_track,
62 true,
63 )?;
64 (true, false)
65 } else {
66 let mut succeeded = vec![];
67 let mut failed = vec![];
68 let mut first = true;
69 for krate in krates {
70 let root = root.clone();
71 let map = map.clone();
72 match install_one(
73 config,
74 &root,
75 &map,
76 Some(krate),
77 source_id,
78 from_cwd,
79 vers,
80 opts,
81 force,
82 no_track,
83 first,
84 ) {
85 Ok(()) => succeeded.push(krate),
86 Err(e) => {
87 crate::display_error(&e, &mut config.shell());
88 failed.push(krate)
89 }
90 }
91 first = false;
92 }
93
94 let mut summary = vec![];
95 if !succeeded.is_empty() {
96 summary.push(format!("Successfully installed {}!", succeeded.join(", ")));
97 }
98 if !failed.is_empty() {
99 summary.push(format!(
100 "Failed to install {} (see error(s) above).",
101 failed.join(", ")
102 ));
103 }
104 if !succeeded.is_empty() || !failed.is_empty() {
105 config.shell().status("Summary", summary.join(" "))?;
106 }
107
108 (!succeeded.is_empty(), !failed.is_empty())
109 };
110
111 if installed_anything {
112 let dst = root.join("bin").into_path_unlocked();
115 let path = env::var_os("PATH").unwrap_or_default();
116 for path in env::split_paths(&path) {
117 if path == dst {
118 return Ok(());
119 }
120 }
121
122 config.shell().warn(&format!(
123 "be sure to add `{}` to your PATH to be \
124 able to run the installed binaries",
125 dst.display()
126 ))?;
127 }
128
129 if scheduled_error {
130 bail!("some crates failed to install");
131 }
132
133 Ok(())
134}
135
136fn install_one(
137 config: &Config,
138 root: &Filesystem,
139 map: &SourceConfigMap<'_>,
140 krate: Option<&str>,
141 source_id: SourceId,
142 from_cwd: bool,
143 vers: Option<&str>,
144 opts: &ops::CompileOptions,
145 force: bool,
146 no_track: bool,
147 is_first_install: bool,
148) -> CargoResult<()> {
149 let pkg = if source_id.is_git() {
150 select_pkg(
151 GitSource::new(source_id, config)?,
152 krate,
153 vers,
154 config,
155 true,
156 &mut |git| git.read_packages(),
157 )?
158 } else if source_id.is_path() {
159 let mut src = path_source(source_id, config)?;
160 if !src.path().is_dir() {
161 bail!(
162 "`{}` is not a directory. \
163 --path must point to a directory containing a Cargo.toml file.",
164 src.path().display()
165 )
166 }
167 if !src.path().join("Cargo.toml").exists() {
168 if from_cwd {
169 bail!(
170 "`{}` is not a crate root; specify a crate to \
171 install from crates.io, or use --path or --git to \
172 specify an alternate source",
173 src.path().display()
174 );
175 } else {
176 bail!(
177 "`{}` does not contain a Cargo.toml file. \
178 --path must point to a directory containing a Cargo.toml file.",
179 src.path().display()
180 )
181 }
182 }
183 src.update()?;
184 select_pkg(src, krate, vers, config, false, &mut |path| {
185 path.read_packages()
186 })?
187 } else {
188 select_pkg(
189 map.load(source_id, &HashSet::new())?,
190 krate,
191 vers,
192 config,
193 is_first_install,
194 &mut |_| {
195 bail!(
196 "must specify a crate to install from \
197 crates.io, or use --path or --git to \
198 specify alternate source"
199 )
200 },
201 )?
202 };
203
204 let (mut ws, git_package) = if source_id.is_git() {
205 (Workspace::new(pkg.manifest_path(), config)?, Some(&pkg))
208 } else if source_id.is_path() {
209 (Workspace::new(pkg.manifest_path(), config)?, None)
210 } else {
211 (Workspace::ephemeral(pkg, config, None, false)?, None)
212 };
213 ws.set_ignore_lock(config.lock_update_allowed());
214 ws.set_require_optional_deps(false);
215
216 let mut td_opt = None;
217 let mut needs_cleanup = false;
218 if !source_id.is_path() {
219 let target_dir = if let Some(dir) = config.target_dir()? {
220 dir
221 } else if let Ok(td) = TempFileBuilder::new().prefix("cargo-install").tempdir() {
222 let p = td.path().to_owned();
223 td_opt = Some(td);
224 Filesystem::new(p)
225 } else {
226 needs_cleanup = true;
227 Filesystem::new(config.cwd().join("target-install"))
228 };
229 ws.set_target_dir(target_dir);
230 }
231
232 let pkg = git_package.map_or_else(|| ws.current(), |pkg| Ok(pkg))?;
233
234 if from_cwd {
235 if pkg.manifest().edition() == Edition::Edition2015 {
236 config.shell().warn(
237 "Using `cargo install` to install the binaries for the \
238 package in current working directory is deprecated, \
239 use `cargo install --path .` instead. \
240 Use `cargo build` if you want to simply build the package.",
241 )?
242 } else {
243 bail!(
244 "Using `cargo install` to install the binaries for the \
245 package in current working directory is no longer supported, \
246 use `cargo install --path .` instead. \
247 Use `cargo build` if you want to simply build the package."
248 )
249 }
250 };
251
252 if !opts.filter.is_specific() && !pkg.targets().iter().any(|t| t.is_bin()) {
256 bail!("specified package `{}` has no binaries", pkg);
257 }
258
259 let dst = root.join("bin").into_path_unlocked();
263 let rustc = config.load_global_rustc(Some(&ws))?;
264 let target = match &opts.build_config.requested_kind {
265 CompileKind::Host => rustc.host.as_str(),
266 CompileKind::Target(target) => target.short_name(),
267 };
268
269 let no_track_duplicates = || -> CargoResult<BTreeMap<String, Option<PackageId>>> {
271 let duplicates: BTreeMap<String, Option<PackageId>> = exe_names(pkg, &opts.filter)
272 .into_iter()
273 .filter(|name| dst.join(name).exists())
274 .map(|name| (name, None))
275 .collect();
276 if !force && !duplicates.is_empty() {
277 let mut msg: Vec<String> = duplicates
278 .iter()
279 .map(|(name, _)| format!("binary `{}` already exists in destination", name))
280 .collect();
281 msg.push("Add --force to overwrite".to_string());
282 bail!("{}", msg.join("\n"));
283 }
284 Ok(duplicates)
285 };
286
287 if no_track {
290 no_track_duplicates()?;
292 } else {
293 let tracker = InstallTracker::load(config, root)?;
294 let (freshness, _duplicates) =
295 tracker.check_upgrade(&dst, pkg, force, opts, target, &rustc.verbose_version)?;
296 if freshness == Freshness::Fresh {
297 let msg = format!(
298 "package `{}` is already installed, use --force to override",
299 pkg
300 );
301 config.shell().status("Ignored", &msg)?;
302 return Ok(());
303 }
304 drop(tracker);
306 }
307
308 config.shell().status("Installing", pkg)?;
309
310 check_yanked_install(&ws)?;
311
312 let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
313 let compile = ops::compile_ws(&ws, opts, &exec).chain_err(|| {
314 if let Some(td) = td_opt.take() {
315 td.into_path();
317 }
318
319 format_err!(
320 "failed to compile `{}`, intermediate artifacts can be \
321 found at `{}`",
322 pkg,
323 ws.target_dir().display()
324 )
325 })?;
326 let mut binaries: Vec<(&str, &Path)> = compile
327 .binaries
328 .iter()
329 .map(|bin| {
330 let name = bin.file_name().unwrap();
331 if let Some(s) = name.to_str() {
332 Ok((s, bin.as_ref()))
333 } else {
334 bail!("Binary `{:?}` name can't be serialized into string", name)
335 }
336 })
337 .collect::<CargoResult<_>>()?;
338 if binaries.is_empty() {
339 bail!("no binaries are available for install using the selected features");
340 }
341 binaries.sort_unstable();
343
344 let (tracker, duplicates) = if no_track {
345 (None, no_track_duplicates()?)
346 } else {
347 let tracker = InstallTracker::load(config, root)?;
348 let (_freshness, duplicates) =
349 tracker.check_upgrade(&dst, pkg, force, opts, target, &rustc.verbose_version)?;
350 (Some(tracker), duplicates)
351 };
352
353 paths::create_dir_all(&dst)?;
354
355 let staging_dir = TempFileBuilder::new()
359 .prefix("cargo-install")
360 .tempdir_in(&dst)?;
361 for &(bin, src) in binaries.iter() {
362 let dst = staging_dir.path().join(bin);
363 if !source_id.is_path() && fs::rename(src, &dst).is_ok() {
365 continue;
366 }
367 fs::copy(src, &dst).chain_err(|| {
368 format_err!("failed to copy `{}` to `{}`", src.display(), dst.display())
369 })?;
370 }
371
372 let (to_replace, to_install): (Vec<&str>, Vec<&str>) = binaries
373 .iter()
374 .map(|&(bin, _)| bin)
375 .partition(|&bin| duplicates.contains_key(bin));
376
377 let mut installed = Transaction { bins: Vec::new() };
378 let mut successful_bins = BTreeSet::new();
379
380 for bin in to_install.iter() {
382 let src = staging_dir.path().join(bin);
383 let dst = dst.join(bin);
384 config.shell().status("Installing", dst.display())?;
385 fs::rename(&src, &dst).chain_err(|| {
386 format_err!("failed to move `{}` to `{}`", src.display(), dst.display())
387 })?;
388 installed.bins.push(dst);
389 successful_bins.insert(bin.to_string());
390 }
391
392 let replace_result = {
395 let mut try_install = || -> CargoResult<()> {
396 for &bin in to_replace.iter() {
397 let src = staging_dir.path().join(bin);
398 let dst = dst.join(bin);
399 config.shell().status("Replacing", dst.display())?;
400 fs::rename(&src, &dst).chain_err(|| {
401 format_err!("failed to move `{}` to `{}`", src.display(), dst.display())
402 })?;
403 successful_bins.insert(bin.to_string());
404 }
405 Ok(())
406 };
407 try_install()
408 };
409
410 if let Some(mut tracker) = tracker {
411 tracker.mark_installed(
412 pkg,
413 &successful_bins,
414 vers.map(|s| s.to_string()),
415 opts,
416 target,
417 &rustc.verbose_version,
418 );
419
420 if let Err(e) = remove_orphaned_bins(&ws, &mut tracker, &duplicates, pkg, &dst) {
421 config
423 .shell()
424 .warn(format!("failed to remove orphan: {:?}", e))?;
425 }
426
427 match tracker.save() {
428 Err(err) => replace_result.chain_err(|| err)?,
429 Ok(_) => replace_result?,
430 }
431 }
432
433 installed.success();
435 if needs_cleanup {
436 let target_dir = ws.target_dir().into_path_unlocked();
439 paths::remove_dir_all(&target_dir)?;
440 }
441
442 fn executables<T: AsRef<str>>(mut names: impl Iterator<Item = T> + Clone) -> String {
444 if names.clone().count() == 1 {
445 format!("(executable `{}`)", names.next().unwrap().as_ref())
446 } else {
447 format!(
448 "(executables {})",
449 names
450 .map(|b| format!("`{}`", b.as_ref()))
451 .collect::<Vec<_>>()
452 .join(", ")
453 )
454 }
455 }
456
457 if duplicates.is_empty() {
458 config.shell().status(
459 "Installed",
460 format!("package `{}` {}", pkg, executables(successful_bins.iter())),
461 )?;
462 Ok(())
463 } else {
464 if !to_install.is_empty() {
465 config.shell().status(
466 "Installed",
467 format!("package `{}` {}", pkg, executables(to_install.iter())),
468 )?;
469 }
470 let mut pkg_map = BTreeMap::new();
472 for (bin_name, opt_pkg_id) in &duplicates {
473 let key = opt_pkg_id.map_or_else(|| "unknown".to_string(), |pkg_id| pkg_id.to_string());
474 pkg_map.entry(key).or_insert_with(Vec::new).push(bin_name);
475 }
476 for (pkg_descr, bin_names) in &pkg_map {
477 config.shell().status(
478 "Replaced",
479 format!(
480 "package `{}` with `{}` {}",
481 pkg_descr,
482 pkg,
483 executables(bin_names.iter())
484 ),
485 )?;
486 }
487 Ok(())
488 }
489}
490
491fn check_yanked_install(ws: &Workspace<'_>) -> CargoResult<()> {
492 if ws.ignore_lock() || !ws.root().join("Cargo.lock").exists() {
493 return Ok(());
494 }
495 let (pkg_set, resolve) = ops::resolve_ws(ws)?;
499 let mut sources = pkg_set.sources_mut();
500
501 let _lock = ws.config().acquire_package_cache_lock()?;
504
505 for pkg_id in resolve.iter() {
506 if let Some(source) = sources.get_mut(pkg_id.source_id()) {
507 if source.is_yanked(pkg_id)? {
508 ws.config().shell().warn(format!(
509 "package `{}` in Cargo.lock is yanked in registry `{}`, \
510 consider running without --locked",
511 pkg_id,
512 pkg_id.source_id().display_registry_name()
513 ))?;
514 }
515 }
516 }
517
518 Ok(())
519}
520
521pub fn install_list(dst: Option<&str>, config: &Config) -> CargoResult<()> {
523 let root = resolve_root(dst, config)?;
524 let tracker = InstallTracker::load(config, &root)?;
525 for (k, v) in tracker.all_installed_bins() {
526 println!("{}:", k);
527 for bin in v {
528 println!(" {}", bin);
529 }
530 }
531 Ok(())
532}
533
534fn remove_orphaned_bins(
537 ws: &Workspace<'_>,
538 tracker: &mut InstallTracker,
539 duplicates: &BTreeMap<String, Option<PackageId>>,
540 pkg: &Package,
541 dst: &Path,
542) -> CargoResult<()> {
543 let filter = ops::CompileFilter::new_all_targets();
544 let all_self_names = exe_names(pkg, &filter);
545 let mut to_remove: HashMap<PackageId, BTreeSet<String>> = HashMap::new();
546 for other_pkg in duplicates.values() {
548 if let Some(other_pkg) = other_pkg {
550 if other_pkg.name() == pkg.name() {
551 if let Some(installed) = tracker.installed_bins(*other_pkg) {
553 for installed_name in installed {
556 if !all_self_names.contains(installed_name.as_str()) {
557 to_remove
558 .entry(*other_pkg)
559 .or_default()
560 .insert(installed_name.clone());
561 }
562 }
563 }
564 }
565 }
566 }
567
568 for (old_pkg, bins) in to_remove {
569 tracker.remove(old_pkg, &bins);
570 for bin in bins {
571 let full_path = dst.join(bin);
572 if full_path.exists() {
573 ws.config().shell().status(
574 "Removing",
575 format!(
576 "executable `{}` from previous version {}",
577 full_path.display(),
578 old_pkg
579 ),
580 )?;
581 paths::remove_file(&full_path)
582 .chain_err(|| format!("failed to remove {:?}", full_path))?;
583 }
584 }
585 }
586 Ok(())
587}