Skip to main content

cargo/ops/
cargo_install.rs

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