cargo/ops/
cargo_install.rs

1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4use std::{env, fs};
5
6use crate::core::compiler::{CompileKind, DefaultExecutor, Executor, UnitOutput};
7use crate::core::{Dependency, Edition, Package, PackageId, SourceId, Target, Workspace};
8use crate::ops::{CompileFilter, Packages};
9use crate::ops::{FilterRule, common_for_install_and_uninstall::*};
10use crate::sources::source::Source;
11use crate::sources::{GitSource, PathSource, SourceConfigMap};
12use crate::util::context::FeatureUnification;
13use crate::util::errors::CargoResult;
14use crate::util::{Filesystem, GlobalContext, Rustc};
15use crate::{drop_println, ops};
16
17use anyhow::{Context as _, bail};
18use cargo_util::paths;
19use cargo_util_schemas::core::PartialVersion;
20use itertools::Itertools;
21use semver::VersionReq;
22use tempfile::Builder as TempFileBuilder;
23
24struct Transaction {
25    bins: Vec<PathBuf>,
26}
27
28impl Transaction {
29    fn success(mut self) {
30        self.bins.clear();
31    }
32}
33
34impl Drop for Transaction {
35    fn drop(&mut self) {
36        for bin in self.bins.iter() {
37            let _ = paths::remove_file(bin);
38        }
39    }
40}
41
42struct InstallablePackage<'gctx> {
43    gctx: &'gctx GlobalContext,
44    opts: ops::CompileOptions,
45    root: Filesystem,
46    source_id: SourceId,
47    vers: Option<VersionReq>,
48    force: bool,
49    no_track: bool,
50    pkg: Package,
51    ws: Workspace<'gctx>,
52    rustc: Rustc,
53    target: String,
54}
55
56impl<'gctx> InstallablePackage<'gctx> {
57    // Returns pkg to install. None if pkg is already installed
58    pub fn new(
59        gctx: &'gctx GlobalContext,
60        root: Filesystem,
61        map: SourceConfigMap<'_>,
62        krate: Option<&str>,
63        source_id: SourceId,
64        from_cwd: bool,
65        vers: Option<&VersionReq>,
66        original_opts: &ops::CompileOptions,
67        force: bool,
68        no_track: bool,
69        needs_update_if_source_is_index: bool,
70        current_rust_version: Option<&PartialVersion>,
71        lockfile_path: Option<&Path>,
72    ) -> CargoResult<Option<Self>> {
73        if let Some(name) = krate {
74            if name == "." {
75                bail!(
76                    "To install the binaries for the package in current working \
77                     directory use `cargo install --path .`. \n\
78                     Use `cargo build` if you want to simply build the package."
79                )
80            }
81        }
82
83        let dst = root.join("bin").into_path_unlocked();
84        let pkg = {
85            let dep = {
86                if let Some(krate) = krate {
87                    let vers = if let Some(vers) = vers {
88                        Some(vers.to_string())
89                    } else if source_id.is_registry() {
90                        // Avoid pre-release versions from crate.io
91                        // unless explicitly asked for
92                        Some(String::from("*"))
93                    } else {
94                        None
95                    };
96                    Some(Dependency::parse(krate, vers.as_deref(), source_id)?)
97                } else {
98                    None
99                }
100            };
101
102            if source_id.is_git() {
103                let mut source = GitSource::new(source_id, gctx)?;
104                select_pkg(
105                    &mut source,
106                    dep,
107                    |git: &mut GitSource<'_>| git.read_packages(),
108                    gctx,
109                    current_rust_version,
110                )?
111            } else if source_id.is_path() {
112                let mut src = path_source(source_id, gctx)?;
113                if !src.path().is_dir() {
114                    bail!(
115                        "`{}` is not a directory. \
116                     --path must point to a directory containing a Cargo.toml file.",
117                        src.path().display()
118                    )
119                }
120                if !src.path().join("Cargo.toml").exists() {
121                    if from_cwd {
122                        bail!(
123                            "`{}` is not a crate root; specify a crate to \
124                         install from crates.io, or use --path or --git to \
125                         specify an alternate source",
126                            src.path().display()
127                        );
128                    } else if src.path().join("cargo.toml").exists() {
129                        bail!(
130                            "`{}` does not contain a Cargo.toml file, but found cargo.toml please try to rename it to Cargo.toml. \
131                     --path must point to a directory containing a Cargo.toml file.",
132                            src.path().display()
133                        )
134                    } else {
135                        bail!(
136                            "`{}` does not contain a Cargo.toml file. \
137                     --path must point to a directory containing a Cargo.toml file.",
138                            src.path().display()
139                        )
140                    }
141                }
142                select_pkg(
143                    &mut src,
144                    dep,
145                    |path: &mut PathSource<'_>| path.root_package().map(|p| vec![p]),
146                    gctx,
147                    current_rust_version,
148                )?
149            } else if let Some(dep) = dep {
150                let mut source = map.load(source_id, &HashSet::new())?;
151                if let Ok(Some(pkg)) = installed_exact_package(
152                    dep.clone(),
153                    &mut source,
154                    gctx,
155                    original_opts,
156                    &root,
157                    &dst,
158                    force,
159                    lockfile_path,
160                ) {
161                    let msg = format!(
162                        "package `{}` is already installed, use --force to override",
163                        pkg
164                    );
165                    gctx.shell().status("Ignored", &msg)?;
166                    return Ok(None);
167                }
168                select_dep_pkg(
169                    &mut source,
170                    dep,
171                    gctx,
172                    needs_update_if_source_is_index,
173                    current_rust_version,
174                )?
175            } else {
176                bail!(
177                    "must specify a crate to install from \
178                         crates.io, or use --path or --git to \
179                         specify alternate source"
180                )
181            }
182        };
183
184        let (ws, rustc, target) = make_ws_rustc_target(
185            gctx,
186            &original_opts,
187            &source_id,
188            pkg.clone(),
189            lockfile_path.clone(),
190        )?;
191
192        if !gctx.lock_update_allowed() {
193            // When --lockfile-path is set, check that passed lock file exists
194            // (unlike the usual flag behavior, lockfile won't be created as we imply --locked)
195            if let Some(requested_lockfile_path) = ws.requested_lockfile_path() {
196                if !requested_lockfile_path.is_file() {
197                    bail!(
198                        "no Cargo.lock file found in the requested path {}",
199                        requested_lockfile_path.display()
200                    );
201                }
202            // If we're installing in --locked mode and there's no `Cargo.lock` published
203            // ie. the bin was published before https://github.com/rust-lang/cargo/pull/7026
204            } else if !ws.root().join("Cargo.lock").exists() {
205                gctx.shell()
206                    .warn(format!("no Cargo.lock file published in {}", pkg))?;
207            }
208        }
209        let pkg = if source_id.is_git() {
210            // Don't use ws.current() in order to keep the package source as a git source so that
211            // install tracking uses the correct source.
212            pkg
213        } else {
214            ws.current()?.clone()
215        };
216
217        // When we build this package, we want to build the *specified* package only,
218        // and avoid building e.g. workspace default-members instead. Do so by constructing
219        // specialized compile options specific to the identified package.
220        // See test `path_install_workspace_root_despite_default_members`.
221        let mut opts = original_opts.clone();
222        // For cargo install tracking, we retain the source git url in `pkg`, but for the build spec
223        // we need to unconditionally use `ws.current()` to correctly address the path where we
224        // locally cloned that repo.
225        let pkgidspec = ws.current()?.package_id().to_spec();
226        opts.spec = Packages::Packages(vec![pkgidspec.to_string()]);
227
228        if from_cwd {
229            if pkg.manifest().edition() == Edition::Edition2015 {
230                gctx.shell().warn(
231                    "Using `cargo install` to install the binaries from the \
232                     package in current working directory is deprecated, \
233                     use `cargo install --path .` instead. \
234                     Use `cargo build` if you want to simply build the package.",
235                )?
236            } else {
237                bail!(
238                    "Using `cargo install` to install the binaries from the \
239                     package in current working directory is no longer supported, \
240                     use `cargo install --path .` instead. \
241                     Use `cargo build` if you want to simply build the package."
242                )
243            }
244        };
245
246        // For bare `cargo install` (no `--bin` or `--example`), check if there is
247        // *something* to install. Explicit `--bin` or `--example` flags will be
248        // checked at the start of `compile_ws`.
249        if !opts.filter.is_specific() && !pkg.targets().iter().any(|t| t.is_bin()) {
250            bail!(
251                "there is nothing to install in `{}`, because it has no binaries\n\
252                 `cargo install` is only for installing programs, and can't be used with libraries.\n\
253                 To use a library crate, add it as a dependency to a Cargo project with `cargo add`.",
254                pkg,
255            );
256        }
257
258        let ip = InstallablePackage {
259            gctx,
260            opts,
261            root,
262            source_id,
263            vers: vers.cloned(),
264            force,
265            no_track,
266            pkg,
267            ws,
268            rustc,
269            target,
270        };
271
272        // WARNING: no_track does not perform locking, so there is no protection
273        // of concurrent installs.
274        if no_track {
275            // Check for conflicts.
276            ip.no_track_duplicates(&dst)?;
277        } else if is_installed(
278            &ip.pkg, gctx, &ip.opts, &ip.rustc, &ip.target, &ip.root, &dst, force,
279        )? {
280            let msg = format!(
281                "package `{}` is already installed, use --force to override",
282                ip.pkg
283            );
284            gctx.shell().status("Ignored", &msg)?;
285            return Ok(None);
286        }
287
288        Ok(Some(ip))
289    }
290
291    fn no_track_duplicates(&self, dst: &Path) -> CargoResult<BTreeMap<String, Option<PackageId>>> {
292        // Helper for --no-track flag to make sure it doesn't overwrite anything.
293        let duplicates: BTreeMap<String, Option<PackageId>> =
294            exe_names(&self.pkg, &self.opts.filter)
295                .into_iter()
296                .filter(|name| dst.join(name).exists())
297                .map(|name| (name, None))
298                .collect();
299        if !self.force && !duplicates.is_empty() {
300            let mut msg: Vec<String> = duplicates
301                .iter()
302                .map(|(name, _)| {
303                    format!(
304                        "binary `{}` already exists in destination `{}`",
305                        name,
306                        dst.join(name).to_string_lossy()
307                    )
308                })
309                .collect();
310            msg.push("Add --force to overwrite".to_string());
311            bail!("{}", msg.join("\n"));
312        }
313        Ok(duplicates)
314    }
315
316    fn install_one(mut self, dry_run: bool) -> CargoResult<bool> {
317        self.gctx.shell().status("Installing", &self.pkg)?;
318
319        // Normalize to absolute path for consistency throughout.
320        // See: https://github.com/rust-lang/cargo/issues/16023
321        let dst = self.root.join("bin").into_path_unlocked();
322        let cwd = self.gctx.cwd();
323        let dst = if dst.is_absolute() {
324            paths::normalize_path(dst.as_path())
325        } else {
326            paths::normalize_path(&cwd.join(&dst))
327        };
328
329        let mut td_opt = None;
330        let mut needs_cleanup = false;
331        if !self.source_id.is_path() {
332            let target_dir = if let Some(dir) = self.gctx.target_dir()? {
333                dir
334            } else if let Ok(td) = TempFileBuilder::new().prefix("cargo-install").tempdir() {
335                let p = td.path().to_owned();
336                td_opt = Some(td);
337                Filesystem::new(p)
338            } else {
339                needs_cleanup = true;
340                Filesystem::new(self.gctx.cwd().join("target-install"))
341            };
342            self.ws.set_target_dir(target_dir);
343        }
344
345        self.check_yanked_install()?;
346
347        let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
348        self.opts.build_config.dry_run = dry_run;
349        let compile = ops::compile_ws(&self.ws, &self.opts, &exec).with_context(|| {
350            if let Some(td) = td_opt.take() {
351                // preserve the temporary directory, so the user can inspect it
352                drop(td.keep());
353            }
354
355            format!(
356                "failed to compile `{}`, intermediate artifacts can be \
357                 found at `{}`.\nTo reuse those artifacts with a future \
358                 compilation, set the environment variable \
359                 `CARGO_TARGET_DIR` to that path.",
360                self.pkg,
361                self.ws.target_dir().display()
362            )
363        })?;
364        let mut binaries: Vec<(&str, &Path)> = compile
365            .binaries
366            .iter()
367            .map(|UnitOutput { path, .. }| {
368                let name = path.file_name().unwrap();
369                if let Some(s) = name.to_str() {
370                    Ok((s, path.as_ref()))
371                } else {
372                    bail!("Binary `{:?}` name can't be serialized into string", name)
373                }
374            })
375            .collect::<CargoResult<_>>()?;
376        if binaries.is_empty() {
377            // Cargo already warns the user if they use a target specifier that matches nothing,
378            // but we want to error if the user asked for a _particular_ binary to be installed,
379            // and we didn't end up installing it.
380            //
381            // NOTE: This _should_ be impossible to hit since --bin=does_not_exist will fail on
382            // target selection, and --bin=requires_a without --features=a will fail with "target
383            // .. requires the features ..". But rather than assume that's the case, we define the
384            // behavior for this fallback case as well.
385            if let CompileFilter::Only { bins, examples, .. } = &self.opts.filter {
386                let mut any_specific = false;
387                if let FilterRule::Just(v) = bins {
388                    if !v.is_empty() {
389                        any_specific = true;
390                    }
391                }
392                if let FilterRule::Just(v) = examples {
393                    if !v.is_empty() {
394                        any_specific = true;
395                    }
396                }
397                if any_specific {
398                    bail!("no binaries are available for install using the selected features");
399                }
400            }
401
402            // If there _are_ binaries available, but none were selected given the current set of
403            // features, let the user know.
404            //
405            // Note that we know at this point that _if_ bins or examples is set to `::Just`,
406            // they're `::Just([])`, which is `FilterRule::none()`.
407            let binaries: Vec<_> = self
408                .pkg
409                .targets()
410                .iter()
411                .filter(|t| t.is_executable())
412                .collect();
413            if !binaries.is_empty() {
414                self.gctx
415                    .shell()
416                    .warn(make_warning_about_missing_features(&binaries))?;
417            }
418
419            return Ok(false);
420        }
421        // This is primarily to make testing easier.
422        binaries.sort_unstable();
423
424        let (tracker, duplicates) = if self.no_track {
425            (None, self.no_track_duplicates(&dst)?)
426        } else {
427            let tracker = InstallTracker::load(self.gctx, &self.root)?;
428            let (_freshness, duplicates) = tracker.check_upgrade(
429                &dst,
430                &self.pkg,
431                self.force,
432                &self.opts,
433                &self.target,
434                &self.rustc.verbose_version,
435            )?;
436            (Some(tracker), duplicates)
437        };
438
439        paths::create_dir_all(&dst)?;
440
441        // Copy all binaries to a temporary directory under `dst` first, catching
442        // some failure modes (e.g., out of space) before touching the existing
443        // binaries. This directory will get cleaned up via RAII.
444        let staging_dir = TempFileBuilder::new()
445            .prefix("cargo-install")
446            .tempdir_in(&dst)?;
447        if !dry_run {
448            for &(bin, src) in binaries.iter() {
449                let dst = staging_dir.path().join(bin);
450                // Try to move if `target_dir` is transient.
451                if !self.source_id.is_path() && fs::rename(src, &dst).is_ok() {
452                    continue;
453                }
454                paths::copy(src, &dst)?;
455            }
456        }
457
458        let (to_replace, to_install): (Vec<&str>, Vec<&str>) = binaries
459            .iter()
460            .map(|&(bin, _)| bin)
461            .partition(|&bin| duplicates.contains_key(bin));
462
463        let mut installed = Transaction { bins: Vec::new() };
464        let mut successful_bins = BTreeSet::new();
465
466        // Move the temporary copies into `dst` starting with new binaries.
467        for bin in to_install.iter() {
468            let src = staging_dir.path().join(bin);
469            let dst = dst.join(bin);
470            self.gctx.shell().status("Installing", dst.display())?;
471            if !dry_run {
472                fs::rename(&src, &dst).with_context(|| {
473                    format!("failed to move `{}` to `{}`", src.display(), dst.display())
474                })?;
475                installed.bins.push(dst);
476                successful_bins.insert(bin.to_string());
477            }
478        }
479
480        // Repeat for binaries which replace existing ones but don't pop the error
481        // up until after updating metadata.
482        let replace_result = {
483            let mut try_install = || -> CargoResult<()> {
484                for &bin in to_replace.iter() {
485                    let src = staging_dir.path().join(bin);
486                    let dst = dst.join(bin);
487                    self.gctx.shell().status("Replacing", dst.display())?;
488                    if !dry_run {
489                        fs::rename(&src, &dst).with_context(|| {
490                            format!("failed to move `{}` to `{}`", src.display(), dst.display())
491                        })?;
492                        successful_bins.insert(bin.to_string());
493                    }
494                }
495                Ok(())
496            };
497            try_install()
498        };
499
500        if let Some(mut tracker) = tracker {
501            tracker.mark_installed(
502                &self.pkg,
503                &successful_bins,
504                self.vers.map(|s| s.to_string()),
505                &self.opts,
506                &self.target,
507                &self.rustc.verbose_version,
508            );
509
510            if let Err(e) = remove_orphaned_bins(
511                &self.ws,
512                &mut tracker,
513                &duplicates,
514                &self.pkg,
515                &dst,
516                dry_run,
517            ) {
518                // Don't hard error on remove.
519                self.gctx
520                    .shell()
521                    .warn(format!("failed to remove orphan: {:?}", e))?;
522            }
523
524            match tracker.save() {
525                Err(err) => replace_result.with_context(|| err)?,
526                Ok(_) => replace_result?,
527            }
528        }
529
530        // Reaching here means all actions have succeeded. Clean up.
531        installed.success();
532        if needs_cleanup {
533            // Don't bother grabbing a lock as we're going to blow it all away
534            // anyway.
535            let target_dir = self.ws.target_dir().into_path_unlocked();
536            paths::remove_dir_all(&target_dir)?;
537        }
538
539        // Helper for creating status messages.
540        fn executables<T: AsRef<str>>(mut names: impl Iterator<Item = T> + Clone) -> String {
541            if names.clone().count() == 1 {
542                format!("(executable `{}`)", names.next().unwrap().as_ref())
543            } else {
544                format!(
545                    "(executables {})",
546                    names
547                        .map(|b| format!("`{}`", b.as_ref()))
548                        .collect::<Vec<_>>()
549                        .join(", ")
550                )
551            }
552        }
553
554        if dry_run {
555            self.gctx.shell().warn("aborting install due to dry run")?;
556            Ok(true)
557        } else if duplicates.is_empty() {
558            self.gctx.shell().status(
559                "Installed",
560                format!(
561                    "package `{}` {}",
562                    self.pkg,
563                    executables(successful_bins.iter())
564                ),
565            )?;
566            Ok(true)
567        } else {
568            if !to_install.is_empty() {
569                self.gctx.shell().status(
570                    "Installed",
571                    format!("package `{}` {}", self.pkg, executables(to_install.iter())),
572                )?;
573            }
574            // Invert the duplicate map.
575            let mut pkg_map = BTreeMap::new();
576            for (bin_name, opt_pkg_id) in &duplicates {
577                let key =
578                    opt_pkg_id.map_or_else(|| "unknown".to_string(), |pkg_id| pkg_id.to_string());
579                pkg_map.entry(key).or_insert_with(Vec::new).push(bin_name);
580            }
581            for (pkg_descr, bin_names) in &pkg_map {
582                self.gctx.shell().status(
583                    "Replaced",
584                    format!(
585                        "package `{}` with `{}` {}",
586                        pkg_descr,
587                        self.pkg,
588                        executables(bin_names.iter())
589                    ),
590                )?;
591            }
592            Ok(true)
593        }
594    }
595
596    fn check_yanked_install(&self) -> CargoResult<()> {
597        if self.ws.ignore_lock() || !self.ws.root().join("Cargo.lock").exists() {
598            return Ok(());
599        }
600        // It would be best if `source` could be passed in here to avoid a
601        // duplicate "Updating", but since `source` is taken by value, then it
602        // wouldn't be available for `compile_ws`.
603        let dry_run = false;
604        let (pkg_set, resolve) = ops::resolve_ws(&self.ws, dry_run)?;
605        ops::check_yanked(
606            self.ws.gctx(),
607            &pkg_set,
608            &resolve,
609            "consider running without --locked",
610        )
611    }
612}
613
614fn make_warning_about_missing_features(binaries: &[&Target]) -> String {
615    let max_targets_listed = 7;
616    let target_features_message = binaries
617        .iter()
618        .take(max_targets_listed)
619        .map(|b| {
620            let name = b.description_named();
621            let features = b
622                .required_features()
623                .unwrap_or(&Vec::new())
624                .iter()
625                .map(|f| format!("`{f}`"))
626                .join(", ");
627            format!("  {name} requires the features: {features}")
628        })
629        .join("\n");
630
631    let additional_bins_message = if binaries.len() > max_targets_listed {
632        format!(
633            "\n{} more targets also requires features not enabled. See them in the Cargo.toml file.",
634            binaries.len() - max_targets_listed
635        )
636    } else {
637        "".into()
638    };
639
640    let example_features = binaries[0]
641        .required_features()
642        .map(|f| f.join(" "))
643        .unwrap_or_default();
644
645    format!(
646        "\
647none of the package's binaries are available for install using the selected features
648{target_features_message}{additional_bins_message}
649Consider enabling some of the needed features by passing, e.g., `--features=\"{example_features}\"`"
650    )
651}
652
653pub fn install(
654    gctx: &GlobalContext,
655    root: Option<&str>,
656    krates: Vec<(String, Option<VersionReq>)>,
657    source_id: SourceId,
658    from_cwd: bool,
659    opts: &ops::CompileOptions,
660    force: bool,
661    no_track: bool,
662    dry_run: bool,
663    lockfile_path: Option<&Path>,
664) -> CargoResult<()> {
665    let root = resolve_root(root, gctx)?;
666    // Normalize to absolute path for consistency throughout.
667    // See: https://github.com/rust-lang/cargo/issues/16023
668    let dst = root.join("bin").into_path_unlocked();
669    let cwd = gctx.cwd();
670    let dst = if dst.is_absolute() {
671        paths::normalize_path(dst.as_path())
672    } else {
673        paths::normalize_path(&cwd.join(&dst))
674    };
675    let map = SourceConfigMap::new(gctx)?;
676
677    let current_rust_version = if opts.honor_rust_version.unwrap_or(true) {
678        let rustc = gctx.load_global_rustc(None)?;
679        Some(rustc.version.clone().into())
680    } else {
681        None
682    };
683
684    let (installed_anything, scheduled_error) = if krates.len() <= 1 {
685        let (krate, vers) = krates
686            .iter()
687            .next()
688            .map(|(k, v)| (Some(k.as_str()), v.as_ref()))
689            .unwrap_or((None, None));
690        let installable_pkg = InstallablePackage::new(
691            gctx,
692            root,
693            map,
694            krate,
695            source_id,
696            from_cwd,
697            vers,
698            opts,
699            force,
700            no_track,
701            true,
702            current_rust_version.as_ref(),
703            lockfile_path,
704        )?;
705        let mut installed_anything = true;
706        if let Some(installable_pkg) = installable_pkg {
707            installed_anything = installable_pkg.install_one(dry_run)?;
708        }
709        (installed_anything, false)
710    } else {
711        let mut succeeded = vec![];
712        let mut failed = vec![];
713        // "Tracks whether or not the source (such as a registry or git repo) has been updated.
714        // This is used to avoid updating it multiple times when installing multiple crates.
715        let mut did_update = false;
716
717        let pkgs_to_install: Vec<_> = krates
718            .iter()
719            .filter_map(|(krate, vers)| {
720                let root = root.clone();
721                let map = map.clone();
722                match InstallablePackage::new(
723                    gctx,
724                    root,
725                    map,
726                    Some(krate.as_str()),
727                    source_id,
728                    from_cwd,
729                    vers.as_ref(),
730                    opts,
731                    force,
732                    no_track,
733                    !did_update,
734                    current_rust_version.as_ref(),
735                    lockfile_path,
736                ) {
737                    Ok(Some(installable_pkg)) => {
738                        did_update = true;
739                        Some((krate, installable_pkg))
740                    }
741                    Ok(None) => {
742                        // Already installed
743                        succeeded.push(krate.as_str());
744                        None
745                    }
746                    Err(e) => {
747                        crate::display_error(&e, &mut gctx.shell());
748                        failed.push(krate.as_str());
749                        // We assume an update was performed if we got an error.
750                        did_update = true;
751                        None
752                    }
753                }
754            })
755            .collect();
756
757        let install_results: Vec<_> = pkgs_to_install
758            .into_iter()
759            .map(|(krate, installable_pkg)| (krate, installable_pkg.install_one(dry_run)))
760            .collect();
761
762        for (krate, result) in install_results {
763            match result {
764                Ok(installed) => {
765                    if installed {
766                        succeeded.push(krate);
767                    }
768                }
769                Err(e) => {
770                    crate::display_error(&e, &mut gctx.shell());
771                    failed.push(krate);
772                }
773            }
774        }
775
776        let mut summary = vec![];
777        if !succeeded.is_empty() {
778            summary.push(format!("Successfully installed {}!", succeeded.join(", ")));
779        }
780        if !failed.is_empty() {
781            summary.push(format!(
782                "Failed to install {} (see error(s) above).",
783                failed.join(", ")
784            ));
785        }
786        if !succeeded.is_empty() || !failed.is_empty() {
787            gctx.shell().status("Summary", summary.join(" "))?;
788        }
789
790        (!succeeded.is_empty(), !failed.is_empty())
791    };
792
793    if installed_anything {
794        // Print a warning that if this directory isn't in PATH that they won't be
795        // able to run these commands.
796        let path = gctx.get_env_os("PATH").unwrap_or_default();
797        let dst_in_path = env::split_paths(&path).any(|path| path == dst);
798
799        if !dst_in_path {
800            gctx.shell().warn(&format!(
801                "be sure to add `{}` to your PATH to be \
802             able to run the installed binaries",
803                dst.display()
804            ))?;
805        }
806    }
807
808    if scheduled_error {
809        bail!("some crates failed to install");
810    }
811
812    Ok(())
813}
814
815fn is_installed(
816    pkg: &Package,
817    gctx: &GlobalContext,
818    opts: &ops::CompileOptions,
819    rustc: &Rustc,
820    target: &str,
821    root: &Filesystem,
822    dst: &Path,
823    force: bool,
824) -> CargoResult<bool> {
825    let tracker = InstallTracker::load(gctx, root)?;
826    let (freshness, _duplicates) =
827        tracker.check_upgrade(dst, pkg, force, opts, target, &rustc.verbose_version)?;
828    Ok(freshness.is_fresh())
829}
830
831/// Checks if vers can only be satisfied by exactly one version of a package in a registry, and it's
832/// already installed. If this is the case, we can skip interacting with a registry to check if
833/// newer versions may be installable, as no newer version can exist.
834fn installed_exact_package<T>(
835    dep: Dependency,
836    source: &mut T,
837    gctx: &GlobalContext,
838    opts: &ops::CompileOptions,
839    root: &Filesystem,
840    dst: &Path,
841    force: bool,
842    lockfile_path: Option<&Path>,
843) -> CargoResult<Option<Package>>
844where
845    T: Source,
846{
847    if !dep.version_req().is_exact() {
848        // If the version isn't exact, we may need to update the registry and look for a newer
849        // version - we can't know if the package is installed without doing so.
850        return Ok(None);
851    }
852    // Try getting the package from the registry  without updating it, to avoid a potentially
853    // expensive network call in the case that the package is already installed.
854    // If this fails, the caller will possibly do an index update and try again, this is just a
855    // best-effort check to see if we can avoid hitting the network.
856    if let Ok(pkg) = select_dep_pkg(source, dep, gctx, false, None) {
857        let (_ws, rustc, target) =
858            make_ws_rustc_target(gctx, opts, &source.source_id(), pkg.clone(), lockfile_path)?;
859        if let Ok(true) = is_installed(&pkg, gctx, opts, &rustc, &target, root, dst, force) {
860            return Ok(Some(pkg));
861        }
862    }
863    Ok(None)
864}
865
866fn make_ws_rustc_target<'gctx>(
867    gctx: &'gctx GlobalContext,
868    opts: &ops::CompileOptions,
869    source_id: &SourceId,
870    pkg: Package,
871    lockfile_path: Option<&Path>,
872) -> CargoResult<(Workspace<'gctx>, Rustc, String)> {
873    let mut ws = if source_id.is_git() || source_id.is_path() {
874        Workspace::new(pkg.manifest_path(), gctx)?
875    } else {
876        let mut ws = Workspace::ephemeral(pkg, gctx, None, false)?;
877        ws.set_resolve_honors_rust_version(Some(false));
878        ws
879    };
880    ws.set_resolve_feature_unification(FeatureUnification::Selected);
881    ws.set_ignore_lock(gctx.lock_update_allowed());
882    ws.set_requested_lockfile_path(lockfile_path.map(|p| p.to_path_buf()));
883    // if --lockfile-path is set, imply --locked
884    if ws.requested_lockfile_path().is_some() {
885        ws.set_ignore_lock(false);
886    }
887    ws.set_require_optional_deps(false);
888
889    let rustc = gctx.load_global_rustc(Some(&ws))?;
890    let target = match &opts.build_config.single_requested_kind()? {
891        CompileKind::Host => rustc.host.as_str().to_owned(),
892        CompileKind::Target(target) => target.short_name().to_owned(),
893    };
894
895    Ok((ws, rustc, target))
896}
897
898/// Display a list of installed binaries.
899pub fn install_list(dst: Option<&str>, gctx: &GlobalContext) -> CargoResult<()> {
900    let root = resolve_root(dst, gctx)?;
901    let tracker = InstallTracker::load(gctx, &root)?;
902    for (k, v) in tracker.all_installed_bins() {
903        drop_println!(gctx, "{}:", k);
904        for bin in v {
905            drop_println!(gctx, "    {}", bin);
906        }
907    }
908    Ok(())
909}
910
911/// Removes executables that are no longer part of a package that was
912/// previously installed.
913fn remove_orphaned_bins(
914    ws: &Workspace<'_>,
915    tracker: &mut InstallTracker,
916    duplicates: &BTreeMap<String, Option<PackageId>>,
917    pkg: &Package,
918    dst: &Path,
919    dry_run: bool,
920) -> CargoResult<()> {
921    let filter = ops::CompileFilter::new_all_targets();
922    let all_self_names = exe_names(pkg, &filter);
923    let mut to_remove: HashMap<PackageId, BTreeSet<String>> = HashMap::new();
924    // For each package that we stomped on.
925    for other_pkg in duplicates.values().flatten() {
926        // Only for packages with the same name.
927        if other_pkg.name() == pkg.name() {
928            // Check what the old package had installed.
929            if let Some(installed) = tracker.installed_bins(*other_pkg) {
930                // If the old install has any names that no longer exist,
931                // add them to the list to remove.
932                for installed_name in installed {
933                    if !all_self_names.contains(installed_name.as_str()) {
934                        to_remove
935                            .entry(*other_pkg)
936                            .or_default()
937                            .insert(installed_name.clone());
938                    }
939                }
940            }
941        }
942    }
943
944    for (old_pkg, bins) in to_remove {
945        tracker.remove(old_pkg, &bins);
946        for bin in bins {
947            let full_path = dst.join(bin);
948            if full_path.exists() {
949                ws.gctx().shell().status(
950                    "Removing",
951                    format!(
952                        "executable `{}` from previous version {}",
953                        full_path.display(),
954                        old_pkg
955                    ),
956                )?;
957                if !dry_run {
958                    paths::remove_file(&full_path)
959                        .with_context(|| format!("failed to remove {:?}", full_path))?;
960                }
961            }
962        }
963    }
964    Ok(())
965}