1 use std
::collections
::{BTreeMap, BTreeSet, HashMap, HashSet}
;
2 use std
::path
::{Path, PathBuf}
;
6 use crate::core
::compiler
::{CompileKind, DefaultExecutor, Executor, UnitOutput}
;
8 Dependency
, Edition
, Package
, PackageId
, PackageIdSpec
, Source
, SourceId
, Target
, Workspace
,
10 use crate::ops
::{common_for_install_and_uninstall::*, FilterRule}
;
11 use crate::ops
::{CompileFilter, Packages}
;
12 use crate::sources
::{GitSource, PathSource, SourceConfigMap}
;
13 use crate::util
::errors
::CargoResult
;
14 use crate::util
::{Config, Filesystem, Rustc, ToSemver, VersionReqExt}
;
15 use crate::{drop_println, ops}
;
17 use anyhow
::{bail, format_err, Context as _}
;
18 use cargo_util
::paths
;
19 use itertools
::Itertools
;
20 use semver
::VersionReq
;
21 use tempfile
::Builder
as TempFileBuilder
;
28 fn success(mut self) {
33 impl Drop
for Transaction
{
35 for bin
in self.bins
.iter() {
36 let _
= paths
::remove_file(bin
);
41 struct InstallablePackage
<'cfg
, 'a
> {
43 opts
: ops
::CompileOptions
,
46 vers
: Option
<&'a
str>,
56 impl<'cfg
, 'a
> InstallablePackage
<'cfg
, 'a
> {
57 // Returns pkg to install. None if pkg is already installed
61 map
: SourceConfigMap
<'_
>,
65 vers
: Option
<&'a
str>,
66 original_opts
: &'a ops
::CompileOptions
,
69 needs_update_if_source_is_index
: bool
,
70 ) -> CargoResult
<Option
<InstallablePackage
<'cfg
, 'a
>>> {
71 if let Some(name
) = krate
{
74 "To install the binaries for the package in current working \
75 directory use `cargo install --path .`. \n\
76 Use `cargo build` if you want to simply build the package."
81 let dst
= root
.join("bin").into_path_unlocked();
84 if let Some(krate
) = krate
{
85 let vers
= if let Some(vers_flag
) = vers
{
86 Some(parse_semver_flag(vers_flag
)?
.to_string())
87 } else if source_id
.is_registry() {
88 // Avoid pre-release versions from crate.io
89 // unless explicitly asked for
90 Some(String
::from("*"))
94 Some(Dependency
::parse(krate
, vers
.as_deref(), source_id
)?
)
100 if source_id
.is_git() {
101 let mut source
= GitSource
::new(source_id
, config
)?
;
105 |git
: &mut GitSource
<'_
>| git
.read_packages(),
108 } else if source_id
.is_path() {
109 let mut src
= path_source(source_id
, config
)?
;
110 if !src
.path().is_dir() {
112 "`{}` is not a directory. \
113 --path must point to a directory containing a Cargo.toml file.",
117 if !src
.path().join("Cargo.toml").exists() {
120 "`{}` is not a crate root; specify a crate to \
121 install from crates.io, or use --path or --git to \
122 specify an alternate source",
125 } else if src
.path().join("cargo.toml").exists() {
127 "`{}` does not contain a Cargo.toml file, but found cargo.toml please try to rename it to Cargo.toml. \
128 --path must point to a directory containing a Cargo.toml file.",
133 "`{}` does not contain a Cargo.toml file. \
134 --path must point to a directory containing a Cargo.toml file.",
142 |path
: &mut PathSource
<'_
>| path
.read_packages(),
145 } else if let Some(dep
) = dep
{
146 let mut source
= map
.load(source_id
, &HashSet
::new())?
;
147 if let Ok(Some(pkg
)) = installed_exact_package(
157 "package `{}` is already installed, use --force to override",
160 config
.shell().status("Ignored", &msg
)?
;
163 select_dep_pkg(&mut source
, dep
, config
, needs_update_if_source_is_index
)?
166 "must specify a crate to install from \
167 crates.io, or use --path or --git to \
168 specify alternate source"
173 let (ws
, rustc
, target
) =
174 make_ws_rustc_target(config
, &original_opts
, &source_id
, pkg
.clone())?
;
175 // If we're installing in --locked mode and there's no `Cargo.lock` published
176 // ie. the bin was published before https://github.com/rust-lang/cargo/pull/7026
177 if config
.locked() && !ws
.root().join("Cargo.lock").exists() {
178 config
.shell().warn(format
!(
179 "no Cargo.lock file published in {}",
183 let pkg
= if source_id
.is_git() {
184 // Don't use ws.current() in order to keep the package source as a git source so that
185 // install tracking uses the correct source.
188 ws
.current()?
.clone()
191 // When we build this package, we want to build the *specified* package only,
192 // and avoid building e.g. workspace default-members instead. Do so by constructing
193 // specialized compile options specific to the identified package.
194 // See test `path_install_workspace_root_despite_default_members`.
195 let mut opts
= original_opts
.clone();
196 // For cargo install tracking, we retain the source git url in `pkg`, but for the build spec
197 // we need to unconditionally use `ws.current()` to correctly address the path where we
198 // locally cloned that repo.
199 let pkgidspec
= PackageIdSpec
::from_package_id(ws
.current()?
.package_id());
200 opts
.spec
= Packages
::Packages(vec
![pkgidspec
.to_string()]);
203 if pkg
.manifest().edition() == Edition
::Edition2015
{
205 "Using `cargo install` to install the binaries for the \
206 package in current working directory is deprecated, \
207 use `cargo install --path .` instead. \
208 Use `cargo build` if you want to simply build the package.",
212 "Using `cargo install` to install the binaries for the \
213 package in current working directory is no longer supported, \
214 use `cargo install --path .` instead. \
215 Use `cargo build` if you want to simply build the package."
220 // For bare `cargo install` (no `--bin` or `--example`), check if there is
221 // *something* to install. Explicit `--bin` or `--example` flags will be
222 // checked at the start of `compile_ws`.
223 if !opts
.filter
.is_specific() && !pkg
.targets().iter().any(|t
| t
.is_bin()) {
225 "there is nothing to install in `{}`, because it has no binaries\n\
226 `cargo install` is only for installing programs, and can't be used with libraries.\n\
227 To use a library crate, add it as a dependency to a Cargo project with `cargo add`.",
232 let ip
= InstallablePackage
{
247 // WARNING: no_track does not perform locking, so there is no protection
248 // of concurrent installs.
250 // Check for conflicts.
251 ip
.no_track_duplicates(&dst
)?
;
252 } else if is_installed(
253 &ip
.pkg
, config
, &ip
.opts
, &ip
.rustc
, &ip
.target
, &ip
.root
, &dst
, force
,
256 "package `{}` is already installed, use --force to override",
259 config
.shell().status("Ignored", &msg
)?
;
266 fn no_track_duplicates(&self, dst
: &Path
) -> CargoResult
<BTreeMap
<String
, Option
<PackageId
>>> {
267 // Helper for --no-track flag to make sure it doesn't overwrite anything.
268 let duplicates
: BTreeMap
<String
, Option
<PackageId
>> =
269 exe_names(&self.pkg
, &self.opts
.filter
)
271 .filter(|name
| dst
.join(name
).exists())
272 .map(|name
| (name
, None
))
274 if !self.force
&& !duplicates
.is_empty() {
275 let mut msg
: Vec
<String
> = duplicates
279 "binary `{}` already exists in destination `{}`",
281 dst
.join(name
).to_string_lossy()
285 msg
.push("Add --force to overwrite".to_string());
286 bail
!("{}", msg
.join("\n"));
291 fn install_one(mut self) -> CargoResult
<bool
> {
292 self.config
.shell().status("Installing", &self.pkg
)?
;
294 let dst
= self.root
.join("bin").into_path_unlocked();
296 let mut td_opt
= None
;
297 let mut needs_cleanup
= false;
298 if !self.source_id
.is_path() {
299 let target_dir
= if let Some(dir
) = self.config
.target_dir()?
{
301 } else if let Ok(td
) = TempFileBuilder
::new().prefix("cargo-install").tempdir() {
302 let p
= td
.path().to_owned();
306 needs_cleanup
= true;
307 Filesystem
::new(self.config
.cwd().join("target-install"))
309 self.ws
.set_target_dir(target_dir
);
312 self.check_yanked_install()?
;
314 let exec
: Arc
<dyn Executor
> = Arc
::new(DefaultExecutor
);
315 let compile
= ops
::compile_ws(&self.ws
, &self.opts
, &exec
).with_context(|| {
316 if let Some(td
) = td_opt
.take() {
317 // preserve the temporary directory, so the user can inspect it
318 drop(td
.into_path());
322 "failed to compile `{}`, intermediate artifacts can be \
323 found at `{}`.\nTo reuse those artifacts with a future \
324 compilation, set the environment variable \
325 `CARGO_TARGET_DIR` to that path.",
327 self.ws
.target_dir().display()
330 let mut binaries
: Vec
<(&str, &Path
)> = compile
333 .map(|UnitOutput { path, .. }
| {
334 let name
= path
.file_name().unwrap();
335 if let Some(s
) = name
.to_str() {
336 Ok((s
, path
.as_ref()))
338 bail
!("Binary `{:?}` name can't be serialized into string", name
)
341 .collect
::<CargoResult
<_
>>()?
;
342 if binaries
.is_empty() {
343 // Cargo already warns the user if they use a target specifier that matches nothing,
344 // but we want to error if the user asked for a _particular_ binary to be installed,
345 // and we didn't end up installing it.
347 // NOTE: This _should_ be impossible to hit since --bin=does_not_exist will fail on
348 // target selection, and --bin=requires_a without --features=a will fail with "target
349 // .. requires the features ..". But rather than assume that's the case, we define the
350 // behavior for this fallback case as well.
351 if let CompileFilter
::Only { bins, examples, .. }
= &self.opts
.filter
{
352 let mut any_specific
= false;
353 if let FilterRule
::Just(ref v
) = bins
{
358 if let FilterRule
::Just(ref v
) = examples
{
364 bail
!("no binaries are available for install using the selected features");
368 // If there _are_ binaries available, but none were selected given the current set of
369 // features, let the user know.
371 // Note that we know at this point that _if_ bins or examples is set to `::Just`,
372 // they're `::Just([])`, which is `FilterRule::none()`.
373 let binaries
: Vec
<_
> = self
377 .filter(|t
| t
.is_executable())
379 if !binaries
.is_empty() {
382 .warn(make_warning_about_missing_features(&binaries
))?
;
387 // This is primarily to make testing easier.
388 binaries
.sort_unstable();
390 let (tracker
, duplicates
) = if self.no_track
{
391 (None
, self.no_track_duplicates(&dst
)?
)
393 let tracker
= InstallTracker
::load(self.config
, &self.root
)?
;
394 let (_freshness
, duplicates
) = tracker
.check_upgrade(
400 &self.rustc
.verbose_version
,
402 (Some(tracker
), duplicates
)
405 paths
::create_dir_all(&dst
)?
;
407 // Copy all binaries to a temporary directory under `dst` first, catching
408 // some failure modes (e.g., out of space) before touching the existing
409 // binaries. This directory will get cleaned up via RAII.
410 let staging_dir
= TempFileBuilder
::new()
411 .prefix("cargo-install")
413 for &(bin
, src
) in binaries
.iter() {
414 let dst
= staging_dir
.path().join(bin
);
415 // Try to move if `target_dir` is transient.
416 if !self.source_id
.is_path() && fs
::rename(src
, &dst
).is_ok() {
419 paths
::copy(src
, &dst
)?
;
422 let (to_replace
, to_install
): (Vec
<&str>, Vec
<&str>) = binaries
424 .map(|&(bin
, _
)| bin
)
425 .partition(|&bin
| duplicates
.contains_key(bin
));
427 let mut installed
= Transaction { bins: Vec::new() }
;
428 let mut successful_bins
= BTreeSet
::new();
430 // Move the temporary copies into `dst` starting with new binaries.
431 for bin
in to_install
.iter() {
432 let src
= staging_dir
.path().join(bin
);
433 let dst
= dst
.join(bin
);
434 self.config
.shell().status("Installing", dst
.display())?
;
435 fs
::rename(&src
, &dst
).with_context(|| {
436 format
!("failed to move `{}` to `{}`", src
.display(), dst
.display())
438 installed
.bins
.push(dst
);
439 successful_bins
.insert(bin
.to_string());
442 // Repeat for binaries which replace existing ones but don't pop the error
443 // up until after updating metadata.
444 let replace_result
= {
445 let mut try_install
= || -> CargoResult
<()> {
446 for &bin
in to_replace
.iter() {
447 let src
= staging_dir
.path().join(bin
);
448 let dst
= dst
.join(bin
);
449 self.config
.shell().status("Replacing", dst
.display())?
;
450 fs
::rename(&src
, &dst
).with_context(|| {
451 format
!("failed to move `{}` to `{}`", src
.display(), dst
.display())
453 successful_bins
.insert(bin
.to_string());
460 if let Some(mut tracker
) = tracker
{
461 tracker
.mark_installed(
464 self.vers
.map(|s
| s
.to_string()),
467 &self.rustc
.verbose_version
,
471 remove_orphaned_bins(&self.ws
, &mut tracker
, &duplicates
, &self.pkg
, &dst
)
473 // Don't hard error on remove.
476 .warn(format
!("failed to remove orphan: {:?}", e
))?
;
479 match tracker
.save() {
480 Err(err
) => replace_result
.with_context(|| err
)?
,
481 Ok(_
) => replace_result?
,
485 // Reaching here means all actions have succeeded. Clean up.
488 // Don't bother grabbing a lock as we're going to blow it all away
490 let target_dir
= self.ws
.target_dir().into_path_unlocked();
491 paths
::remove_dir_all(&target_dir
)?
;
494 // Helper for creating status messages.
495 fn executables
<T
: AsRef
<str>>(mut names
: impl Iterator
<Item
= T
> + Clone
) -> String
{
496 if names
.clone().count() == 1 {
497 format
!("(executable `{}`)", names
.next().unwrap().as_ref())
502 .map(|b
| format
!("`{}`", b
.as_ref()))
509 if duplicates
.is_empty() {
510 self.config
.shell().status(
515 executables(successful_bins
.iter())
520 if !to_install
.is_empty() {
521 self.config
.shell().status(
523 format
!("package `{}` {}", self.pkg
, executables(to_install
.iter())),
526 // Invert the duplicate map.
527 let mut pkg_map
= BTreeMap
::new();
528 for (bin_name
, opt_pkg_id
) in &duplicates
{
530 opt_pkg_id
.map_or_else(|| "unknown".to_string(), |pkg_id
| pkg_id
.to_string());
531 pkg_map
.entry(key
).or_insert_with(Vec
::new
).push(bin_name
);
533 for (pkg_descr
, bin_names
) in &pkg_map
{
534 self.config
.shell().status(
537 "package `{}` with `{}` {}",
540 executables(bin_names
.iter())
548 fn check_yanked_install(&self) -> CargoResult
<()> {
549 if self.ws
.ignore_lock() || !self.ws
.root().join("Cargo.lock").exists() {
552 // It would be best if `source` could be passed in here to avoid a
553 // duplicate "Updating", but since `source` is taken by value, then it
554 // wouldn't be available for `compile_ws`.
555 let (pkg_set
, resolve
) = ops
::resolve_ws(&self.ws
)?
;
560 "consider running without --locked",
565 fn make_warning_about_missing_features(binaries
: &[&Target
]) -> String
{
566 let max_targets_listed
= 7;
567 let target_features_message
= binaries
569 .take(max_targets_listed
)
571 let name
= b
.description_named();
574 .unwrap_or(&Vec
::new())
576 .map(|f
| format
!("`{f}`"))
578 format
!(" {name} requires the features: {features}")
582 let additional_bins_message
= if binaries
.len() > max_targets_listed
{
584 "\n{} more targets also requires features not enabled. See them in the Cargo.toml file.",
585 binaries
.len() - max_targets_listed
591 let example_features
= binaries
[0]
593 .map(|f
| f
.join(" "))
594 .unwrap_or_default();
598 none of the package's binaries are available for install using the selected features
599 {target_features_message}{additional_bins_message}
600 Consider enabling some of the needed features by passing, e.g., `--features=\"{example_features}\"`"
607 krates
: Vec
<(&str, Option
<&str>)>,
610 opts
: &ops
::CompileOptions
,
613 ) -> CargoResult
<()> {
614 let root
= resolve_root(root
, config
)?
;
615 let dst
= root
.join("bin").into_path_unlocked();
616 let map
= SourceConfigMap
::new(config
)?
;
618 let (installed_anything
, scheduled_error
) = if krates
.len() <= 1 {
619 let (krate
, vers
) = krates
622 .map(|(k
, v
)| (Some(k
), v
))
623 .unwrap_or((None
, None
));
624 let installable_pkg
= InstallablePackage
::new(
625 config
, root
, map
, krate
, source_id
, from_cwd
, vers
, opts
, force
, no_track
, true,
627 let mut installed_anything
= true;
628 if let Some(installable_pkg
) = installable_pkg
{
629 installed_anything
= installable_pkg
.install_one()?
;
631 (installed_anything
, false)
633 let mut succeeded
= vec
![];
634 let mut failed
= vec
![];
635 // "Tracks whether or not the source (such as a registry or git repo) has been updated.
636 // This is used to avoid updating it multiple times when installing multiple crates.
637 let mut did_update
= false;
639 let pkgs_to_install
: Vec
<_
> = krates
641 .filter_map(|(krate
, vers
)| {
642 let root
= root
.clone();
643 let map
= map
.clone();
644 match InstallablePackage
::new(
657 Ok(Some(installable_pkg
)) => {
659 Some((krate
, installable_pkg
))
663 succeeded
.push(krate
);
667 crate::display_error(&e
, &mut config
.shell());
669 // We assume an update was performed if we got an error.
677 let install_results
: Vec
<_
> = pkgs_to_install
679 .map(|(krate
, installable_pkg
)| (krate
, installable_pkg
.install_one()))
682 for (krate
, result
) in install_results
{
686 succeeded
.push(krate
);
690 crate::display_error(&e
, &mut config
.shell());
696 let mut summary
= vec
![];
697 if !succeeded
.is_empty() {
698 summary
.push(format
!("Successfully installed {}!", succeeded
.join(", ")));
700 if !failed
.is_empty() {
701 summary
.push(format
!(
702 "Failed to install {} (see error(s) above).",
706 if !succeeded
.is_empty() || !failed
.is_empty() {
707 config
.shell().status("Summary", summary
.join(" "))?
;
710 (!succeeded
.is_empty(), !failed
.is_empty())
713 if installed_anything
{
714 // Print a warning that if this directory isn't in PATH that they won't be
715 // able to run these commands.
716 let path
= config
.get_env_os("PATH").unwrap_or_default();
717 let dst_in_path
= env
::split_paths(&path
).any(|path
| path
== dst
);
720 config
.shell().warn(&format
!(
721 "be sure to add `{}` to your PATH to be \
722 able to run the installed binaries",
729 bail
!("some crates failed to install");
738 opts
: &ops
::CompileOptions
,
744 ) -> CargoResult
<bool
> {
745 let tracker
= InstallTracker
::load(config
, root
)?
;
746 let (freshness
, _duplicates
) =
747 tracker
.check_upgrade(dst
, pkg
, force
, opts
, target
, &rustc
.verbose_version
)?
;
748 Ok(freshness
.is_fresh())
751 /// Checks if vers can only be satisfied by exactly one version of a package in a registry, and it's
752 /// already installed. If this is the case, we can skip interacting with a registry to check if
753 /// newer versions may be installable, as no newer version can exist.
754 fn installed_exact_package
<T
>(
758 opts
: &ops
::CompileOptions
,
762 ) -> CargoResult
<Option
<Package
>>
766 if !dep
.version_req().is_exact() {
767 // If the version isn't exact, we may need to update the registry and look for a newer
768 // version - we can't know if the package is installed without doing so.
771 // Try getting the package from the registry without updating it, to avoid a potentially
772 // expensive network call in the case that the package is already installed.
773 // If this fails, the caller will possibly do an index update and try again, this is just a
774 // best-effort check to see if we can avoid hitting the network.
775 if let Ok(pkg
) = select_dep_pkg(source
, dep
, config
, false) {
776 let (_ws
, rustc
, target
) =
777 make_ws_rustc_target(config
, opts
, &source
.source_id(), pkg
.clone())?
;
778 if let Ok(true) = is_installed(&pkg
, config
, opts
, &rustc
, &target
, root
, dst
, force
) {
779 return Ok(Some(pkg
));
785 fn make_ws_rustc_target
<'cfg
>(
786 config
: &'cfg Config
,
787 opts
: &ops
::CompileOptions
,
788 source_id
: &SourceId
,
790 ) -> CargoResult
<(Workspace
<'cfg
>, Rustc
, String
)> {
791 let mut ws
= if source_id
.is_git() || source_id
.is_path() {
792 Workspace
::new(pkg
.manifest_path(), config
)?
794 Workspace
::ephemeral(pkg
, config
, None
, false)?
796 ws
.set_ignore_lock(config
.lock_update_allowed());
797 ws
.set_require_optional_deps(false);
799 let rustc
= config
.load_global_rustc(Some(&ws
))?
;
800 let target
= match &opts
.build_config
.single_requested_kind()?
{
801 CompileKind
::Host
=> rustc
.host
.as_str().to_owned(),
802 CompileKind
::Target(target
) => target
.short_name().to_owned(),
805 Ok((ws
, rustc
, target
))
808 /// Parses x.y.z as if it were =x.y.z, and gives CLI-specific error messages in the case of invalid
810 fn parse_semver_flag(v
: &str) -> CargoResult
<VersionReq
> {
811 // If the version begins with character <, >, =, ^, ~ parse it as a
812 // version range, otherwise parse it as a specific version
816 .ok_or_else(|| format_err
!("no version provided for the `--version` flag"))?
;
818 let is_req
= "<>=^~".contains(first
) || v
.contains('
*'
);
820 match v
.parse
::<VersionReq
>() {
823 "the `--version` provided, `{}`, is \
824 not a valid semver version requirement\n\n\
825 Please have a look at \
826 https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html \
827 for the correct format",
832 match v
.to_semver() {
833 Ok(v
) => Ok(VersionReq
::exact(&v
)),
835 let mut msg
= format
!(
836 "the `--version` provided, `{}`, is \
837 not a valid semver version: {}\n",
841 // If it is not a valid version but it is a valid version
842 // requirement, add a note to the warning
843 if v
.parse
::<VersionReq
>().is_ok() {
844 msg
.push_str(&format
!(
845 "\nif you want to specify semver range, \
846 add an explicit qualifier, like ^{}",
856 /// Display a list of installed binaries.
857 pub fn install_list(dst
: Option
<&str>, config
: &Config
) -> CargoResult
<()> {
858 let root
= resolve_root(dst
, config
)?
;
859 let tracker
= InstallTracker
::load(config
, &root
)?
;
860 for (k
, v
) in tracker
.all_installed_bins() {
861 drop_println
!(config
, "{}:", k
);
863 drop_println
!(config
, " {}", bin
);
869 /// Removes executables that are no longer part of a package that was
870 /// previously installed.
871 fn remove_orphaned_bins(
873 tracker
: &mut InstallTracker
,
874 duplicates
: &BTreeMap
<String
, Option
<PackageId
>>,
877 ) -> CargoResult
<()> {
878 let filter
= ops
::CompileFilter
::new_all_targets();
879 let all_self_names
= exe_names(pkg
, &filter
);
880 let mut to_remove
: HashMap
<PackageId
, BTreeSet
<String
>> = HashMap
::new();
881 // For each package that we stomped on.
882 for other_pkg
in duplicates
.values().flatten() {
883 // Only for packages with the same name.
884 if other_pkg
.name() == pkg
.name() {
885 // Check what the old package had installed.
886 if let Some(installed
) = tracker
.installed_bins(*other_pkg
) {
887 // If the old install has any names that no longer exist,
888 // add them to the list to remove.
889 for installed_name
in installed
{
890 if !all_self_names
.contains(installed_name
.as_str()) {
894 .insert(installed_name
.clone());
901 for (old_pkg
, bins
) in to_remove
{
902 tracker
.remove(old_pkg
, &bins
);
904 let full_path
= dst
.join(bin
);
905 if full_path
.exists() {
906 ws
.config().shell().status(
909 "executable `{}` from previous version {}",
914 paths
::remove_file(&full_path
)
915 .with_context(|| format
!("failed to remove {:?}", full_path
))?
;