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 anyhow::{bail, format_err};
7use tempfile::Builder as TempFileBuilder;
8
9use crate::core::compiler::Freshness;
10use crate::core::compiler::{CompileKind, DefaultExecutor, Executor};
11use crate::core::{Edition, Package, PackageId, Source, SourceId, Workspace};
12use crate::ops;
13use crate::ops::common_for_install_and_uninstall::*;
14use crate::sources::{GitSource, SourceConfigMap};
15use crate::util::errors::{CargoResult, CargoResultExt};
16use crate::util::{paths, Config, Filesystem};
17
18struct Transaction {
19    bins: Vec<PathBuf>,
20}
21
22impl Transaction {
23    fn success(mut self) {
24        self.bins.clear();
25    }
26}
27
28impl Drop for Transaction {
29    fn drop(&mut self) {
30        for bin in self.bins.iter() {
31            let _ = paths::remove_file(bin);
32        }
33    }
34}
35
36pub fn install(
37    config: &Config,
38    root: Option<&str>,
39    krates: Vec<&str>,
40    source_id: SourceId,
41    from_cwd: bool,
42    vers: Option<&str>,
43    opts: &ops::CompileOptions,
44    force: bool,
45    no_track: bool,
46) -> CargoResult<()> {
47    let root = resolve_root(root, config)?;
48    let map = SourceConfigMap::new(config)?;
49
50    let (installed_anything, scheduled_error) = if krates.len() <= 1 {
51        install_one(
52            config,
53            &root,
54            &map,
55            krates.into_iter().next(),
56            source_id,
57            from_cwd,
58            vers,
59            opts,
60            force,
61            no_track,
62            true,
63        )?;
64        (true, false)
65    } else {
66        let mut succeeded = vec![];
67        let mut failed = vec![];
68        let mut first = true;
69        for krate in krates {
70            let root = root.clone();
71            let map = map.clone();
72            match install_one(
73                config,
74                &root,
75                &map,
76                Some(krate),
77                source_id,
78                from_cwd,
79                vers,
80                opts,
81                force,
82                no_track,
83                first,
84            ) {
85                Ok(()) => succeeded.push(krate),
86                Err(e) => {
87                    crate::display_error(&e, &mut config.shell());
88                    failed.push(krate)
89                }
90            }
91            first = false;
92        }
93
94        let mut summary = vec![];
95        if !succeeded.is_empty() {
96            summary.push(format!("Successfully installed {}!", succeeded.join(", ")));
97        }
98        if !failed.is_empty() {
99            summary.push(format!(
100                "Failed to install {} (see error(s) above).",
101                failed.join(", ")
102            ));
103        }
104        if !succeeded.is_empty() || !failed.is_empty() {
105            config.shell().status("Summary", summary.join(" "))?;
106        }
107
108        (!succeeded.is_empty(), !failed.is_empty())
109    };
110
111    if installed_anything {
112        // Print a warning that if this directory isn't in PATH that they won't be
113        // able to run these commands.
114        let dst = root.join("bin").into_path_unlocked();
115        let path = env::var_os("PATH").unwrap_or_default();
116        for path in env::split_paths(&path) {
117            if path == dst {
118                return Ok(());
119            }
120        }
121
122        config.shell().warn(&format!(
123            "be sure to add `{}` to your PATH to be \
124             able to run the installed binaries",
125            dst.display()
126        ))?;
127    }
128
129    if scheduled_error {
130        bail!("some crates failed to install");
131    }
132
133    Ok(())
134}
135
136fn install_one(
137    config: &Config,
138    root: &Filesystem,
139    map: &SourceConfigMap<'_>,
140    krate: Option<&str>,
141    source_id: SourceId,
142    from_cwd: bool,
143    vers: Option<&str>,
144    opts: &ops::CompileOptions,
145    force: bool,
146    no_track: bool,
147    is_first_install: bool,
148) -> CargoResult<()> {
149    let pkg = if source_id.is_git() {
150        select_pkg(
151            GitSource::new(source_id, config)?,
152            krate,
153            vers,
154            config,
155            true,
156            &mut |git| git.read_packages(),
157        )?
158    } else if source_id.is_path() {
159        let mut src = path_source(source_id, config)?;
160        if !src.path().is_dir() {
161            bail!(
162                "`{}` is not a directory. \
163                 --path must point to a directory containing a Cargo.toml file.",
164                src.path().display()
165            )
166        }
167        if !src.path().join("Cargo.toml").exists() {
168            if from_cwd {
169                bail!(
170                    "`{}` is not a crate root; specify a crate to \
171                     install from crates.io, or use --path or --git to \
172                     specify an alternate source",
173                    src.path().display()
174                );
175            } else {
176                bail!(
177                    "`{}` does not contain a Cargo.toml file. \
178                     --path must point to a directory containing a Cargo.toml file.",
179                    src.path().display()
180                )
181            }
182        }
183        src.update()?;
184        select_pkg(src, krate, vers, config, false, &mut |path| {
185            path.read_packages()
186        })?
187    } else {
188        select_pkg(
189            map.load(source_id, &HashSet::new())?,
190            krate,
191            vers,
192            config,
193            is_first_install,
194            &mut |_| {
195                bail!(
196                    "must specify a crate to install from \
197                     crates.io, or use --path or --git to \
198                     specify alternate source"
199                )
200            },
201        )?
202    };
203
204    let (mut ws, git_package) = if source_id.is_git() {
205        // Don't use ws.current() in order to keep the package source as a git source so that
206        // install tracking uses the correct source.
207        (Workspace::new(pkg.manifest_path(), config)?, Some(&pkg))
208    } else if source_id.is_path() {
209        (Workspace::new(pkg.manifest_path(), config)?, None)
210    } else {
211        (Workspace::ephemeral(pkg, config, None, false)?, None)
212    };
213    ws.set_ignore_lock(config.lock_update_allowed());
214    ws.set_require_optional_deps(false);
215
216    let mut td_opt = None;
217    let mut needs_cleanup = false;
218    if !source_id.is_path() {
219        let target_dir = if let Some(dir) = config.target_dir()? {
220            dir
221        } else if let Ok(td) = TempFileBuilder::new().prefix("cargo-install").tempdir() {
222            let p = td.path().to_owned();
223            td_opt = Some(td);
224            Filesystem::new(p)
225        } else {
226            needs_cleanup = true;
227            Filesystem::new(config.cwd().join("target-install"))
228        };
229        ws.set_target_dir(target_dir);
230    }
231
232    let pkg = git_package.map_or_else(|| ws.current(), |pkg| Ok(pkg))?;
233
234    if from_cwd {
235        if pkg.manifest().edition() == Edition::Edition2015 {
236            config.shell().warn(
237                "Using `cargo install` to install the binaries for the \
238                 package in current working directory is deprecated, \
239                 use `cargo install --path .` instead. \
240                 Use `cargo build` if you want to simply build the package.",
241            )?
242        } else {
243            bail!(
244                "Using `cargo install` to install the binaries for the \
245                 package in current working directory is no longer supported, \
246                 use `cargo install --path .` instead. \
247                 Use `cargo build` if you want to simply build the package."
248            )
249        }
250    };
251
252    // For bare `cargo install` (no `--bin` or `--example`), check if there is
253    // *something* to install. Explicit `--bin` or `--example` flags will be
254    // checked at the start of `compile_ws`.
255    if !opts.filter.is_specific() && !pkg.targets().iter().any(|t| t.is_bin()) {
256        bail!("specified package `{}` has no binaries", pkg);
257    }
258
259    // Preflight checks to check up front whether we'll overwrite something.
260    // We have to check this again afterwards, but may as well avoid building
261    // anything if we're gonna throw it away anyway.
262    let dst = root.join("bin").into_path_unlocked();
263    let rustc = config.load_global_rustc(Some(&ws))?;
264    let target = match &opts.build_config.requested_kind {
265        CompileKind::Host => rustc.host.as_str(),
266        CompileKind::Target(target) => target.short_name(),
267    };
268
269    // Helper for --no-track flag to make sure it doesn't overwrite anything.
270    let no_track_duplicates = || -> CargoResult<BTreeMap<String, Option<PackageId>>> {
271        let duplicates: BTreeMap<String, Option<PackageId>> = exe_names(pkg, &opts.filter)
272            .into_iter()
273            .filter(|name| dst.join(name).exists())
274            .map(|name| (name, None))
275            .collect();
276        if !force && !duplicates.is_empty() {
277            let mut msg: Vec<String> = duplicates
278                .iter()
279                .map(|(name, _)| format!("binary `{}` already exists in destination", name))
280                .collect();
281            msg.push("Add --force to overwrite".to_string());
282            bail!("{}", msg.join("\n"));
283        }
284        Ok(duplicates)
285    };
286
287    // WARNING: no_track does not perform locking, so there is no protection
288    // of concurrent installs.
289    if no_track {
290        // Check for conflicts.
291        no_track_duplicates()?;
292    } else {
293        let tracker = InstallTracker::load(config, root)?;
294        let (freshness, _duplicates) =
295            tracker.check_upgrade(&dst, pkg, force, opts, target, &rustc.verbose_version)?;
296        if freshness == Freshness::Fresh {
297            let msg = format!(
298                "package `{}` is already installed, use --force to override",
299                pkg
300            );
301            config.shell().status("Ignored", &msg)?;
302            return Ok(());
303        }
304        // Unlock while building.
305        drop(tracker);
306    }
307
308    config.shell().status("Installing", pkg)?;
309
310    check_yanked_install(&ws)?;
311
312    let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
313    let compile = ops::compile_ws(&ws, opts, &exec).chain_err(|| {
314        if let Some(td) = td_opt.take() {
315            // preserve the temporary directory, so the user can inspect it
316            td.into_path();
317        }
318
319        format_err!(
320            "failed to compile `{}`, intermediate artifacts can be \
321             found at `{}`",
322            pkg,
323            ws.target_dir().display()
324        )
325    })?;
326    let mut binaries: Vec<(&str, &Path)> = compile
327        .binaries
328        .iter()
329        .map(|bin| {
330            let name = bin.file_name().unwrap();
331            if let Some(s) = name.to_str() {
332                Ok((s, bin.as_ref()))
333            } else {
334                bail!("Binary `{:?}` name can't be serialized into string", name)
335            }
336        })
337        .collect::<CargoResult<_>>()?;
338    if binaries.is_empty() {
339        bail!("no binaries are available for install using the selected features");
340    }
341    // This is primarily to make testing easier.
342    binaries.sort_unstable();
343
344    let (tracker, duplicates) = if no_track {
345        (None, no_track_duplicates()?)
346    } else {
347        let tracker = InstallTracker::load(config, root)?;
348        let (_freshness, duplicates) =
349            tracker.check_upgrade(&dst, pkg, force, opts, target, &rustc.verbose_version)?;
350        (Some(tracker), duplicates)
351    };
352
353    paths::create_dir_all(&dst)?;
354
355    // Copy all binaries to a temporary directory under `dst` first, catching
356    // some failure modes (e.g., out of space) before touching the existing
357    // binaries. This directory will get cleaned up via RAII.
358    let staging_dir = TempFileBuilder::new()
359        .prefix("cargo-install")
360        .tempdir_in(&dst)?;
361    for &(bin, src) in binaries.iter() {
362        let dst = staging_dir.path().join(bin);
363        // Try to move if `target_dir` is transient.
364        if !source_id.is_path() && fs::rename(src, &dst).is_ok() {
365            continue;
366        }
367        fs::copy(src, &dst).chain_err(|| {
368            format_err!("failed to copy `{}` to `{}`", src.display(), dst.display())
369        })?;
370    }
371
372    let (to_replace, to_install): (Vec<&str>, Vec<&str>) = binaries
373        .iter()
374        .map(|&(bin, _)| bin)
375        .partition(|&bin| duplicates.contains_key(bin));
376
377    let mut installed = Transaction { bins: Vec::new() };
378    let mut successful_bins = BTreeSet::new();
379
380    // Move the temporary copies into `dst` starting with new binaries.
381    for bin in to_install.iter() {
382        let src = staging_dir.path().join(bin);
383        let dst = dst.join(bin);
384        config.shell().status("Installing", dst.display())?;
385        fs::rename(&src, &dst).chain_err(|| {
386            format_err!("failed to move `{}` to `{}`", src.display(), dst.display())
387        })?;
388        installed.bins.push(dst);
389        successful_bins.insert(bin.to_string());
390    }
391
392    // Repeat for binaries which replace existing ones but don't pop the error
393    // up until after updating metadata.
394    let replace_result = {
395        let mut try_install = || -> CargoResult<()> {
396            for &bin in to_replace.iter() {
397                let src = staging_dir.path().join(bin);
398                let dst = dst.join(bin);
399                config.shell().status("Replacing", dst.display())?;
400                fs::rename(&src, &dst).chain_err(|| {
401                    format_err!("failed to move `{}` to `{}`", src.display(), dst.display())
402                })?;
403                successful_bins.insert(bin.to_string());
404            }
405            Ok(())
406        };
407        try_install()
408    };
409
410    if let Some(mut tracker) = tracker {
411        tracker.mark_installed(
412            pkg,
413            &successful_bins,
414            vers.map(|s| s.to_string()),
415            opts,
416            target,
417            &rustc.verbose_version,
418        );
419
420        if let Err(e) = remove_orphaned_bins(&ws, &mut tracker, &duplicates, pkg, &dst) {
421            // Don't hard error on remove.
422            config
423                .shell()
424                .warn(format!("failed to remove orphan: {:?}", e))?;
425        }
426
427        match tracker.save() {
428            Err(err) => replace_result.chain_err(|| err)?,
429            Ok(_) => replace_result?,
430        }
431    }
432
433    // Reaching here means all actions have succeeded. Clean up.
434    installed.success();
435    if needs_cleanup {
436        // Don't bother grabbing a lock as we're going to blow it all away
437        // anyway.
438        let target_dir = ws.target_dir().into_path_unlocked();
439        paths::remove_dir_all(&target_dir)?;
440    }
441
442    // Helper for creating status messages.
443    fn executables<T: AsRef<str>>(mut names: impl Iterator<Item = T> + Clone) -> String {
444        if names.clone().count() == 1 {
445            format!("(executable `{}`)", names.next().unwrap().as_ref())
446        } else {
447            format!(
448                "(executables {})",
449                names
450                    .map(|b| format!("`{}`", b.as_ref()))
451                    .collect::<Vec<_>>()
452                    .join(", ")
453            )
454        }
455    }
456
457    if duplicates.is_empty() {
458        config.shell().status(
459            "Installed",
460            format!("package `{}` {}", pkg, executables(successful_bins.iter())),
461        )?;
462        Ok(())
463    } else {
464        if !to_install.is_empty() {
465            config.shell().status(
466                "Installed",
467                format!("package `{}` {}", pkg, executables(to_install.iter())),
468            )?;
469        }
470        // Invert the duplicate map.
471        let mut pkg_map = BTreeMap::new();
472        for (bin_name, opt_pkg_id) in &duplicates {
473            let key = opt_pkg_id.map_or_else(|| "unknown".to_string(), |pkg_id| pkg_id.to_string());
474            pkg_map.entry(key).or_insert_with(Vec::new).push(bin_name);
475        }
476        for (pkg_descr, bin_names) in &pkg_map {
477            config.shell().status(
478                "Replaced",
479                format!(
480                    "package `{}` with `{}` {}",
481                    pkg_descr,
482                    pkg,
483                    executables(bin_names.iter())
484                ),
485            )?;
486        }
487        Ok(())
488    }
489}
490
491fn check_yanked_install(ws: &Workspace<'_>) -> CargoResult<()> {
492    if ws.ignore_lock() || !ws.root().join("Cargo.lock").exists() {
493        return Ok(());
494    }
495    // It would be best if `source` could be passed in here to avoid a
496    // duplicate "Updating", but since `source` is taken by value, then it
497    // wouldn't be available for `compile_ws`.
498    let (pkg_set, resolve) = ops::resolve_ws(ws)?;
499    let mut sources = pkg_set.sources_mut();
500
501    // Checking the yanked status involves taking a look at the registry and
502    // maybe updating files, so be sure to lock it here.
503    let _lock = ws.config().acquire_package_cache_lock()?;
504
505    for pkg_id in resolve.iter() {
506        if let Some(source) = sources.get_mut(pkg_id.source_id()) {
507            if source.is_yanked(pkg_id)? {
508                ws.config().shell().warn(format!(
509                    "package `{}` in Cargo.lock is yanked in registry `{}`, \
510                     consider running without --locked",
511                    pkg_id,
512                    pkg_id.source_id().display_registry_name()
513                ))?;
514            }
515        }
516    }
517
518    Ok(())
519}
520
521/// Display a list of installed binaries.
522pub fn install_list(dst: Option<&str>, config: &Config) -> CargoResult<()> {
523    let root = resolve_root(dst, config)?;
524    let tracker = InstallTracker::load(config, &root)?;
525    for (k, v) in tracker.all_installed_bins() {
526        println!("{}:", k);
527        for bin in v {
528            println!("    {}", bin);
529        }
530    }
531    Ok(())
532}
533
534/// Removes executables that are no longer part of a package that was
535/// previously installed.
536fn remove_orphaned_bins(
537    ws: &Workspace<'_>,
538    tracker: &mut InstallTracker,
539    duplicates: &BTreeMap<String, Option<PackageId>>,
540    pkg: &Package,
541    dst: &Path,
542) -> CargoResult<()> {
543    let filter = ops::CompileFilter::new_all_targets();
544    let all_self_names = exe_names(pkg, &filter);
545    let mut to_remove: HashMap<PackageId, BTreeSet<String>> = HashMap::new();
546    // For each package that we stomped on.
547    for other_pkg in duplicates.values() {
548        // Only for packages with the same name.
549        if let Some(other_pkg) = other_pkg {
550            if other_pkg.name() == pkg.name() {
551                // Check what the old package had installed.
552                if let Some(installed) = tracker.installed_bins(*other_pkg) {
553                    // If the old install has any names that no longer exist,
554                    // add them to the list to remove.
555                    for installed_name in installed {
556                        if !all_self_names.contains(installed_name.as_str()) {
557                            to_remove
558                                .entry(*other_pkg)
559                                .or_default()
560                                .insert(installed_name.clone());
561                        }
562                    }
563                }
564            }
565        }
566    }
567
568    for (old_pkg, bins) in to_remove {
569        tracker.remove(old_pkg, &bins);
570        for bin in bins {
571            let full_path = dst.join(bin);
572            if full_path.exists() {
573                ws.config().shell().status(
574                    "Removing",
575                    format!(
576                        "executable `{}` from previous version {}",
577                        full_path.display(),
578                        old_pkg
579                    ),
580                )?;
581                paths::remove_file(&full_path)
582                    .chain_err(|| format!("failed to remove {:?}", full_path))?;
583            }
584        }
585    }
586    Ok(())
587}