From 3a19e8d303e2091a29839ff707b9deacddee7ce9 Mon Sep 17 00:00:00 2001 From: Nathaniel McCallum Date: Wed, 3 Jun 2026 01:37:12 -0400 Subject: [PATCH 1/5] test(bindeps): cover same-package duplicate-name behavior Add regression coverage for the current invariant that multiple dependency declarations to the same package are rejected when they would require distinct dependency names. This makes the existing behavior explicit before relaxing the artifact-dependency cases tracked in #12374. --- tests/testsuite/artifact_dep.rs | 336 ++++++++++++++++++++++++++++++++ tests/testsuite/metadata.rs | 178 +++++++++++++++++ tests/testsuite/unit_graph.rs | 40 +++- 3 files changed, 553 insertions(+), 1 deletion(-) diff --git a/tests/testsuite/artifact_dep.rs b/tests/testsuite/artifact_dep.rs index 4842127bcc5..03fdcd1f4d4 100644 --- a/tests/testsuite/artifact_dep.rs +++ b/tests/testsuite/artifact_dep.rs @@ -229,6 +229,55 @@ fn disallow_artifact_and_no_artifact_dep_to_same_package_within_the_same_dep_cat .run(); } +#[cargo_test] +fn disallow_artifact_lib_and_no_artifact_dep_to_same_package_within_the_same_dep_category() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.0" + edition = "2015" + authors = [] + resolver = "2" + + [dependencies] + bar = { path = "bar/", artifact = "bin", lib = true } + bar_stable = { path = "bar/", package = "bar" } + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.5.0" + edition = "2015" + + [lib] + name = "bar" + + [[bin]] + name = "bar" + "#, + ) + .file("bar/src/lib.rs", "") + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("check -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .with_status(101) + .with_stderr_data(str![[r#" +[LOCKING] 1 package to latest compatible version +[ERROR] the crate `foo v0.0.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names + +"#]]) + .run(); +} + #[cargo_test] fn features_are_unified_among_lib_and_bin_dep_of_same_target() { let p = project() @@ -1193,6 +1242,293 @@ fn build_script_deps_adopt_do_not_allow_multiple_targets_under_different_name_an .run(); } +#[cargo_test] +fn non_build_deps_do_not_allow_multiple_targets_under_different_names() { + if cross_compile_disabled() { + return; + } + + let alternate = cross_compile::alternate(); + let native = cross_compile::native(); + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + [package] + name = "foo" + version = "0.0.0" + edition = "2015" + authors = [] + resolver = "2" + + [dependencies.bar] + path = "bar/" + artifact = "bin" + target = "{alternate}" + + [dependencies.bar-native] + package = "bar" + path = "bar/" + artifact = "bin" + target = "{native}" + "# + ), + ) + .file( + "src/lib.rs", + r#" + pub fn f() { + let _alternate = include_bytes!(env!("CARGO_BIN_FILE_BAR_bar")); + let _native = include_bytes!(env!("CARGO_BIN_FILE_BAR_NATIVE_bar")); + } + "#, + ) + .file("bar/Cargo.toml", &basic_bin_manifest("bar")) + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("check -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .with_status(101) + .with_stderr_data(str![[r#" +[LOCKING] 1 package to latest compatible version +[ERROR] the crate `foo v0.0.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names + +"#]]) + .run(); +} + +#[cargo_test] +fn build_script_deps_do_not_allow_same_target_under_different_names() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.0" + edition = "2015" + authors = [] + resolver = "2" + + [build-dependencies.bar] + path = "bar/" + artifact = "bin" + + [build-dependencies.bar-alt] + package = "bar" + path = "bar/" + artifact = "bin" + "#, + ) + .file("src/lib.rs", "") + .file( + "build.rs", + r#" + fn main() { + let bar: std::path::PathBuf = std::env::var("CARGO_BIN_FILE_BAR").expect("CARGO_BIN_FILE_BAR").into(); + assert!(bar.is_file()); + let bar_alt: std::path::PathBuf = std::env::var("CARGO_BIN_FILE_BAR_ALT_bar").expect("CARGO_BIN_FILE_BAR_ALT_bar").into(); + assert!(bar_alt.is_file()); + assert_eq!(bar_alt, bar, "same-target aliases should share the artifact path"); + } + "#, + ) + .file("bar/Cargo.toml", &basic_bin_manifest("bar")) + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("check -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .with_status(101) + .with_stderr_data(str![[r#" +[LOCKING] 1 package to latest compatible version +[ERROR] the crate `foo v0.0.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names + +"#]]) + .run(); +} + +#[cargo_test] +fn artifact_output_only_dep_cannot_coexist_with_one_artifact_lib_dep() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.0" + edition = "2015" + authors = [] + resolver = "2" + + [build-dependencies.bar-lib] + package = "bar" + path = "bar/" + artifact = "bin" + lib = true + + [build-dependencies.bar-bin] + package = "bar" + path = "bar/" + artifact = "bin" + "#, + ) + .file("src/lib.rs", "") + .file( + "build.rs", + r#" + extern crate bar_lib; + + fn main() { + bar_lib::f(); + let bar_lib_bin: std::path::PathBuf = std::env::var("CARGO_BIN_FILE_BAR_LIB_bar").expect("CARGO_BIN_FILE_BAR_LIB_bar").into(); + assert!(bar_lib_bin.is_file()); + let bar_bin: std::path::PathBuf = std::env::var("CARGO_BIN_FILE_BAR_BIN_bar").expect("CARGO_BIN_FILE_BAR_BIN_bar").into(); + assert!(bar_bin.is_file()); + } + "#, + ) + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.5.0" + edition = "2015" + + [lib] + name = "bar" + + [[bin]] + name = "bar" + "#, + ) + .file("bar/src/lib.rs", "pub fn f() {}") + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("check -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .with_status(101) + .with_stderr_data(str![[r#" +[LOCKING] 1 package to latest compatible version +[ERROR] the crate `foo v0.0.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names + +"#]]) + .run(); +} + +#[cargo_test] +fn multiple_artifact_lib_deps_to_same_package_still_error() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.0" + edition = "2015" + authors = [] + resolver = "2" + + [dependencies.bar-a] + package = "bar" + path = "bar/" + artifact = "bin" + lib = true + + [dependencies.bar-b] + package = "bar" + path = "bar/" + artifact = "bin" + lib = true + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.5.0" + edition = "2015" + + [lib] + name = "bar" + + [[bin]] + name = "bar" + "#, + ) + .file("bar/src/lib.rs", "") + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("check -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .with_status(101) + .with_stderr_data(str![[r#" +[LOCKING] 1 package to latest compatible version +[ERROR] the crate `foo v0.0.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names + +"#]]) + .run(); +} + +#[cargo_test] +fn multiple_artifact_lib_deps_with_different_artifact_targets_still_error() { + if cross_compile_disabled() { + return; + } + + let alternate = cross_compile::alternate(); + let native = cross_compile::native(); + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + [package] + name = "foo" + version = "0.0.0" + edition = "2015" + authors = [] + resolver = "2" + + [dependencies.bar] + path = "bar/" + artifact = "bin" + lib = true + target = "{alternate}" + + [dependencies.bar-native] + package = "bar" + path = "bar/" + artifact = "bin" + lib = true + target = "{native}" + "# + ), + ) + .file("src/lib.rs", "") + .file("bar/Cargo.toml", &basic_manifest("bar", "0.5.0")) + .file("bar/src/lib.rs", "pub fn doit() {}") + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("check -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .with_status(101) + .with_stderr_data(str![[r#" +[LOCKING] 1 package to latest compatible version +[ERROR] the crate `foo v0.0.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names + +"#]]) + .run(); +} + #[cargo_test] fn non_build_script_deps_adopt_specified_target_unconditionally() { if cross_compile_disabled() { diff --git a/tests/testsuite/metadata.rs b/tests/testsuite/metadata.rs index dd6f6c93188..21a3ed14f8c 100644 --- a/tests/testsuite/metadata.rs +++ b/tests/testsuite/metadata.rs @@ -2129,6 +2129,184 @@ fn cargo_metadata_with_invalid_artifact_deps() { .run(); } +#[cargo_test] +fn cargo_metadata_rejects_one_artifact_lib_alias_to_same_package() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.5.0" + + [dependencies.bar-lib] + package = "bar" + path = "bar" + artifact = "bin" + lib = true + + [dependencies.bar-bin] + package = "bar" + path = "bar" + artifact = "bin" + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.5.0" + + [lib] + name = "bar" + + [[bin]] + name = "bar" + "#, + ) + .file("bar/src/lib.rs", "") + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("metadata -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .with_status(101) + .with_stderr_data(str![[r#" +[WARNING] please specify `--format-version` flag explicitly to avoid compatibility problems +[LOCKING] 1 package to latest compatible version +[ERROR] the crate `foo v0.5.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names + +"#]]) + .run(); +} + +#[cargo_test] +fn cargo_metadata_rejects_multiple_bin_only_artifact_aliases_to_lib_package() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.5.0" + + [dependencies.bar-a] + package = "bar" + path = "bar" + artifact = "bin" + + [dependencies.bar-b] + package = "bar" + path = "bar" + artifact = "bin" + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.5.0" + + [lib] + name = "bar" + + [[bin]] + name = "bar" + "#, + ) + .file("bar/src/lib.rs", "") + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("metadata -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .with_status(101) + .with_stderr_data(str![[r#" +[WARNING] please specify `--format-version` flag explicitly to avoid compatibility problems +[LOCKING] 1 package to latest compatible version +[ERROR] the crate `foo v0.5.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names + +"#]]) + .run(); +} + +#[cargo_test] +fn cargo_metadata_rejects_mixed_artifact_and_no_artifact_dep_to_same_package() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.5.0" + + [dependencies] + bar = { path = "bar", artifact = "bin" } + bar_stable = { path = "bar", package = "bar" } + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.5.0" + + [lib] + name = "bar" + + [[bin]] + name = "bar" + "#, + ) + .file("bar/src/lib.rs", "") + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("metadata -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .with_status(101) + .with_stderr_data(str![[r#" +[WARNING] please specify `--format-version` flag explicitly to avoid compatibility problems +[LOCKING] 1 package to latest compatible version +[ERROR] the crate `foo v0.5.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names + +"#]]) + .run(); +} + +#[cargo_test] +fn cargo_metadata_allows_mixed_artifact_and_no_artifact_dep_to_bin_only_package() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.5.0" + + [dependencies] + bar = { path = "bar", artifact = "bin" } + bar_stable = { path = "bar", package = "bar" } + "#, + ) + .file("src/lib.rs", "") + .file("bar/Cargo.toml", &basic_bin_manifest("bar")) + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("metadata -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .with_stdout_contains(r#"[..]"rename":"bar_stable"[..]"#) + .with_stdout_contains(r#"[..]"deps":[{"name":"","pkg":"path+[ROOTURL]/foo/bar#0.5.0"[..]"#) + .run(); +} + #[cargo_test] fn cargo_metadata_with_invalid_duplicate_renamed_deps() { let p = project() diff --git a/tests/testsuite/unit_graph.rs b/tests/testsuite/unit_graph.rs index cead13bc6e8..371efdfd7f8 100644 --- a/tests/testsuite/unit_graph.rs +++ b/tests/testsuite/unit_graph.rs @@ -1,9 +1,9 @@ //! Tests for --unit-graph option. use crate::prelude::*; -use cargo_test_support::project; use cargo_test_support::registry::Package; use cargo_test_support::str; +use cargo_test_support::{basic_bin_manifest, project}; #[cargo_test] fn gated() { @@ -237,3 +237,41 @@ fn simple() { ) .run(); } + +#[cargo_test] +fn unit_graph_rejects_artifact_alias_edges_to_same_package() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + authors = [] + + [dependencies.bar] + path = "bar/" + artifact = "bin" + + [dependencies.bar-alt] + package = "bar" + path = "bar/" + artifact = "bin" + "#, + ) + .file("src/lib.rs", "") + .file("bar/Cargo.toml", &basic_bin_manifest("bar")) + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("build --unit-graph -Zunstable-options -Z bindeps") + .masquerade_as_nightly_cargo(&["unit-graph", "bindeps"]) + .with_status(101) + .with_stderr_data(str![[r#" +[LOCKING] 1 package to latest compatible version +[ERROR] the crate `foo v0.1.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names + +"#]]) + .run(); +} From bd55374243b3bb0dda318a8ed7fddff3bd5a9106 Mon Sep 17 00:00:00 2001 From: Nathaniel McCallum Date: Fri, 26 Jun 2026 12:53:44 -0400 Subject: [PATCH 2/5] refactor(resolve): share dependency name helpers Extract dependency-name calculation so artifact alias handling can use the same extern crate naming rules in build graph lowering and metadata output. --- src/cargo/core/resolver/resolve.rs | 72 +++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/src/cargo/core/resolver/resolve.rs b/src/cargo/core/resolver/resolve.rs index 3e3b8a5e98e..c865fb770f4 100644 --- a/src/cargo/core/resolver/resolve.rs +++ b/src/cargo/core/resolver/resolve.rs @@ -419,12 +419,19 @@ unable to verify that `{0}` is the same as when the lockfile was generated self.dependencies_listed(from, to) }; - let target_crate_name = || (to_target.crate_name(), None); - let mut name_pairs = deps.iter().map(|d| { - d.explicit_name_in_toml() - .map(|s| (s.as_str().replace("-", "_"), Some(s))) - .unwrap_or_else(target_crate_name) - }); + Self::extern_crate_name_and_dep_name_from_deps(from, to, deps.iter(), to_target) + } + + fn extern_crate_name_and_dep_name_from_deps<'a>( + from: PackageId, + to: PackageId, + deps: impl IntoIterator, + to_target: &Target, + ) -> CargoResult<(InternedString, Option)> { + let target_crate_name = || (to_target.crate_name().into(), None); + let mut name_pairs = deps + .into_iter() + .map(|d| Self::dep_extern_crate_name_and_dep_name(d, to_target)); let (extern_crate_name, dep_name) = name_pairs.next().unwrap_or_else(target_crate_name); for (n, _) in name_pairs { anyhow::ensure!( @@ -434,7 +441,58 @@ unable to verify that `{0}` is the same as when the lockfile was generated to, ); } - Ok((extern_crate_name.into(), dep_name)) + Ok((extern_crate_name, dep_name)) + } + + pub fn dep_extern_crate_name_and_dep_name( + dep: &Dependency, + to_target: &Target, + ) -> (InternedString, Option) { + dep.explicit_name_in_toml() + .map(|name| (name.as_str().replace('-', "_").into(), Some(name))) + .unwrap_or_else(|| (to_target.crate_name().into(), None)) + } + + pub fn lib_dependency_name<'a>( + &self, + from: PackageId, + to: PackageId, + deps: impl IntoIterator, + to_target: &Target, + ) -> CargoResult)>> { + let deps = deps.into_iter().collect::>(); + if !deps.iter().all(|dep| dep.artifact().is_some()) { + return Self::extern_crate_name_and_dep_name_from_deps( + from, + to, + deps.into_iter(), + to_target, + ) + .map(Some); + } + + let mut names = deps + .into_iter() + .filter(|dep| dep.artifact().is_some_and(|artifact| artifact.is_lib())) + .map(|dep| Self::dep_extern_crate_name_and_dep_name(dep, to_target)) + .collect::>(); + + if names.is_empty() { + return Ok(None); + } + + names.sort(); + names.dedup(); + let (extern_crate_name, dep_name) = names[0]; + for (name, _) in &names[1..] { + anyhow::ensure!( + *name == extern_crate_name, + "the crate `{}` depends on crate `{}` multiple times with different names", + from, + to, + ); + } + Ok(Some((extern_crate_name, dep_name))) } fn dependencies_listed(&self, from: PackageId, to: PackageId) -> &HashSet { From b56c5a7550665db48ef02665aae853f22af9262f Mon Sep 17 00:00:00 2001 From: Nathaniel McCallum Date: Fri, 26 Jun 2026 12:55:13 -0400 Subject: [PATCH 3/5] feat(bindeps): allow artifact aliases in unit deps Permit all-artifact aliases to the same package to keep their dependency-specific names when lowering build and unit graph edges. Mixed artifact and non-artifact dependencies still use the existing duplicate-name check so library dependencies have one extern crate name. --- src/cargo/core/compiler/unit_dependencies.rs | 89 ++++++++++--- tests/testsuite/artifact_dep.rs | 128 +++++++++++-------- tests/testsuite/unit_graph.rs | 52 +++++--- 3 files changed, 178 insertions(+), 91 deletions(-) diff --git a/src/cargo/core/compiler/unit_dependencies.rs b/src/cargo/core/compiler/unit_dependencies.rs index 2d0ea7c3c62..3f4f7f40ce5 100644 --- a/src/cargo/core/compiler/unit_dependencies.rs +++ b/src/cargo/core/compiler/unit_dependencies.rs @@ -310,6 +310,16 @@ fn compute_deps( let dep_unit_for = unit_for.with_dependency(unit, dep_lib, unit_for.root_compile_kind()); let manifest_deps = deps.iter().map(|d| (*d).clone()).collect::>(); + let lib_dep_name = if deps.iter().any(|dep| dep.artifact().is_some()) { + state.resolve().lib_dependency_name( + unit.pkg.package_id(), + dep_pkg_id, + deps.iter().copied(), + dep_lib, + )? + } else { + None + }; let start = ret.len(); if state.gctx.cli_unstable().dual_proc_macros @@ -322,6 +332,7 @@ fn compute_deps( dep_pkg, dep_lib, Some(manifest_deps.clone()), + lib_dep_name, dep_unit_for, unit.kind, mode, @@ -334,6 +345,7 @@ fn compute_deps( dep_pkg, dep_lib, Some(manifest_deps), + lib_dep_name, dep_unit_for, CompileKind::Host, mode, @@ -347,6 +359,7 @@ fn compute_deps( dep_pkg, dep_lib, Some(manifest_deps), + lib_dep_name, dep_unit_for, unit.kind.for_target(dep_lib), mode, @@ -426,6 +439,7 @@ fn compute_deps( &unit.pkg, t, None, // artificial + None, UnitFor::new_normal(unit_for.root_compile_kind()), unit.kind.for_target(t), CompileMode::Build, @@ -452,6 +466,7 @@ fn calc_artifact_deps<'a>( let mut has_artifact_lib = false; let mut maybe_non_artifact_lib = false; let artifact_pkg = state.get(dep_id); + let all_artifact = deps.iter().all(|d| d.artifact().is_some()); for dep in deps { let Some(artifact) = dep.artifact() else { maybe_non_artifact_lib = true; @@ -478,6 +493,7 @@ fn calc_artifact_deps<'a>( .unwrap_or(unit.kind), artifact_pkg, dep, + all_artifact.then_some(*dep), )?); } } @@ -522,6 +538,7 @@ fn compute_deps_custom_build( &unit.pkg, &unit.target, None, // artificial + None, script_unit_for, // Build scripts always compiled for the host. CompileKind::Host, @@ -546,6 +563,7 @@ fn compute_deps_custom_build( let root_unit_compile_target = unit_for.root_compile_kind(); let unit_for = UnitFor::new_host(/*host_features*/ true, root_unit_compile_target); for (dep_pkg_id, deps) in state.deps(unit, script_unit_for) { + let all_artifact = deps.iter().all(|d| d.artifact().is_some()); for dep in deps { if dep.kind() != DepKind::Build || dep.artifact().is_none() { continue; @@ -565,6 +583,7 @@ fn compute_deps_custom_build( resolved_artifact_compile_kind.unwrap_or(CompileKind::Host), artifact_pkg, dep, + all_artifact.then_some(dep), )?); } } @@ -589,6 +608,7 @@ fn artifact_targets_to_unit_deps( compile_kind: CompileKind, artifact_pkg: &Package, dep: &Dependency, + naming_dep: Option<&Dependency>, ) -> CargoResult> { let ret = match_artifacts_kind_with_targets(dep, artifact_pkg.targets(), parent.pkg.name().as_str())? @@ -608,14 +628,18 @@ fn artifact_targets_to_unit_deps( _ => false, }) .map(|target_kind| { + let mut target = target.clone(); + target.set_kind(TargetKind::Lib(vec![target_kind.clone()])); + let name_override = naming_dep.map(|dep| { + Resolve::dep_extern_crate_name_and_dep_name(dep, &target) + }); new_unit_dep( state, parent, artifact_pkg, - target - .clone() - .set_kind(TargetKind::Lib(vec![target_kind.clone()])), + &target, None, // TBD + name_override, parent_unit_for, compile_kind, CompileMode::Build, @@ -623,17 +647,22 @@ fn artifact_targets_to_unit_deps( ) }), ) as Box>, - _ => Box::new(std::iter::once(new_unit_dep( - state, - parent, - artifact_pkg, - target, - None, // TBD - parent_unit_for, - compile_kind, - CompileMode::Build, - dep.artifact(), - ))), + _ => { + let name_override = naming_dep + .map(|dep| Resolve::dep_extern_crate_name_and_dep_name(dep, target)); + Box::new(std::iter::once(new_unit_dep( + state, + parent, + artifact_pkg, + target, + None, // TBD + name_override, + parent_unit_for, + compile_kind, + CompileMode::Build, + dep.artifact(), + ))) + } } }) .collect::, _>>()?; @@ -659,12 +688,23 @@ fn compute_deps_doc( // However, for plugins/proc macros, deps should be built like normal. let mode = check_or_build_mode(unit.mode, dep_lib); let dep_unit_for = unit_for.with_dependency(unit, dep_lib, unit_for.root_compile_kind()); + let lib_dep_name = if deps.iter().any(|dep| dep.artifact().is_some()) { + state.resolve().lib_dependency_name( + unit.pkg.package_id(), + id, + deps.iter().copied(), + dep_lib, + )? + } else { + None + }; let lib_unit_dep = new_unit_dep( state, unit, dep_pkg, dep_lib, None, // not checking unused deps + lib_dep_name, dep_unit_for, unit.kind.for_target(dep_lib), mode, @@ -679,6 +719,7 @@ fn compute_deps_doc( dep_pkg, dep_lib, None, // not checking unused deps + lib_dep_name, dep_unit_for, unit.kind.for_target(dep_lib), unit.mode, @@ -713,6 +754,7 @@ fn compute_deps_doc( &unit.pkg, lib, None, // not checking unused deps + None, dep_unit_for, unit.kind.for_target(lib), unit.mode, @@ -733,6 +775,7 @@ fn compute_deps_doc( &scrape_unit.pkg, &scrape_unit.target, None, // not checking unused deps + None, scrape_unit_for, scrape_unit.kind, scrape_unit.mode, @@ -762,6 +805,7 @@ fn maybe_lib( &unit.pkg, t, None, + None, dep_unit_for, unit.kind.for_target(t), mode, @@ -824,6 +868,7 @@ fn dep_build_script( &unit.pkg, t, None, // artificial + None, script_unit_for, unit.kind, CompileMode::RunCustomBuild, @@ -861,6 +906,7 @@ fn new_unit_dep( pkg: &Package, target: &Target, manifest_deps: Option>, + name_override: Option<(InternedString, Option)>, unit_for: UnitFor, kind: CompileKind, mode: CompileMode, @@ -880,6 +926,7 @@ fn new_unit_dep( pkg, target, manifest_deps, + name_override, unit_for, kind, mode, @@ -894,17 +941,21 @@ fn new_unit_dep_with_profile( pkg: &Package, target: &Target, manifest_deps: Option>, + name_override: Option<(InternedString, Option)>, unit_for: UnitFor, kind: CompileKind, mode: CompileMode, profile: Profile, artifact: Option<&Artifact>, ) -> CargoResult { - let (extern_crate_name, dep_name) = state.resolve().extern_crate_name_and_dep_name( - parent.pkg.package_id(), - pkg.package_id(), - target, - )?; + let (extern_crate_name, dep_name) = match name_override { + Some(name) => name, + None => state.resolve().extern_crate_name_and_dep_name( + parent.pkg.package_id(), + pkg.package_id(), + target, + )?, + }; let public = state .resolve() .is_public_dep(parent.pkg.package_id(), pkg.package_id()); diff --git a/tests/testsuite/artifact_dep.rs b/tests/testsuite/artifact_dep.rs index 03fdcd1f4d4..354b82f691c 100644 --- a/tests/testsuite/artifact_dep.rs +++ b/tests/testsuite/artifact_dep.rs @@ -1182,9 +1182,8 @@ fn build_script_deps_adopt_specified_target_unconditionally() { .run(); } -/// inverse RFC-3176 #[cargo_test] -fn build_script_deps_adopt_do_not_allow_multiple_targets_under_different_name_and_same_version() { +fn build_script_deps_allow_multiple_targets_under_different_names() { if cross_compile_disabled() { return; } @@ -1203,16 +1202,9 @@ fn build_script_deps_adopt_do_not_allow_multiple_targets_under_different_name_an authors = [] resolver = "2" - [build-dependencies.bar] - path = "bar/" - artifact = "bin" - target = "{}" - - [build-dependencies.bar-native] - package = "bar" - path = "bar/" - artifact = "bin" - target = "{}" + [build-dependencies] + bar = {{ path = "bar/", artifact = "bin", target = "{}" }} + bar-native = {{ package = "bar", path = "bar/", artifact = "bin", target = "{}" }} "#, alternate, native @@ -1233,17 +1225,14 @@ fn build_script_deps_adopt_do_not_allow_multiple_targets_under_different_name_an p.cargo("check -v -Z bindeps") .masquerade_as_nightly_cargo(&["bindeps"]) - .with_status(101) - .with_stderr_data(str![[r#" -[LOCKING] 1 package to latest compatible version -[ERROR] the crate `foo v0.0.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names - -"#]]) + .with_stderr_contains( + "[RUNNING] `rustc --crate-name bar --edition=2015 bar/src/main.rs [..]--target [ALT_TARGET] [..]", + ) .run(); } #[cargo_test] -fn non_build_deps_do_not_allow_multiple_targets_under_different_names() { +fn non_build_deps_allow_multiple_targets_under_different_names() { if cross_compile_disabled() { return; } @@ -1262,16 +1251,9 @@ fn non_build_deps_do_not_allow_multiple_targets_under_different_names() { authors = [] resolver = "2" - [dependencies.bar] - path = "bar/" - artifact = "bin" - target = "{alternate}" - - [dependencies.bar-native] - package = "bar" - path = "bar/" - artifact = "bin" - target = "{native}" + [dependencies] + bar = {{ path = "bar/", artifact = "bin", target = "{alternate}" }} + bar-native = {{ package = "bar", path = "bar/", artifact = "bin", target = "{native}" }} "# ), ) @@ -1288,19 +1270,16 @@ fn non_build_deps_do_not_allow_multiple_targets_under_different_names() { .file("bar/src/main.rs", "fn main() {}") .build(); - p.cargo("check -Z bindeps") + p.cargo("check -v -Z bindeps") .masquerade_as_nightly_cargo(&["bindeps"]) - .with_status(101) - .with_stderr_data(str![[r#" -[LOCKING] 1 package to latest compatible version -[ERROR] the crate `foo v0.0.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names - -"#]]) + .with_stderr_contains( + "[RUNNING] `rustc --crate-name bar --edition=2015 bar/src/main.rs [..]--target [ALT_TARGET] [..]", + ) .run(); } #[cargo_test] -fn build_script_deps_do_not_allow_same_target_under_different_names() { +fn build_script_deps_allow_same_target_under_different_names() { let p = project() .file( "Cargo.toml", @@ -1312,14 +1291,9 @@ fn build_script_deps_do_not_allow_same_target_under_different_names() { authors = [] resolver = "2" - [build-dependencies.bar] - path = "bar/" - artifact = "bin" - - [build-dependencies.bar-alt] - package = "bar" - path = "bar/" - artifact = "bin" + [build-dependencies] + bar = { path = "bar/", artifact = "bin" } + bar-alt = { package = "bar", path = "bar/", artifact = "bin" } "#, ) .file("src/lib.rs", "") @@ -1341,17 +1315,21 @@ fn build_script_deps_do_not_allow_same_target_under_different_names() { p.cargo("check -Z bindeps") .masquerade_as_nightly_cargo(&["bindeps"]) - .with_status(101) - .with_stderr_data(str![[r#" + .with_stderr_data( + str![[r#" [LOCKING] 1 package to latest compatible version -[ERROR] the crate `foo v0.0.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names +[COMPILING] bar v0.5.0 ([ROOT]/foo/bar) +[COMPILING] foo v0.0.0 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s -"#]]) +"#]] + .unordered(), + ) .run(); } #[cargo_test] -fn artifact_output_only_dep_cannot_coexist_with_one_artifact_lib_dep() { +fn artifact_output_only_dep_can_coexist_with_one_artifact_lib_dep() { let p = project() .file( "Cargo.toml", @@ -1411,12 +1389,6 @@ fn artifact_output_only_dep_cannot_coexist_with_one_artifact_lib_dep() { p.cargo("check -Z bindeps") .masquerade_as_nightly_cargo(&["bindeps"]) - .with_status(101) - .with_stderr_data(str![[r#" -[LOCKING] 1 package to latest compatible version -[ERROR] the crate `foo v0.0.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names - -"#]]) .run(); } @@ -1529,6 +1501,50 @@ fn multiple_artifact_lib_deps_with_different_artifact_targets_still_error() { .run(); } +#[cargo_test] +fn multiple_artifact_aliases_no_op_rebuild() { + if cross_compile_disabled() { + return; + } + + let alternate = cross_compile::alternate(); + let native = cross_compile::native(); + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + [package] + name = "foo" + version = "0.0.0" + edition = "2015" + authors = [] + resolver = "2" + + [build-dependencies] + bar = {{ path = "bar/", artifact = "bin", target = "{alternate}" }} + bar-native = {{ package = "bar", path = "bar/", artifact = "bin", target = "{native}" }} + "# + ), + ) + .file("src/lib.rs", "") + .file("build.rs", "fn main() {}") + .file("bar/Cargo.toml", &basic_bin_manifest("bar")) + .file("bar/src/main.rs", "fn main() {}") + .build(); + + p.cargo("check -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .run(); + p.cargo("check -Z bindeps") + .masquerade_as_nightly_cargo(&["bindeps"]) + .with_stderr_data(str![[r#" +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + #[cargo_test] fn non_build_script_deps_adopt_specified_target_unconditionally() { if cross_compile_disabled() { diff --git a/tests/testsuite/unit_graph.rs b/tests/testsuite/unit_graph.rs index 371efdfd7f8..e2f5c22de50 100644 --- a/tests/testsuite/unit_graph.rs +++ b/tests/testsuite/unit_graph.rs @@ -239,7 +239,7 @@ fn simple() { } #[cargo_test] -fn unit_graph_rejects_artifact_alias_edges_to_same_package() { +fn artifact_alias_edges() { let p = project() .file( "Cargo.toml", @@ -250,14 +250,9 @@ fn unit_graph_rejects_artifact_alias_edges_to_same_package() { edition = "2015" authors = [] - [dependencies.bar] - path = "bar/" - artifact = "bin" - - [dependencies.bar-alt] - package = "bar" - path = "bar/" - artifact = "bin" + [dependencies] + bar = { path = "bar/", artifact = "bin" } + bar-alt = { package = "bar", path = "bar/", artifact = "bin" } "#, ) .file("src/lib.rs", "") @@ -265,13 +260,38 @@ fn unit_graph_rejects_artifact_alias_edges_to_same_package() { .file("bar/src/main.rs", "fn main() {}") .build(); - p.cargo("build --unit-graph -Zunstable-options -Z bindeps") + let graph = p + .cargo("build --unit-graph -Zunstable-options -Z bindeps") .masquerade_as_nightly_cargo(&["unit-graph", "bindeps"]) - .with_status(101) - .with_stderr_data(str![[r#" -[LOCKING] 1 package to latest compatible version -[ERROR] the crate `foo v0.1.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names + .run_json(); -"#]]) - .run(); + let units = graph["units"].as_array().unwrap(); + let artifact_index = units + .iter() + .position(|unit| unit["target"]["name"] == "bar") + .unwrap(); + let root_unit = units + .iter() + .find(|unit| unit["target"]["name"] == "foo") + .unwrap(); + let mut aliases = root_unit["dependencies"] + .as_array() + .unwrap() + .iter() + .map(|dep| { + ( + dep["extern_crate_name"].as_str().unwrap().to_owned(), + dep["index"].as_u64().unwrap(), + ) + }) + .collect::>(); + aliases.sort(); + + assert_eq!( + aliases, + vec![ + ("bar".to_owned(), artifact_index as u64), + ("bar_alt".to_owned(), artifact_index as u64), + ] + ); } From 4bc23fc9b67330f2158d9776d474baf26c313d43 Mon Sep 17 00:00:00 2001 From: Nathaniel McCallum Date: Fri, 26 Jun 2026 12:55:22 -0400 Subject: [PATCH 4/5] feat(metadata): report artifact alias names Use the shared dependency-name rules when emitting cargo metadata for artifact aliases to the same package. Bin-only artifact aliases emit dependency-specific extern names, while lib aliases continue to require a single library dependency name. --- src/cargo/ops/cargo_output_metadata.rs | 36 ++++++++++++++++---------- tests/testsuite/metadata.rs | 29 ++++++++++----------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/cargo/ops/cargo_output_metadata.rs b/src/cargo/ops/cargo_output_metadata.rs index 929a4892e97..cca9b158b72 100644 --- a/src/cargo/ops/cargo_output_metadata.rs +++ b/src/cargo/ops/cargo_output_metadata.rs @@ -246,14 +246,11 @@ fn build_resolve_graph_r( let targets = package_map[&dep_id].targets(); - // Try to get the extern name for lib, or crate name for bins. - let extern_name = |target| { - resolve - .extern_crate_name_and_dep_name(pkg_id, dep_id, target) - .map(|(ext_crate_name, _)| ext_crate_name) - }; - let lib_target = targets.iter().find(|t| t.is_lib()); + let lib_dep_name = lib_target + .map(|target| resolve.lib_dependency_name(pkg_id, dep_id, deps.iter(), target)) + .transpose()? + .flatten(); for dep in deps.iter() { if let Some(target) = lib_target { @@ -269,7 +266,7 @@ fn build_resolve_graph_r( // if the user is not using -Zbindeps. // Remove this condition ` after -Zbindeps gets stabilized. let extern_name = if dep.artifact().is_some() { - Some(extern_name(target)?) + Some(Resolve::dep_extern_crate_name_and_dep_name(dep, target).0) } else { None }; @@ -308,7 +305,9 @@ fn build_resolve_graph_r( dep_kinds.push(DepKindInfo { kind: dep.kind(), target: dep.platform().cloned(), - extern_name: extern_name(target).ok(), + extern_name: Some( + Resolve::dep_extern_crate_name_and_dep_name(dep, target).0, + ), artifact: Some(kind.crate_type()), compile_target, bin_name: target.is_bin().then(|| target.name().to_string()), @@ -320,15 +319,24 @@ fn build_resolve_graph_r( let pkg_id = normalize_id(dep_id); - let dep = match (lib_target, dep_kinds.len()) { - (Some(target), _) => Dep { - name: extern_name(target)?, + let dep = match (lib_target, dep_kinds.len(), lib_dep_name) { + (Some(_), _, Some((name, _))) => Dep { + name, + pkg: pkg_id.to_spec(), + pkg_id, + dep_kinds, + }, + (Some(_), 1.., None) => Dep { + name: "".into(), pkg: pkg_id.to_spec(), pkg_id, dep_kinds, }, + // All active artifact deps emit at least one dep_kind or error while + // matching targets, but keep this arm explicit for exhaustiveness. + (Some(_), 0, None) => continue, // No lib target exists but contains artifact deps. - (None, 1..) => Dep { + (None, 1.., _) => Dep { name: "".into(), pkg: pkg_id.to_spec(), pkg_id, @@ -336,7 +344,7 @@ fn build_resolve_graph_r( }, // No lib or artifact dep exists. // Usually this mean parent depending on non-lib bin crate. - (None, _) => continue, + (None, _, _) => continue, }; dep_metadatas.push(dep) diff --git a/tests/testsuite/metadata.rs b/tests/testsuite/metadata.rs index 21a3ed14f8c..83d530a6861 100644 --- a/tests/testsuite/metadata.rs +++ b/tests/testsuite/metadata.rs @@ -2130,7 +2130,7 @@ fn cargo_metadata_with_invalid_artifact_deps() { } #[cargo_test] -fn cargo_metadata_rejects_one_artifact_lib_alias_to_same_package() { +fn cargo_metadata_with_one_artifact_lib_alias_to_same_package() { let p = project() .file( "Cargo.toml", @@ -2172,18 +2172,16 @@ fn cargo_metadata_rejects_one_artifact_lib_alias_to_same_package() { p.cargo("metadata -Z bindeps") .masquerade_as_nightly_cargo(&["bindeps"]) - .with_status(101) - .with_stderr_data(str![[r#" -[WARNING] please specify `--format-version` flag explicitly to avoid compatibility problems -[LOCKING] 1 package to latest compatible version -[ERROR] the crate `foo v0.5.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names - -"#]]) + .with_stdout_contains(r#"[..]"name":"bar_lib","pkg":"path+[ROOTURL]/foo/bar#0.5.0"[..]"#) + .with_stdout_contains(r#"[..]"rename":"bar-lib"[..]"#) + .with_stdout_contains(r#"[..]"rename":"bar-bin"[..]"#) + .with_stdout_contains(r#"[..]"extern_name":"bar_lib"[..]"#) + .with_stdout_contains(r#"[..]"extern_name":"bar_bin"[..]"#) .run(); } #[cargo_test] -fn cargo_metadata_rejects_multiple_bin_only_artifact_aliases_to_lib_package() { +fn cargo_metadata_with_multiple_bin_only_artifact_aliases_to_lib_package() { let p = project() .file( "Cargo.toml", @@ -2224,13 +2222,12 @@ fn cargo_metadata_rejects_multiple_bin_only_artifact_aliases_to_lib_package() { p.cargo("metadata -Z bindeps") .masquerade_as_nightly_cargo(&["bindeps"]) - .with_status(101) - .with_stderr_data(str![[r#" -[WARNING] please specify `--format-version` flag explicitly to avoid compatibility problems -[LOCKING] 1 package to latest compatible version -[ERROR] the crate `foo v0.5.0 ([ROOT]/foo)` depends on crate `bar v0.5.0 ([ROOT]/foo/bar)` multiple times with different names - -"#]]) + .with_stdout_contains(r#"[..]"rename":"bar-a"[..]"#) + .with_stdout_contains(r#"[..]"rename":"bar-b"[..]"#) + .with_stdout_contains(r#"[..]"extern_name":"bar_a"[..]"#) + .with_stdout_contains(r#"[..]"extern_name":"bar_b"[..]"#) + .with_stdout_contains(r#"[..]"dependencies":["path+[ROOTURL]/foo/bar#0.5.0"][..]"#) + .with_stdout_contains(r#"[..]"deps":[{"name":"","pkg":"path+[ROOTURL]/foo/bar#0.5.0"[..]"#) .run(); } From 55cb07889c8a30bedde7e2703527430acd9ff39a Mon Sep 17 00:00:00 2001 From: Nathaniel McCallum Date: Fri, 26 Jun 2026 12:55:28 -0400 Subject: [PATCH 5/5] docs(bindeps): show artifact aliases per target Document declaring separate aliases when building artifacts from the same package for multiple target platforms. Also call out that multiple lib=true aliases must agree on one library extern crate name. --- src/doc/src/reference/unstable.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 06eaf2943f2..e4c4e76b9fb 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -1024,6 +1024,15 @@ Artifact-dependencies adds the following keys to a dependency declaration in `Ca zoo = { version = "1.0", artifact = ["bin:cat", "bin:dog"]} ``` + To build artifacts from the same package for multiple target platforms, + declare each alias separately: + + ```toml + [build-dependencies] + firmware-x86 = { package = "firmware", version = "1.0", artifact = "bin", target = "x86_64-unknown-none" } + firmware-arm = { package = "firmware", version = "1.0", artifact = "bin", target = "thumbv7em-none-eabihf" } + ``` + - `lib` --- This is a Boolean value which indicates whether or not to also build the dependency's library as a normal Rust `lib` dependency. This field can only be specified when `artifact` is specified. @@ -1038,6 +1047,10 @@ Artifact-dependencies adds the following keys to a dependency declaration in `Ca bar = { version = "1.0", artifact = "bin", lib = true } ``` + If multiple aliases to the same package set `lib = true`, Cargo must be + able to refer to the package's library with a single Rust extern crate name. + Aliases that would give that library different names are rejected. + - `target` --- The platform target to build the dependency for. This field can only be specified when `artifact` is specified.