Skip to main content

cargo/ops/
cargo_install.rs

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