diff --git a/src/cargo/core/resolver/errors.rs b/src/cargo/core/resolver/errors.rs index cab65502f38..6164474e27c 100644 --- a/src/cargo/core/resolver/errors.rs +++ b/src/cargo/core/resolver/errors.rs @@ -1,5 +1,6 @@ use std::fmt; use std::fmt::Write as _; +use std::path::PathBuf; use crate::core::{Dependency, PackageId, Registry, Summary}; use crate::sources::IndexSummary; @@ -360,6 +361,51 @@ 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) { + let path = dep.source_id().url().to_file_path().unwrap(); + let _ = writeln!( + &mut msg, + "no matching package named `{}` found", + dep.package_name() + ); + + 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())); + } + } + + let mut add_hint = |name: &str, p: &std::path::Path| { + let _ = writeln!(&mut hints); + let _ = write!( + &mut hints, + "help: package `{}` exists at `{}`", + name, + p.display() + ); + }; + + if let Some(dir) = exact_match { + add_hint(dep.package_name().as_str(), &dir); + } else if let Some(dir_pkg) = found_dir { + add_hint(&dir_pkg, &path); + } else { + names_found.sort_by(|a, b| a.0.cmp(&b.0)); + for (name, p) in names_found.iter() { + add_hint(name, p); + } + } } else if let Some(name_candidates) = alt_names(registry, dep) { let name_candidates = match name_candidates { Ok(c) => c, @@ -497,6 +543,29 @@ fn alt_names( } } +/// For path dependencies, scan the dependency directory for any packages +/// that exist in subdirectories. This helps when the user points to a +/// directory without a Cargo.toml or with the wrong package name. +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()?; + if !path.is_dir() { + return None; + } + use crate::sources::path::RecursivePathSource; + let source_id = dep.source_id(); + let mut source = RecursivePathSource::new(&path, source_id, gctx); + let packages = source.read_packages().ok()?; + if packages.is_empty() { + None + } else { + Some(packages) + } +} + /// 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/src/cargo/sources/path.rs b/src/cargo/sources/path.rs index 78dbe2027ee..bf968b8d760 100644 --- a/src/cargo/sources/path.rs +++ b/src/cargo/sources/path.rs @@ -36,8 +36,8 @@ pub struct PathSource<'gctx> { source_id: SourceId, /// The root path of this source. path: PathBuf, - /// Packages that this sources has discovered. - package: RefCell>, + /// The package discovered in this source, if any. + package: RefCell>>, gctx: &'gctx GlobalContext, } @@ -63,23 +63,23 @@ impl<'gctx> PathSource<'gctx> { Self { source_id, path, - package: RefCell::new(Some(pkg)), + package: RefCell::new(Some(Some(pkg))), gctx, } } - /// Gets the package on the root path. + /// Returns the root package, or an error if it is missing or failed to load. pub fn root_package(&mut self) -> CargoResult { trace!("root_package; source={:?}", self); self.load()?; match &*self.package.borrow() { - Some(pkg) => Ok(pkg.clone()), - None => Err(internal(format!( - "no package found in source {:?}", - self.path - ))), + Some(Some(pkg)) => Ok(pkg.clone()), + Some(None) | None => Err(anyhow::format_err!( + "failed to read `{}`", + self.path.join("Cargo.toml").display() + )), } } @@ -124,10 +124,14 @@ impl<'gctx> PathSource<'gctx> { Ok(()) } - fn read_package(&self) -> CargoResult { + /// Reads the manifest. Returning `Ok(None)` if missing allows the resolver + /// to handle it as "not found" instead of an early IO error. + fn read_package(&self) -> CargoResult> { let path = self.path.join("Cargo.toml"); - let pkg = ops::read_package(&path, self.source_id, self.gctx)?; - Ok(pkg) + if !path.exists() { + return Ok(None); + } + Ok(Some(ops::read_package(&path, self.source_id, self.gctx)?)) } } @@ -146,7 +150,8 @@ impl<'gctx> Source for PathSource<'gctx> { f: &mut dyn FnMut(IndexSummary), ) -> CargoResult<()> { self.load()?; - if let Some(s) = self.package.borrow().as_ref().map(|p| p.summary()) { + if let Some(Some(p)) = &*self.package.borrow() { + let s = p.summary(); let matched = match kind { QueryKind::Exact | QueryKind::RejectedVersions => dep.matches(s), QueryKind::AlternativeNames => true, @@ -175,7 +180,10 @@ impl<'gctx> Source for PathSource<'gctx> { trace!("getting packages; id={}", id); self.load()?; let pkg = self.package.borrow(); - let pkg = pkg.iter().find(|pkg| pkg.package_id() == id); + let pkg = pkg + .as_ref() + .and_then(|p| p.as_ref()) + .filter(|pkg| pkg.package_id() == id); pkg.cloned() .map(MaybePackage::Ready) .ok_or_else(|| internal(format!("failed to find {} in path source", id))) diff --git a/tests/testsuite/build.rs b/tests/testsuite/build.rs index d28efeace62..e60c094dfb9 100644 --- a/tests/testsuite/build.rs +++ b/tests/testsuite/build.rs @@ -1293,6 +1293,7 @@ fn cargo_compile_with_dep_name_mismatch() { [ERROR] no matching package named `notquitebar` found location searched: [ROOT]/foo/bar required by package `foo v0.0.1 ([ROOT]/foo)` +[HELP] package `bar` exists at `[ROOT]/foo/bar` "#]]) .run(); diff --git a/tests/testsuite/cargo_add/invalid_path/stderr.term.svg b/tests/testsuite/cargo_add/invalid_path/stderr.term.svg index 203ba67dc93..f82f0733b67 100644 --- a/tests/testsuite/cargo_add/invalid_path/stderr.term.svg +++ b/tests/testsuite/cargo_add/invalid_path/stderr.term.svg @@ -1,4 +1,4 @@ - +