diff --git a/src/cargo/core/registry.rs b/src/cargo/core/registry.rs index 055b3b63ff3..e66a62f848f 100644 --- a/src/cargo/core/registry.rs +++ b/src/cargo/core/registry.rs @@ -731,10 +731,16 @@ impl<'gctx> Registry for PackageRegistry<'gctx> { kind: QueryKind, f: &mut dyn FnMut(IndexSummary), ) -> CargoResult<()> { - source - .query(dep, kind, f) - .await - .with_context(|| format!("unable to update {}", source.source_id())) + let res = source.query(dep, kind, f).await; + if let Err(e) = &res { + if dep.source_id().is_path() + && crate::core::resolver::errors::is_manifest_not_found(e) + && has_nearby_manifests(dep) + { + return Ok(()); + } + } + res.with_context(|| format!("unable to update {}", source.source_id())) .with_context(|| { format!( "failed to load source for dependency `{}`", @@ -743,6 +749,48 @@ impl<'gctx> Registry for PackageRegistry<'gctx> { }) } + /// Check if the dependency path contains subdirectories with Cargo.toml + /// files that could provide useful hints in the resolver error message. + /// This mirrors the check that `alt_paths` performs — + /// only suppress the error when RecursivePathSource would find packages. + fn has_nearby_manifests(dep: &Dependency) -> bool { + let dep_path = match dep.source_id().url().to_file_path() { + Ok(p) => p, + Err(_) => return false, + }; + + // Only check inside the dep path itself, since `alt_paths` + // searches inside `dep_path` using RecursivePathSource. + if dep_path.is_dir() { + has_cargo_toml_in_subdirs(&dep_path, 0) + } else { + false + } + } + + fn has_cargo_toml_in_subdirs(dir: &std::path::Path, depth: usize) -> bool { + if depth >= 5 { + return false; + } + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return false, + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + if path.join("Cargo.toml").exists() { + return true; + } + // Check one level deeper + if has_cargo_toml_in_subdirs(&path, depth + 1) { + return true; + } + } + } + false + } + let source = self.sources.borrow().get(dep.source_id()).cloned(); match (override_summary, source) { (Some(_), None) => { diff --git a/src/cargo/core/resolver/errors.rs b/src/cargo/core/resolver/errors.rs index cab65502f38..05ff2d2be5c 100644 --- a/src/cargo/core/resolver/errors.rs +++ b/src/cargo/core/resolver/errors.rs @@ -1,7 +1,8 @@ use std::fmt; use std::fmt::Write as _; +use std::path::{Path, PathBuf}; -use crate::core::{Dependency, PackageId, Registry, Summary}; +use crate::core::{Dependency, Package, PackageId, Registry, Summary}; use crate::sources::IndexSummary; use crate::sources::source::QueryKind; use crate::util::edit_distance::{closest, edit_distance}; @@ -93,6 +94,8 @@ pub(super) fn activation_error( ) }; + let mut is_handled_custom_path_error = false; + if !candidates.is_empty() { let mut msg = format!("failed to select a version for `{}`.", dep.package_name()); msg.push_str("\n ... required by "); @@ -360,6 +363,92 @@ pub(super) fn activation_error( "\nnote: perhaps a crate was updated and forgotten to be re-vendored?" ); } + } else if let Some(packages) = alt_paths(dep, gctx) { + is_handled_custom_path_error = true; + let path = dep.source_id().url().to_file_path().unwrap(); + let parent_root = parent.package_id().source_id().url().to_file_path().ok(); + + let rel_path = |p: &Path| -> String { + let rel = if let Some(ref base) = parent_root { + p.strip_prefix(base).unwrap_or(p) + } else { + p + }; + let mut s = rel.to_string_lossy().replace('\\', "/"); + if s.is_empty() { + s = p + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_else(|| ".".to_string()); + } + if !s.ends_with('/') { + s.push('/'); + } + s + }; + + let _ = writeln!( + &mut msg, + "no matching package named `{}` found at `{}`", + dep.package_name(), + rel_path(&path), + ); + let _ = write!( + &mut msg, + "required by {}", + describe_path_in_context(resolver_ctx, &parent.package_id()), + ); + + let mut exact_match: Option = None; + let mut found_dir: Option = None; + let mut names_found: Vec<(String, PathBuf)> = vec![]; + + for pkg in &packages { + let manifest_dir = pkg.manifest_path().parent().unwrap(); + let p_name = pkg.name().as_str(); + if p_name == dep.package_name().as_str() { + exact_match = Some(manifest_dir.to_path_buf()); + break; + } else if manifest_dir == path { + found_dir = Some(p_name.to_string()); + } else { + names_found.push((p_name.to_string(), manifest_dir.to_path_buf())); + } + } + + if let Some(dir) = exact_match { + let _ = writeln!(&mut hints); + let _ = write!( + &mut hints, + "help: package `{}` exists at `{}`", + dep.package_name(), + rel_path(&dir) + ); + } else if let Some(dir_pkg) = found_dir { + let _ = writeln!(&mut hints); + let _ = write!( + &mut hints, + "help: package `{}` exists at `{}`", + dir_pkg, + rel_path(&path) + ); + } else { + names_found.sort_by(|a, b| a.0.cmp(&b.0)); + if !names_found.is_empty() { + let _ = writeln!(&mut hints); + } + for (i, (name, p)) in names_found.iter().take(3).enumerate() { + if i > 0 { + let _ = writeln!(&mut hints); + } + let _ = write!( + &mut hints, + "help: package `{}` exists at `{}`", + name, + rel_path(p) + ); + } + } } else if let Some(name_candidates) = alt_names(registry, dep) { let name_candidates = match name_candidates { Ok(c) => c, @@ -400,16 +489,18 @@ pub(super) fn activation_error( ); } - let mut location_searched_msg = registry.describe_source(dep.source_id()); - if location_searched_msg.is_empty() { - location_searched_msg = format!("{}", dep.source_id()); + if !is_handled_custom_path_error { + let mut location_searched_msg = registry.describe_source(dep.source_id()); + if location_searched_msg.is_empty() { + location_searched_msg = format!("{}", dep.source_id()); + } + let _ = writeln!(&mut msg, "location searched: {}", location_searched_msg); + let _ = write!( + &mut msg, + "required by {}", + describe_path_in_context(resolver_ctx, &parent.package_id()), + ); } - let _ = writeln!(&mut msg, "location searched: {}", location_searched_msg); - let _ = write!( - &mut msg, - "required by {}", - describe_path_in_context(resolver_ctx, &parent.package_id()), - ); if let Some(gctx) = gctx { if let Some(offline_flag) = gctx.offline_flag() { @@ -497,6 +588,45 @@ fn alt_names( } } +/// Maybe the path dependency has packages in subdirectories or siblings? +/// We use `RecursivePathSource` to find possible alternative packages. +fn alt_paths(dep: &Dependency, gctx: Option<&GlobalContext>) -> Option> { + let gctx = gctx?; + if !dep.source_id().is_path() { + return None; + } + let path = dep.source_id().url().to_file_path().ok()?; + let mut src = crate::sources::RecursivePathSource::new(&path, dep.source_id(), gctx); + let packages = src.read_packages().ok()?; + if packages.is_empty() { + None + } else { + Some(packages) + } +} + +/// Checks if an error is a "manifest not found" error (Cargo.toml missing). +/// This is used to allow the resolver to continue and provide helpful hints +/// for path dependencies instead of failing immediately with a load error. +pub fn is_manifest_not_found(err: &anyhow::Error) -> bool { + use crate::util::errors::ManifestError; + use std::error::Error; + + for cause in err.chain() { + if let Some(manifest_err) = cause.downcast_ref::() { + if let Some(io_err) = manifest_err + .source() + .and_then(|s| s.downcast_ref::()) + { + if io_err.kind() == std::io::ErrorKind::NotFound { + return true; + } + } + } + } + false +} + /// Returns String representation of dependency chain for a particular `pkgid` /// within given context. pub(super) fn describe_path_in_context(cx: &ResolverContext, id: &PackageId) -> String { diff --git a/tests/testsuite/build.rs b/tests/testsuite/build.rs index d28efeace62..5345aec9bce 100644 --- a/tests/testsuite/build.rs +++ b/tests/testsuite/build.rs @@ -1290,9 +1290,9 @@ fn cargo_compile_with_dep_name_mismatch() { p.cargo("build") .with_status(101) .with_stderr_data(str![[r#" -[ERROR] no matching package named `notquitebar` found -location searched: [ROOT]/foo/bar +[ERROR] no matching package named `notquitebar` found at `bar/` required by package `foo v0.0.1 ([ROOT]/foo)` +[HELP] package `bar` exists at `bar/` "#]]) .run(); diff --git a/tests/testsuite/path.rs b/tests/testsuite/path.rs index 0305ea58e8a..ae4e38679ea 100644 --- a/tests/testsuite/path.rs +++ b/tests/testsuite/path.rs @@ -1669,11 +1669,9 @@ fn invalid_path_dep_in_workspace_with_lockfile() { p.cargo("check") .with_status(101) .with_stderr_data(str![[r#" -[ERROR] no matching package named `bar` found -location searched: [ROOT]/foo/foo +[ERROR] no matching package named `bar` found at `foo/` required by package `foo v0.5.0 ([ROOT]/foo/foo)` -[HELP] packages with similar names: foo - +[HELP] package `foo` exists at `foo/` "#]]) .run(); @@ -1918,3 +1916,128 @@ foo v1.0.0 ([ROOT]/foo) "#]]) .run(); } + +#[cargo_test] +fn path_dep_wrong_package_name() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2024" + [dependencies] + definitely_not_bar = { path = "bar" } + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.1.0" + edition = "2024" + "#, + ) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("check") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] no matching package named `definitely_not_bar` found at `bar/` +required by package `foo v0.1.0 ([ROOT]/foo)` +[HELP] package `bar` exists at `bar/` + +"#]]) + .run(); +} + +#[cargo_test] +fn path_dep_package_in_subdirectory() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2024" + [dependencies] + definitely_not_bar = { path = "bar" } + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/definitely_not_bar/Cargo.toml", + r#" + [package] + name = "definitely_not_bar" + version = "0.1.0" + edition = "2024" + "#, + ) + .file("bar/definitely_not_bar/src/lib.rs", "") + .build(); + + p.cargo("check") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] no matching package named `definitely_not_bar` found at `bar/` +required by package `foo v0.1.0 ([ROOT]/foo)` +[HELP] package `definitely_not_bar` exists at `bar/definitely_not_bar/` + +"#]]) + .run(); +} + +#[cargo_test] +fn path_dep_other_packages_nearby() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2024" + [dependencies] + definitely_not_bar = { path = "bar" } + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/alice/Cargo.toml", + r#" + [package] + name = "alice" + version = "0.1.0" + edition = "2024" + "#, + ) + .file("bar/alice/src/lib.rs", "") + .file( + "bar/bob/Cargo.toml", + r#" + [package] + name = "bob" + version = "0.1.0" + edition = "2024" + "#, + ) + .file("bar/bob/src/lib.rs", "") + .build(); + + p.cargo("check") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] no matching package named `definitely_not_bar` found at `bar/` +required by package `foo v0.1.0 ([ROOT]/foo)` +[HELP] package `alice` exists at `bar/alice/` +[HELP] package `bob` exists at `bar/bob/` + +"#]]) + .run(); +}