fix(run): Disambiguate bins from different packages that share a name (#15298)

### What does this PR try to resolve?

This builds on the work done in #15199 to improve target selection
errors to also disambiguate when the same binary name is used in
multiple packages.

This also makes the errors from #15199 more consistent with the rustc
style guide and reduces code duplication.

Fixes #13312

### How should we test and review this PR?

This is a first pass and does not do the full `--package foo --bin bar`
syntax. I wanted to focus on the basic functionality before we iterated
on it, e.g. investigating how well we can predict the CLI flags, how
much noise they might add, etc.

### Additional information
This commit is contained in:
Weihang Lo 2025-03-13 14:56:45 +00:00 committed by GitHub
commit bc577dc6c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 297 additions and 216 deletions

View File

@ -259,24 +259,27 @@ impl<'a> UnitGenerator<'a, '_> {
};
let proposals = self.filter_targets(filter, true, mode);
if proposals.is_empty() {
let targets = self
.packages
.iter()
.flat_map(|pkg| {
pkg.targets()
.iter()
.filter(|target| is_expected_kind(target))
})
.collect::<Vec<_>>();
let suggestion = closest_msg(target_name, targets.iter(), |t| t.name(), "target");
let mut targets = std::collections::BTreeMap::new();
for (pkg, target) in self.packages.iter().flat_map(|pkg| {
pkg.targets()
.iter()
.filter(|target| is_expected_kind(target))
.map(move |t| (pkg, t))
}) {
targets
.entry(target.name())
.or_insert_with(Vec::new)
.push((pkg, target));
}
let suggestion = closest_msg(target_name, targets.keys(), |t| t, "target");
let targets_elsewhere = self.get_targets_from_other_packages(filter)?;
let need_append_targets_elsewhere = !targets_elsewhere.is_empty();
let append_targets_elsewhere = |msg: &mut String, prefix: &str| {
let append_targets_elsewhere = |msg: &mut String| {
let mut available_msg = Vec::new();
for (package, targets) in targets_elsewhere {
for (package, targets) in &targets_elsewhere {
if !targets.is_empty() {
available_msg.push(format!(
"help: Available {target_desc} in `{package}` package:"
"help: available {target_desc} in `{package}` package:"
));
for target in targets {
available_msg.push(format!(" {target}"));
@ -284,12 +287,12 @@ impl<'a> UnitGenerator<'a, '_> {
}
}
if !available_msg.is_empty() {
write!(msg, "{prefix}{}", available_msg.join("\n"))?;
write!(msg, "\n{}", available_msg.join("\n"))?;
}
CargoResult::Ok(())
};
let unmatched_packages = || match self.spec {
let unmatched_packages = match self.spec {
Packages::Default | Packages::OptOut(_) | Packages::All(_) => {
"default-run packages".to_owned()
}
@ -305,33 +308,25 @@ impl<'a> UnitGenerator<'a, '_> {
}
};
let mut msg = String::new();
if !suggestion.is_empty() {
write!(
msg,
"no {} target {} `{}` in {}{}",
target_desc,
if is_glob { "matches pattern" } else { "named" },
target_name,
unmatched_packages(),
suggestion,
)?;
append_targets_elsewhere(&mut msg, "\n")?;
} else {
writeln!(
msg,
"no {} target {} `{}` in {}.",
target_desc,
if is_glob { "matches pattern" } else { "named" },
target_name,
unmatched_packages()
)?;
let named = if is_glob { "matches pattern" } else { "named" };
append_targets_elsewhere(&mut msg, "")?;
if !targets.is_empty() && !need_append_targets_elsewhere {
writeln!(msg, "Available {} targets:", target_desc)?;
for target in targets {
writeln!(msg, " {}", target.name())?;
let mut msg = String::new();
write!(
msg,
"no {target_desc} target {named} `{target_name}` in {unmatched_packages}{suggestion}",
)?;
if !targets_elsewhere.is_empty() {
append_targets_elsewhere(&mut msg)?;
} else if suggestion.is_empty() && !targets.is_empty() {
write!(msg, "\nhelp: available {} targets:", target_desc)?;
for (target_name, pkgs) in targets {
if pkgs.len() == 1 {
write!(msg, "\n {target_name}")?;
} else {
for (pkg, _) in pkgs {
let pkg_name = pkg.name();
write!(msg, "\n {target_name} in package {pkg_name}")?;
}
}
}
}

View File

@ -1,4 +1,5 @@
use std::ffi::OsString;
use std::fmt::Write as _;
use std::iter;
use std::path::Path;
@ -69,10 +70,20 @@ pub fn run(
names.join(", ")
)
} else {
anyhow::bail!(
"`cargo run` can run at most one executable, but \
let mut message = "`cargo run` can run at most one executable, but \
multiple were specified"
)
.to_owned();
write!(&mut message, "\nhelp: available targets:")?;
for (pkg, bin) in &bins {
write!(
&mut message,
"\n {} `{}` in package `{}`",
bin.kind().description(),
bin.name(),
pkg.name()
)?;
}
anyhow::bail!(message)
}
}

View File

@ -1337,11 +1337,10 @@ fn cargo_compile_with_filename() {
p.cargo("build --bin bin.rs")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `bin.rs` in default-run packages.
Available bin targets:
[ERROR] no bin target named `bin.rs` in default-run packages
[HELP] available bin targets:
a
"#]])
.run();
@ -1358,11 +1357,10 @@ Available bin targets:
p.cargo("build --example example.rs")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no example target named `example.rs` in default-run packages.
Available example targets:
[ERROR] no example target named `example.rs` in default-run packages
[HELP] available example targets:
a
"#]])
.run();

View File

@ -623,11 +623,10 @@ automatically infer them to be a target, such as in subfolders.
For more information on this warning you can consult
https://github.com/rust-lang/cargo/issues/5330
[ERROR] no example target named `a` in default-run packages.
Available example targets:
[ERROR] no example target named `a` in default-run packages
[HELP] available example targets:
do_magic
"#]])
.run();
}
@ -655,11 +654,10 @@ fn run_example_autodiscover_2015_with_autoexamples_disabled() {
p.cargo("run --example a")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no example target named `a` in default-run packages.
Available example targets:
[ERROR] no example target named `a` in default-run packages
[HELP] available example targets:
do_magic
"#]])
.run();
}
@ -743,11 +741,10 @@ fn run_with_filename() {
p.cargo("run --bin bin.rs")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `bin.rs` in default-run packages.
Available bin targets:
[ERROR] no bin target named `bin.rs` in default-run packages
[HELP] available bin targets:
a
"#]])
.run();
@ -764,11 +761,10 @@ Available bin targets:
p.cargo("run --example example.rs")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no example target named `example.rs` in default-run packages.
Available example targets:
[ERROR] no example target named `example.rs` in default-run packages
[HELP] available example targets:
a
"#]])
.run();
@ -783,6 +779,235 @@ Available example targets:
.run();
}
#[cargo_test]
fn ambiguous_bin_name() {
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
resolver = "3"
members = ["crate1", "crate2", "crate3", "crate4"]
"#,
)
.file("crate1/src/bin/ambiguous.rs", "fn main(){}")
.file(
"crate1/Cargo.toml",
r#"
[package]
name = "crate1"
version = "0.1.0"
edition = "2024"
"#,
)
.file("crate2/src/bin/ambiguous.rs", "fn main(){}")
.file(
"crate2/Cargo.toml",
r#"
[package]
name = "crate2"
version = "0.1.0"
edition = "2024"
"#,
)
.file("crate3/src/bin/ambiguous.rs", "fn main(){}")
.file(
"crate3/Cargo.toml",
r#"
[package]
name = "crate3"
version = "0.1.0"
edition = "2024"
"#,
)
.file("crate4/src/bin/ambiguous.rs", "fn main(){}")
.file(
"crate4/Cargo.toml",
r#"
[package]
name = "crate4"
version = "0.1.0"
edition = "2024"
"#,
);
let p = p.build();
p.cargo("run --bin ambiguous")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] `cargo run` can run at most one executable, but multiple were specified
[HELP] available targets:
bin `ambiguous` in package `crate1`
bin `ambiguous` in package `crate2`
bin `ambiguous` in package `crate3`
bin `ambiguous` in package `crate4`
"#]])
.run();
p.cargo("run --bin crate1/ambiguous")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `crate1/ambiguous` in default-run packages
[HELP] available bin targets:
ambiguous in package crate1
ambiguous in package crate2
ambiguous in package crate3
ambiguous in package crate4
"#]])
.run();
}
// See rust-lang/cargo#14544
#[cargo_test]
fn print_available_targets_within_virtual_workspace() {
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
resolver = "3"
members = ["crate1", "crate2", "pattern1", "pattern2"]
default-members = ["crate1"]
"#,
)
.file("crate1/src/main.rs", "fn main(){}")
.file(
"crate1/Cargo.toml",
r#"
[package]
name = "crate1"
version = "0.1.0"
edition = "2024"
"#,
)
.file("crate2/src/main.rs", "fn main(){}")
.file(
"crate2/Cargo.toml",
r#"
[package]
name = "crate2"
version = "0.1.0"
edition = "2024"
"#,
)
.file("pattern1/src/main.rs", "fn main(){}")
.file(
"pattern1/Cargo.toml",
r#"
[package]
name = "pattern1"
version = "0.1.0"
edition = "2024"
"#,
)
.file("pattern2/src/main.rs", "fn main(){}")
.file(
"pattern2/Cargo.toml",
r#"
[package]
name = "pattern2"
version = "0.1.0"
edition = "2024"
"#,
)
.file("another/src/main.rs", "fn main(){}")
.file(
"another/Cargo.toml",
r#"
[package]
name = "another"
version = "0.1.0"
edition = "2024"
"#,
);
let p = p.build();
p.cargo("run --bin")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] "--bin" takes one argument.
Available binaries:
crate1
"#]])
.run();
p.cargo("run -p crate1 --bin crate2")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `crate2` in `crate1` package
[HELP] a target with a similar name exists: `crate1`
[HELP] available bin in `crate2` package:
crate2
"#]])
.run();
p.cargo("check -p crate1 -p pattern1 -p pattern2 --bin crate2")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `crate2` in `crate1`, ... packages
[HELP] a target with a similar name exists: `crate1`
[HELP] available bin in `crate2` package:
crate2
"#]])
.run();
p.cargo("run --bin crate2")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `crate2` in default-run packages
[HELP] a target with a similar name exists: `crate1`
[HELP] available bin in `crate2` package:
crate2
"#]])
.run();
p.cargo("check --bin pattern*")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target matches pattern `pattern*` in default-run packages
[HELP] available bin in `pattern1` package:
pattern1
[HELP] available bin in `pattern2` package:
pattern2
"#]])
.run();
// This another branch that none of similar name exists, and print available targets in the
// default-members.
p.change_file(
"Cargo.toml",
r#"
[workspace]
resolver = "3"
members = ["crate1", "crate2", "another"]
default-members = ["another"]
"#,
);
p.cargo("run --bin crate2")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `crate2` in default-run packages
[HELP] available bin in `crate2` package:
crate2
"#]])
.run();
}
#[cargo_test]
fn either_name_or_example() {
let p = project()
@ -794,6 +1019,9 @@ fn either_name_or_example() {
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] `cargo run` can run at most one executable, but multiple were specified
[HELP] available targets:
bin `a` in package `foo`
example `b` in package `foo`
"#]])
.run();

View File

@ -2447,16 +2447,14 @@ fn bad_example() {
p.cargo("run --example foo")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no example target named `foo` in default-run packages.
[ERROR] no example target named `foo` in default-run packages
"#]])
.run();
p.cargo("run --bin foo")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `foo` in default-run packages.
[ERROR] no bin target named `foo` in default-run packages
"#]])
.run();

View File

@ -2716,152 +2716,3 @@ fn nonexistence_package_together_with_workspace() {
"#]])
.run();
}
// See rust-lang/cargo#14544
#[cargo_test]
fn print_available_targets_within_virtual_workspace() {
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
resolver = "3"
members = ["crate1", "crate2", "pattern1", "pattern2"]
default-members = ["crate1"]
"#,
)
.file("crate1/src/main.rs", "fn main(){}")
.file(
"crate1/Cargo.toml",
r#"
[package]
name = "crate1"
version = "0.1.0"
edition = "2024"
"#,
)
.file("crate2/src/main.rs", "fn main(){}")
.file(
"crate2/Cargo.toml",
r#"
[package]
name = "crate2"
version = "0.1.0"
edition = "2024"
"#,
)
.file("pattern1/src/main.rs", "fn main(){}")
.file(
"pattern1/Cargo.toml",
r#"
[package]
name = "pattern1"
version = "0.1.0"
edition = "2024"
"#,
)
.file("pattern2/src/main.rs", "fn main(){}")
.file(
"pattern2/Cargo.toml",
r#"
[package]
name = "pattern2"
version = "0.1.0"
edition = "2024"
"#,
)
.file("another/src/main.rs", "fn main(){}")
.file(
"another/Cargo.toml",
r#"
[package]
name = "another"
version = "0.1.0"
edition = "2024"
"#,
);
let p = p.build();
p.cargo("run --bin")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] "--bin" takes one argument.
Available binaries:
crate1
"#]])
.run();
p.cargo("run -p crate1 --bin crate2")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `crate2` in `crate1` package
[HELP] a target with a similar name exists: `crate1`
[HELP] Available bin in `crate2` package:
crate2
"#]])
.run();
p.cargo("check -p crate1 -p pattern1 -p pattern2 --bin crate2")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `crate2` in `crate1`, ... packages
[HELP] a target with a similar name exists: `crate1`
[HELP] Available bin in `crate2` package:
crate2
"#]])
.run();
p.cargo("run --bin crate2")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `crate2` in default-run packages
[HELP] a target with a similar name exists: `crate1`
[HELP] Available bin in `crate2` package:
crate2
"#]])
.run();
p.cargo("check --bin pattern*")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target matches pattern `pattern*` in default-run packages.
[HELP] Available bin in `pattern1` package:
pattern1
[HELP] Available bin in `pattern2` package:
pattern2
"#]])
.run();
// This another branch that none of similar name exists, and print available targets in the
// default-members.
p.change_file(
"Cargo.toml",
r#"
[workspace]
resolver = "3"
members = ["crate1", "crate2", "another"]
default-members = ["another"]
"#,
);
p.cargo("run --bin crate2")
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] no bin target named `crate2` in default-run packages.
[HELP] Available bin in `crate2` package:
crate2
"#]])
.run();
}