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