diff --git a/src/cargo/core/resolver/errors.rs b/src/cargo/core/resolver/errors.rs new file mode 100644 index 00000000000..74979c6df09 --- /dev/null +++ b/src/cargo/core/resolver/errors.rs @@ -0,0 +1,269 @@ +use std::collections::HashMap; +use std::fmt; + +use core::{Dependency, PackageId, Registry, Summary}; +use failure::{Error, Fail}; +use semver; +use util::config::Config; +use util::lev_distance::lev_distance; + +use super::context::Context; +use super::types::{Candidate, ConflictReason}; + +/// Error during resolution providing a path of `PackageId`s. +pub struct ResolveError { + cause: Error, + package_path: Vec, +} + +impl ResolveError { + pub fn new>(cause: E, package_path: Vec) -> Self { + Self { + cause: cause.into(), + package_path, + } + } + + /// Returns a path of packages from the package whose requirements could not be resolved up to + /// the root. + pub fn package_path(&self) -> &[PackageId] { + &self.package_path + } +} + +impl Fail for ResolveError { + fn cause(&self) -> Option<&Fail> { + self.cause.as_fail().cause() + } +} + +impl fmt::Debug for ResolveError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.cause.fmt(f) + } +} + +impl fmt::Display for ResolveError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.cause.fmt(f) + } +} + +pub(super) fn activation_error( + cx: &Context, + registry: &mut Registry, + parent: &Summary, + dep: &Dependency, + conflicting_activations: &HashMap, + candidates: &[Candidate], + config: Option<&Config>, +) -> ResolveError { + let graph = cx.graph(); + let to_resolve_err = |err| { + ResolveError::new( + err, + graph + .path_to_top(parent.package_id()) + .into_iter() + .cloned() + .collect(), + ) + }; + + if !candidates.is_empty() { + let mut msg = format!("failed to select a version for `{}`.", dep.package_name()); + msg.push_str("\n ... required by "); + msg.push_str(&describe_path(&graph.path_to_top(parent.package_id()))); + + msg.push_str("\nversions that meet the requirements `"); + msg.push_str(&dep.version_req().to_string()); + msg.push_str("` are: "); + msg.push_str( + &candidates + .iter() + .map(|v| v.summary.version()) + .map(|v| v.to_string()) + .collect::>() + .join(", "), + ); + + let mut conflicting_activations: Vec<_> = conflicting_activations.iter().collect(); + conflicting_activations.sort_unstable(); + let (links_errors, mut other_errors): (Vec<_>, Vec<_>) = conflicting_activations + .drain(..) + .rev() + .partition(|&(_, r)| r.is_links()); + + for &(p, r) in links_errors.iter() { + if let ConflictReason::Links(ref link) = *r { + msg.push_str("\n\nthe package `"); + msg.push_str(&*dep.package_name()); + msg.push_str("` links to the native library `"); + msg.push_str(link); + msg.push_str("`, but it conflicts with a previous package which links to `"); + msg.push_str(link); + msg.push_str("` as well:\n"); + } + msg.push_str(&describe_path(&graph.path_to_top(p))); + } + + let (features_errors, other_errors): (Vec<_>, Vec<_>) = other_errors + .drain(..) + .partition(|&(_, r)| r.is_missing_features()); + + for &(p, r) in features_errors.iter() { + if let ConflictReason::MissingFeatures(ref features) = *r { + msg.push_str("\n\nthe package `"); + msg.push_str(&*p.name()); + msg.push_str("` depends on `"); + msg.push_str(&*dep.package_name()); + msg.push_str("`, with features: `"); + msg.push_str(features); + msg.push_str("` but `"); + msg.push_str(&*dep.package_name()); + msg.push_str("` does not have these features.\n"); + } + // p == parent so the full path is redundant. + } + + if !other_errors.is_empty() { + msg.push_str( + "\n\nall possible versions conflict with \ + previously selected packages.", + ); + } + + for &(p, _) in other_errors.iter() { + msg.push_str("\n\n previously selected "); + msg.push_str(&describe_path(&graph.path_to_top(p))); + } + + msg.push_str("\n\nfailed to select a version for `"); + msg.push_str(&*dep.package_name()); + msg.push_str("` which could resolve this conflict"); + + return to_resolve_err(format_err!("{}", msg)); + } + + // We didn't actually find any candidates, so we need to + // give an error message that nothing was found. + // + // Maybe the user mistyped the ver_req? Like `dep="2"` when `dep="0.2"` + // was meant. So we re-query the registry with `deb="*"` so we can + // list a few versions that were actually found. + let all_req = semver::VersionReq::parse("*").unwrap(); + let mut new_dep = dep.clone(); + new_dep.set_version_req(all_req); + let mut candidates = match registry.query_vec(&new_dep, false) { + Ok(candidates) => candidates, + Err(e) => return to_resolve_err(e), + }; + candidates.sort_unstable_by(|a, b| b.version().cmp(a.version())); + + let mut msg = if !candidates.is_empty() { + let versions = { + let mut versions = candidates + .iter() + .take(3) + .map(|cand| cand.version().to_string()) + .collect::>(); + + if candidates.len() > 3 { + versions.push("...".into()); + } + + versions.join(", ") + }; + + let mut msg = format!( + "failed to select a version for the requirement `{} = \"{}\"`\n \ + candidate versions found which didn't match: {}\n \ + location searched: {}\n", + dep.package_name(), + dep.version_req(), + versions, + registry.describe_source(dep.source_id()), + ); + msg.push_str("required by "); + msg.push_str(&describe_path(&graph.path_to_top(parent.package_id()))); + + // If we have a path dependency with a locked version, then this may + // indicate that we updated a sub-package and forgot to run `cargo + // update`. In this case try to print a helpful error! + if dep.source_id().is_path() && dep.version_req().to_string().starts_with('=') { + msg.push_str( + "\nconsider running `cargo update` to update \ + a path dependency's locked version", + ); + } + + if registry.is_replaced(dep.source_id()) { + msg.push_str("\nperhaps a crate was updated and forgotten to be re-vendored?"); + } + + msg + } else { + // Maybe the user mistyped the name? Like `dep-thing` when `Dep_Thing` + // was meant. So we try asking the registry for a `fuzzy` search for suggestions. + let mut candidates = Vec::new(); + if let Err(e) = registry.query(&new_dep, &mut |s| candidates.push(s.name()), true) { + return to_resolve_err(e); + }; + candidates.sort_unstable(); + candidates.dedup(); + let mut candidates: Vec<_> = candidates + .iter() + .map(|n| (lev_distance(&*new_dep.package_name(), &*n), n)) + .filter(|&(d, _)| d < 4) + .collect(); + candidates.sort_by_key(|o| o.0); + let mut msg = format!( + "no matching package named `{}` found\n\ + location searched: {}\n", + dep.package_name(), + dep.source_id() + ); + if !candidates.is_empty() { + let mut names = candidates + .iter() + .take(3) + .map(|c| c.1.as_str()) + .collect::>(); + + if candidates.len() > 3 { + names.push("..."); + } + + msg.push_str("did you mean: "); + msg.push_str(&names.join(", ")); + msg.push_str("\n"); + } + msg.push_str("required by "); + msg.push_str(&describe_path(&graph.path_to_top(parent.package_id()))); + + msg + }; + + if let Some(config) = config { + if config.cli_unstable().offline { + msg.push_str( + "\nAs a reminder, you're using offline mode (-Z offline) \ + which can sometimes cause surprising resolution failures, \ + if this error is too confusing you may with to retry \ + without the offline flag.", + ); + } + } + + to_resolve_err(format_err!("{}", msg)) +} + +/// Returns String representation of dependency chain for a particular `pkgid`. +pub(super) fn describe_path(path: &[&PackageId]) -> String { + use std::fmt::Write; + let mut dep_path_desc = format!("package `{}`", path[0]); + for dep in path[1..].iter() { + write!(dep_path_desc, "\n ... which is depended on by `{}`", dep).unwrap(); + } + dep_path_desc +} diff --git a/src/cargo/core/resolver/mod.rs b/src/cargo/core/resolver/mod.rs index d1b3fe878ce..196c25016fb 100644 --- a/src/cargo/core/resolver/mod.rs +++ b/src/cargo/core/resolver/mod.rs @@ -58,8 +58,7 @@ use core::interning::InternedString; use core::PackageIdSpec; use core::{Dependency, PackageId, Registry, Summary}; use util::config::Config; -use util::errors::{CargoError, CargoResult}; -use util::lev_distance::lev_distance; +use util::errors::CargoResult; use util::profile; use self::context::{Activations, Context}; @@ -70,12 +69,14 @@ pub use self::encode::{EncodableDependency, EncodablePackageId, EncodableResolve pub use self::encode::{Metadata, WorkspaceResolve}; pub use self::resolve::Resolve; pub use self::types::Method; +pub use self::errors::ResolveError; mod conflict_cache; mod context; mod encode; mod resolve; mod types; +mod errors; /// Builds the list of all packages required to build the first argument. /// @@ -312,7 +313,7 @@ fn activate_deps_loop( } None => { debug!("no candidates found"); - Err(activation_error( + Err(errors::activation_error( &cx, registry.registry, &parent, @@ -817,214 +818,6 @@ fn find_candidate( None } -fn activation_error( - cx: &Context, - registry: &mut Registry, - parent: &Summary, - dep: &Dependency, - conflicting_activations: &HashMap, - candidates: &[Candidate], - config: Option<&Config>, -) -> CargoError { - let graph = cx.graph(); - if !candidates.is_empty() { - let mut msg = format!("failed to select a version for `{}`.", dep.package_name()); - msg.push_str("\n ... required by "); - msg.push_str(&describe_path(&graph.path_to_top(parent.package_id()))); - - msg.push_str("\nversions that meet the requirements `"); - msg.push_str(&dep.version_req().to_string()); - msg.push_str("` are: "); - msg.push_str( - &candidates - .iter() - .map(|v| v.summary.version()) - .map(|v| v.to_string()) - .collect::>() - .join(", "), - ); - - let mut conflicting_activations: Vec<_> = conflicting_activations.iter().collect(); - conflicting_activations.sort_unstable(); - let (links_errors, mut other_errors): (Vec<_>, Vec<_>) = conflicting_activations - .drain(..) - .rev() - .partition(|&(_, r)| r.is_links()); - - for &(p, r) in links_errors.iter() { - if let ConflictReason::Links(ref link) = *r { - msg.push_str("\n\nthe package `"); - msg.push_str(&*dep.package_name()); - msg.push_str("` links to the native library `"); - msg.push_str(link); - msg.push_str("`, but it conflicts with a previous package which links to `"); - msg.push_str(link); - msg.push_str("` as well:\n"); - } - msg.push_str(&describe_path(&graph.path_to_top(p))); - } - - let (features_errors, other_errors): (Vec<_>, Vec<_>) = other_errors - .drain(..) - .partition(|&(_, r)| r.is_missing_features()); - - for &(p, r) in features_errors.iter() { - if let ConflictReason::MissingFeatures(ref features) = *r { - msg.push_str("\n\nthe package `"); - msg.push_str(&*p.name()); - msg.push_str("` depends on `"); - msg.push_str(&*dep.package_name()); - msg.push_str("`, with features: `"); - msg.push_str(features); - msg.push_str("` but `"); - msg.push_str(&*dep.package_name()); - msg.push_str("` does not have these features.\n"); - } - // p == parent so the full path is redundant. - } - - if !other_errors.is_empty() { - msg.push_str( - "\n\nall possible versions conflict with \ - previously selected packages.", - ); - } - - for &(p, _) in other_errors.iter() { - msg.push_str("\n\n previously selected "); - msg.push_str(&describe_path(&graph.path_to_top(p))); - } - - msg.push_str("\n\nfailed to select a version for `"); - msg.push_str(&*dep.package_name()); - msg.push_str("` which could resolve this conflict"); - - return format_err!("{}", msg); - } - - // We didn't actually find any candidates, so we need to - // give an error message that nothing was found. - // - // Maybe the user mistyped the ver_req? Like `dep="2"` when `dep="0.2"` - // was meant. So we re-query the registry with `deb="*"` so we can - // list a few versions that were actually found. - let all_req = semver::VersionReq::parse("*").unwrap(); - let mut new_dep = dep.clone(); - new_dep.set_version_req(all_req); - let mut candidates = match registry.query_vec(&new_dep, false) { - Ok(candidates) => candidates, - Err(e) => return e, - }; - candidates.sort_unstable_by(|a, b| b.version().cmp(a.version())); - - let mut msg = if !candidates.is_empty() { - let versions = { - let mut versions = candidates - .iter() - .take(3) - .map(|cand| cand.version().to_string()) - .collect::>(); - - if candidates.len() > 3 { - versions.push("...".into()); - } - - versions.join(", ") - }; - - let mut msg = format!( - "failed to select a version for the requirement `{} = \"{}\"`\n \ - candidate versions found which didn't match: {}\n \ - location searched: {}\n", - dep.package_name(), - dep.version_req(), - versions, - registry.describe_source(dep.source_id()), - ); - msg.push_str("required by "); - msg.push_str(&describe_path(&graph.path_to_top(parent.package_id()))); - - // If we have a path dependency with a locked version, then this may - // indicate that we updated a sub-package and forgot to run `cargo - // update`. In this case try to print a helpful error! - if dep.source_id().is_path() && dep.version_req().to_string().starts_with('=') { - msg.push_str( - "\nconsider running `cargo update` to update \ - a path dependency's locked version", - ); - } - - if registry.is_replaced(dep.source_id()) { - msg.push_str("\nperhaps a crate was updated and forgotten to be re-vendored?"); - } - - msg - } else { - // Maybe the user mistyped the name? Like `dep-thing` when `Dep_Thing` - // was meant. So we try asking the registry for a `fuzzy` search for suggestions. - let mut candidates = Vec::new(); - if let Err(e) = registry.query(&new_dep, &mut |s| candidates.push(s.name()), true) { - return e; - }; - candidates.sort_unstable(); - candidates.dedup(); - let mut candidates: Vec<_> = candidates - .iter() - .map(|n| (lev_distance(&*new_dep.package_name(), &*n), n)) - .filter(|&(d, _)| d < 4) - .collect(); - candidates.sort_by_key(|o| o.0); - let mut msg = format!( - "no matching package named `{}` found\n\ - location searched: {}\n", - dep.package_name(), - dep.source_id() - ); - if !candidates.is_empty() { - let mut names = candidates - .iter() - .take(3) - .map(|c| c.1.as_str()) - .collect::>(); - - if candidates.len() > 3 { - names.push("..."); - } - - msg.push_str("did you mean: "); - msg.push_str(&names.join(", ")); - msg.push_str("\n"); - } - msg.push_str("required by "); - msg.push_str(&describe_path(&graph.path_to_top(parent.package_id()))); - - msg - }; - - if let Some(config) = config { - if config.cli_unstable().offline { - msg.push_str( - "\nAs a reminder, you're using offline mode (-Z offline) \ - which can sometimes cause surprising resolution failures, \ - if this error is too confusing you may with to retry \ - without the offline flag.", - ); - } - } - - format_err!("{}", msg) -} - -/// Returns String representation of dependency chain for a particular `pkgid`. -fn describe_path(path: &[&PackageId]) -> String { - use std::fmt::Write; - let mut dep_path_desc = format!("package `{}`", path[0]); - for dep in path[1..].iter() { - write!(dep_path_desc, "\n ... which is depended on by `{}`", dep).unwrap(); - } - dep_path_desc -} - fn check_cycles(resolve: &Resolve, activations: &Activations) -> CargoResult<()> { let summaries: HashMap<&PackageId, &Summary> = activations .values() @@ -1055,7 +848,7 @@ fn check_cycles(resolve: &Resolve, activations: &Activations) -> CargoResult<()> bail!( "cyclic package dependency: package `{}` depends on itself. Cycle:\n{}", id, - describe_path(&resolve.path_to_top(id)) + errors::describe_path(&resolve.path_to_top(id)) ); } diff --git a/tests/testsuite/member_errors.rs b/tests/testsuite/member_errors.rs index d8458501f45..a4f82cff8b0 100644 --- a/tests/testsuite/member_errors.rs +++ b/tests/testsuite/member_errors.rs @@ -1,5 +1,7 @@ -use cargo::core::Workspace; +use cargo::core::{compiler::CompileMode, Workspace}; +use cargo::ops::{self, CompileOptions}; use cargo::util::{config::Config, errors::ManifestError}; +use cargo::core::resolver::ResolveError; use support::project; @@ -102,3 +104,51 @@ fn member_manifest_path_io_error() { assert_eq!(causes[0].manifest_path(), &member_manifest_path); assert_eq!(causes[1].manifest_path(), &missing_manifest_path); } + +/// Test dependency version errors provide which package failed via a `ResolveError`. +#[test] +fn member_manifest_version_error() { + let p = project() + .file( + "Cargo.toml", + r#" + [project] + name = "foo" + version = "0.1.0" + authors = [] + + [dependencies] + bar = { path = "bar" } + + [workspace] + "#, + ) + .file("src/main.rs", "fn main() {}") + .file( + "bar/Cargo.toml", + r#" + [project] + name = "bar" + version = "0.1.0" + authors = [] + + [dependencies] + i-dont-exist = "0.55" + "#, + ) + .file("bar/src/main.rs", "fn main() {}") + .build(); + + let config = Config::default().unwrap(); + let ws = Workspace::new(&p.root().join("Cargo.toml"), &config).unwrap(); + let compile_options = CompileOptions::new(&config, CompileMode::Build).unwrap(); + let member_bar = ws.members().find(|m| &*m.name() == "bar").unwrap(); + + let error = ops::compile(&ws, &compile_options).map(|_| ()).unwrap_err(); + eprintln!("{:?}", error); + + let resolve_err: &ResolveError = error.downcast_ref().expect("Not a ResolveError"); + let package_path = resolve_err.package_path(); + assert_eq!(package_path.len(), 1, "package_path: {:?}", package_path); + assert_eq!(&package_path[0], member_bar.package_id()); +}