Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 52 additions & 4 deletions src/cargo/core/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Comment on lines +734 to +743
Copy link
Copy Markdown
Contributor

@epage epage Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has really ballooned in complexity, including the idea of "catching" specific errors.

View changes since the review

Copy link
Copy Markdown
Contributor Author

@raushan728 raushan728 Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The registry.rs change was needed because without it, the IO error fires before the resolver reaches the diagnostic code for cases 2/3.

Is there a cleaner way you'd suggest to intercept this early failure so we can still provide helpful hints for those cases? Happy to rework the approach if you have a better path in mind.

.with_context(|| {
format!(
"failed to load source for dependency `{}`",
Expand All @@ -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) => {
Expand Down
150 changes: 140 additions & 10 deletions src/cargo/core/resolver/errors.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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 ");
Expand Down Expand Up @@ -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<PathBuf> = None;
let mut found_dir: Option<String> = 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,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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<Vec<Package>> {
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::<ManifestError>() {
if let Some(io_err) = manifest_err
.source()
.and_then(|s| s.downcast_ref::<std::io::Error>())
{
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 {
Expand Down
4 changes: 2 additions & 2 deletions tests/testsuite/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading