Skip to content

Commit f42275c

Browse files
committed
Auto merge of #7246 - ehuss:install-remove-orphan, r=alexcrichton
`cargo install`: Remove orphaned executables. When a new version of a package is installed that no longer contains an executable from a previous version, this change causes those orphaned executables to also be removed. I can place this new behavior behind the `install-upgrade` feature gate if anyone is uncomfortable with changing the behavior now. cc #6797
2 parents e01a8f3 + 5a59b80 commit f42275c

File tree

5 files changed

+152
-17
lines changed

5 files changed

+152
-17
lines changed

src/cargo/ops/cargo_compile.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,9 @@ impl CompileFilter {
443443
all_bens: bool,
444444
all_targets: bool,
445445
) -> CompileFilter {
446+
if all_targets {
447+
return CompileFilter::new_all_targets();
448+
}
446449
let rule_lib = if lib_only {
447450
LibRule::True
448451
} else {
@@ -453,18 +456,7 @@ impl CompileFilter {
453456
let rule_exms = FilterRule::new(exms, all_exms);
454457
let rule_bens = FilterRule::new(bens, all_bens);
455458

456-
if all_targets {
457-
CompileFilter::Only {
458-
all_targets: true,
459-
lib: LibRule::Default,
460-
bins: FilterRule::All,
461-
examples: FilterRule::All,
462-
benches: FilterRule::All,
463-
tests: FilterRule::All,
464-
}
465-
} else {
466-
CompileFilter::new(rule_lib, rule_bins, rule_tsts, rule_exms, rule_bens)
467-
}
459+
CompileFilter::new(rule_lib, rule_bins, rule_tsts, rule_exms, rule_bens)
468460
}
469461

470462
/// Construct a CompileFilter from underlying primitives.
@@ -496,6 +488,17 @@ impl CompileFilter {
496488
}
497489
}
498490

491+
pub fn new_all_targets() -> CompileFilter {
492+
CompileFilter::Only {
493+
all_targets: true,
494+
lib: LibRule::Default,
495+
bins: FilterRule::All,
496+
examples: FilterRule::All,
497+
benches: FilterRule::All,
498+
tests: FilterRule::All,
499+
}
500+
}
501+
499502
pub fn need_dev_deps(&self, mode: CompileMode) -> bool {
500503
match mode {
501504
CompileMode::Test | CompileMode::Doctest | CompileMode::Bench => true,

src/cargo/ops/cargo_install.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::{BTreeMap, BTreeSet, HashSet};
1+
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
22
use std::path::{Path, PathBuf};
33
use std::sync::Arc;
44
use std::{env, fs};
@@ -9,7 +9,7 @@ use tempfile::Builder as TempFileBuilder;
99
use crate::core::compiler::Freshness;
1010
use crate::core::compiler::{DefaultExecutor, Executor};
1111
use crate::core::resolver::ResolveOpts;
12-
use crate::core::{Edition, PackageId, PackageIdSpec, Source, SourceId, Workspace};
12+
use crate::core::{Edition, Package, PackageId, PackageIdSpec, Source, SourceId, Workspace};
1313
use crate::ops;
1414
use crate::ops::common_for_install_and_uninstall::*;
1515
use crate::sources::{GitSource, SourceConfigMap};
@@ -414,6 +414,13 @@ fn install_one(
414414
rustc.verbose_version,
415415
);
416416

417+
if let Err(e) = remove_orphaned_bins(&ws, &mut tracker, &duplicates, pkg, &dst) {
418+
// Don't hard error on remove.
419+
config
420+
.shell()
421+
.warn(format!("failed to remove orphan: {:?}", e))?;
422+
}
423+
417424
match tracker.save() {
418425
Err(err) => replace_result.chain_err(|| err)?,
419426
Ok(_) => replace_result?,
@@ -521,3 +528,58 @@ pub fn install_list(dst: Option<&str>, config: &Config) -> CargoResult<()> {
521528
}
522529
Ok(())
523530
}
531+
532+
/// Removes executables that are no longer part of a package that was
533+
/// previously installed.
534+
fn remove_orphaned_bins(
535+
ws: &Workspace<'_>,
536+
tracker: &mut InstallTracker,
537+
duplicates: &BTreeMap<String, Option<PackageId>>,
538+
pkg: &Package,
539+
dst: &Path,
540+
) -> CargoResult<()> {
541+
let filter = ops::CompileFilter::new_all_targets();
542+
let all_self_names = exe_names(pkg, &filter);
543+
let mut to_remove: HashMap<PackageId, BTreeSet<String>> = HashMap::new();
544+
// For each package that we stomped on.
545+
for other_pkg in duplicates.values() {
546+
// Only for packages with the same name.
547+
if let Some(other_pkg) = other_pkg {
548+
if other_pkg.name() == pkg.name() {
549+
// Check what the old package had installed.
550+
if let Some(installed) = tracker.installed_bins(*other_pkg) {
551+
// If the old install has any names that no longer exist,
552+
// add them to the list to remove.
553+
for installed_name in installed {
554+
if !all_self_names.contains(installed_name.as_str()) {
555+
to_remove
556+
.entry(*other_pkg)
557+
.or_default()
558+
.insert(installed_name.clone());
559+
}
560+
}
561+
}
562+
}
563+
}
564+
}
565+
566+
for (old_pkg, bins) in to_remove {
567+
tracker.remove(old_pkg, &bins);
568+
for bin in bins {
569+
let full_path = dst.join(bin);
570+
if full_path.exists() {
571+
ws.config().shell().status(
572+
"Removing",
573+
format!(
574+
"executable `{}` from previous version {}",
575+
full_path.display(),
576+
old_pkg
577+
),
578+
)?;
579+
paths::remove_file(&full_path)
580+
.chain_err(|| format!("failed to remove {:?}", full_path))?;
581+
}
582+
}
583+
}
584+
Ok(())
585+
}

src/cargo/ops/common_for_install_and_uninstall.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,14 @@ pub fn exe_names(pkg: &Package, filter: &ops::CompileFilter) -> BTreeSet<String>
743743
.filter(|t| t.is_bin())
744744
.map(|t| to_exe(t.name()))
745745
.collect(),
746+
CompileFilter::Only {
747+
all_targets: true, ..
748+
} => pkg
749+
.targets()
750+
.iter()
751+
.filter(|target| target.is_executable())
752+
.map(|target| to_exe(target.name()))
753+
.collect(),
746754
CompileFilter::Only {
747755
ref bins,
748756
ref examples,

tests/testsuite/install.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,7 @@ fn install_force_partial_overlap() {
531531
[FINISHED] release [optimized] target(s) in [..]
532532
[INSTALLING] [CWD]/home/.cargo/bin/foo-bin3[EXE]
533533
[REPLACING] [CWD]/home/.cargo/bin/foo-bin2[EXE]
534+
[REMOVING] executable `[..]/bin/foo-bin1[EXE]` from previous version foo v0.0.1 [..]
534535
[INSTALLED] package `foo v0.2.0 ([..]/foo2)` (executable `foo-bin3[EXE]`)
535536
[REPLACED] package `foo v0.0.1 ([..]/foo)` with `foo v0.2.0 ([..]/foo2)` (executable `foo-bin2[EXE]`)
536537
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
@@ -541,8 +542,6 @@ fn install_force_partial_overlap() {
541542
cargo_process("install --list")
542543
.with_stdout(
543544
"\
544-
foo v0.0.1 ([..]):
545-
foo-bin1[..]
546545
foo v0.2.0 ([..]):
547546
foo-bin2[..]
548547
foo-bin3[..]

tests/testsuite/install_upgrade.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ fn installed_process(name: &str) -> Execs {
6565

6666
/// Check that the given package name/version has the following bins listed in
6767
/// the trackers. Also verifies that both trackers are in sync and valid.
68+
/// Pass in an empty `bins` list to assert that the package is *not* installed.
6869
fn validate_trackers(name: &str, version: &str, bins: &[&str]) {
6970
let v1 = load_crates1();
7071
let v1_table = v1.get("v1").unwrap().as_table().unwrap();
@@ -88,7 +89,14 @@ fn validate_trackers(name: &str, version: &str, bins: &[&str]) {
8889
.map(|b| b.as_str().unwrap().to_string())
8990
.collect();
9091
if pkg_id.name().as_str() == name && pkg_id.version().to_string() == version {
91-
assert_eq!(bins, v1_bins);
92+
if bins.is_empty() {
93+
panic!(
94+
"Expected {} to not be installed, but found: {:?}",
95+
name, v1_bins
96+
);
97+
} else {
98+
assert_eq!(bins, v1_bins);
99+
}
92100
}
93101
let pkg_id_value = serde_json::to_value(&pkg_id).unwrap();
94102
let pkg_id_str = pkg_id_value.as_str().unwrap();
@@ -782,3 +790,58 @@ Add --force to overwrite
782790
.with_status(101)
783791
.run();
784792
}
793+
794+
#[cargo_test]
795+
fn deletes_orphaned() {
796+
// When an executable is removed from a project, upgrading should remove it.
797+
let p = project()
798+
.file(
799+
"Cargo.toml",
800+
r#"
801+
[package]
802+
name = "foo"
803+
version = "0.1.0"
804+
"#,
805+
)
806+
.file("src/main.rs", "fn main() {}")
807+
.file("src/bin/other.rs", "fn main() {}")
808+
.file("examples/ex1.rs", "fn main() {}")
809+
.build();
810+
p.cargo("install -Z install-upgrade --path . --bins --examples")
811+
.masquerade_as_nightly_cargo()
812+
.run();
813+
assert!(installed_exe("other").exists());
814+
815+
// Remove a binary, add a new one, and bump the version.
816+
fs::remove_file(p.root().join("src/bin/other.rs")).unwrap();
817+
p.change_file("examples/ex2.rs", "fn main() {}");
818+
p.change_file(
819+
"Cargo.toml",
820+
r#"
821+
[package]
822+
name = "foo"
823+
version = "0.2.0"
824+
"#,
825+
);
826+
p.cargo("install -Z install-upgrade --path . --bins --examples")
827+
.masquerade_as_nightly_cargo()
828+
.with_stderr(
829+
"\
830+
[INSTALLING] foo v0.2.0 [..]
831+
[COMPILING] foo v0.2.0 [..]
832+
[FINISHED] release [..]
833+
[INSTALLING] [..]/.cargo/bin/ex2[EXE]
834+
[REPLACING] [..]/.cargo/bin/ex1[EXE]
835+
[REPLACING] [..]/.cargo/bin/foo[EXE]
836+
[REMOVING] executable `[..]/.cargo/bin/other[EXE]` from previous version foo v0.1.0 [..]
837+
[INSTALLED] package `foo v0.2.0 [..]` (executable `ex2[EXE]`)
838+
[REPLACED] package `foo v0.1.0 [..]` with `foo v0.2.0 [..]` (executables `ex1[EXE]`, `foo[EXE]`)
839+
[WARNING] be sure to add [..]
840+
",
841+
)
842+
.run();
843+
assert!(!installed_exe("other").exists());
844+
validate_trackers("foo", "0.2.0", &["foo", "ex1", "ex2"]);
845+
// 0.1.0 should not have any entries.
846+
validate_trackers("foo", "0.1.0", &[]);
847+
}

0 commit comments

Comments
 (0)