diff --git a/src/cargo/core/package_id_spec.rs b/src/cargo/core/package_id_spec.rs index 37a6f5e7b3c..53d99b84ba7 100644 --- a/src/cargo/core/package_id_spec.rs +++ b/src/cargo/core/package_id_spec.rs @@ -176,8 +176,7 @@ impl PackageIdSpec { } if let Some(ref v) = self.version { - let req = v.exact_req(); - if !req.matches(package_id.version()) { + if !v.matches(package_id.version()) { return false; } } @@ -444,15 +443,50 @@ mod tests { fn matching() { let url = Url::parse("https://example.com").unwrap(); let sid = SourceId::for_registry(&url).unwrap(); - let foo = PackageId::new("foo", "1.2.3", sid).unwrap(); - let bar = PackageId::new("bar", "1.2.3", sid).unwrap(); + let foo = PackageId::new("foo", "1.2.3", sid).unwrap(); assert!(PackageIdSpec::parse("foo").unwrap().matches(foo)); - assert!(!PackageIdSpec::parse("foo").unwrap().matches(bar)); + assert!(!PackageIdSpec::parse("bar").unwrap().matches(foo)); assert!(PackageIdSpec::parse("foo:1.2.3").unwrap().matches(foo)); assert!(!PackageIdSpec::parse("foo:1.2.2").unwrap().matches(foo)); assert!(PackageIdSpec::parse("foo@1.2.3").unwrap().matches(foo)); assert!(!PackageIdSpec::parse("foo@1.2.2").unwrap().matches(foo)); assert!(PackageIdSpec::parse("foo@1.2").unwrap().matches(foo)); + + let meta = PackageId::new("meta", "1.2.3+hello", sid).unwrap(); + assert!(PackageIdSpec::parse("meta").unwrap().matches(meta)); + assert!(PackageIdSpec::parse("meta@1").unwrap().matches(meta)); + assert!(PackageIdSpec::parse("meta@1.2").unwrap().matches(meta)); + assert!(PackageIdSpec::parse("meta@1.2.3").unwrap().matches(meta)); + assert!(!PackageIdSpec::parse("meta@1.2.3-alpha.0") + .unwrap() + .matches(meta)); + assert!(PackageIdSpec::parse("meta@1.2.3+hello") + .unwrap() + .matches(meta)); + assert!(!PackageIdSpec::parse("meta@1.2.3+bye") + .unwrap() + .matches(meta)); + + let pre = PackageId::new("pre", "1.2.3-alpha.0", sid).unwrap(); + assert!(PackageIdSpec::parse("pre").unwrap().matches(pre)); + assert!(!PackageIdSpec::parse("pre@1").unwrap().matches(pre)); + assert!(!PackageIdSpec::parse("pre@1.2").unwrap().matches(pre)); + assert!(!PackageIdSpec::parse("pre@1.2.3").unwrap().matches(pre)); + assert!(PackageIdSpec::parse("pre@1.2.3-alpha.0") + .unwrap() + .matches(pre)); + assert!(!PackageIdSpec::parse("pre@1.2.3-alpha.1") + .unwrap() + .matches(pre)); + assert!(!PackageIdSpec::parse("pre@1.2.3-beta.0") + .unwrap() + .matches(pre)); + assert!(!PackageIdSpec::parse("pre@1.2.3+hello") + .unwrap() + .matches(pre)); + assert!(!PackageIdSpec::parse("pre@1.2.3-alpha.0+hello") + .unwrap() + .matches(pre)); } } diff --git a/src/cargo/core/resolver/dep_cache.rs b/src/cargo/core/resolver/dep_cache.rs index 7b6e0661f17..9041c5b0f9e 100644 --- a/src/cargo/core/resolver/dep_cache.rs +++ b/src/cargo/core/resolver/dep_cache.rs @@ -173,11 +173,20 @@ impl<'a> RegistryQueryer<'a> { ))); } - // The dependency should be hard-coded to have the same name and an - // exact version requirement, so both of these assertions should - // never fail. - assert_eq!(s.version(), summary.version()); - assert_eq!(s.name(), summary.name()); + assert_eq!( + s.name(), + summary.name(), + "dependency should be hard coded to have the same name" + ); + if s.version() != summary.version() { + return Poll::Ready(Err(anyhow::anyhow!( + "replacement specification `{}` matched {} and tried to override it with {}\n\ + avoid matching unrelated packages by being more specific", + spec, + summary.version(), + s.version(), + ))); + } let replace = if s.source_id() == summary.source_id() { debug!("Preventing\n{:?}\nfrom replacing\n{:?}", summary, s); diff --git a/src/cargo/util/semver_ext.rs b/src/cargo/util/semver_ext.rs index bee3b2da3eb..5839d85d23f 100644 --- a/src/cargo/util/semver_ext.rs +++ b/src/cargo/util/semver_ext.rs @@ -186,16 +186,25 @@ impl PartialVersion { } } - pub fn exact_req(&self) -> VersionReq { - VersionReq { - comparators: vec![Comparator { - op: semver::Op::Exact, - major: self.major, - minor: self.minor, - patch: self.patch, - pre: self.pre.as_ref().cloned().unwrap_or_default(), - }], + /// Check if this matches a version, including build metadata + /// + /// Build metadata does not affect version precedence but may be necessary for uniquely + /// identifying a package. + pub fn matches(&self, version: &Version) -> bool { + if !version.pre.is_empty() && self.pre.is_none() { + // Pre-release versions must be explicitly opted into, if for no other reason than to + // give us room to figure out and define the semantics + return false; } + self.major == version.major + && self.minor.map(|f| f == version.minor).unwrap_or(true) + && self.patch.map(|f| f == version.patch).unwrap_or(true) + && self.pre.as_ref().map(|f| f == &version.pre).unwrap_or(true) + && self + .build + .as_ref() + .map(|f| f == &version.build) + .unwrap_or(true) } } diff --git a/tests/testsuite/doc.rs b/tests/testsuite/doc.rs index 481df859045..a1698091246 100644 --- a/tests/testsuite/doc.rs +++ b/tests/testsuite/doc.rs @@ -2004,7 +2004,7 @@ fn crate_versions() { let output_path = p.root().join("target/doc/foo/index.html"); let output_documentation = fs::read_to_string(&output_path).unwrap(); - assert!(output_documentation.contains("Version 1.2.4")); + assert!(output_documentation.contains("1.2.4")); } #[cargo_test] @@ -2028,7 +2028,7 @@ fn crate_versions_flag_is_overridden() { }; let asserts = |html: String| { assert!(!html.contains("1.2.4")); - assert!(html.contains("Version 2.0.3")); + assert!(html.contains("2.0.3")); }; p.cargo("doc") diff --git a/tests/testsuite/replace.rs b/tests/testsuite/replace.rs index b583de7b7d5..b9de51d2fe6 100644 --- a/tests/testsuite/replace.rs +++ b/tests/testsuite/replace.rs @@ -1298,3 +1298,157 @@ fn override_plus_dep() { .with_stderr_contains("error: cyclic package dependency: [..]") .run(); } + +#[cargo_test] +fn override_generic_matching_other_versions() { + Package::new("bar", "0.1.0+a").publish(); + + let bar = git::repo(&paths::root().join("override")) + .file("Cargo.toml", &basic_manifest("bar", "0.1.0")) + .file("src/lib.rs", "pub fn bar() {}") + .build(); + + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [dependencies] + bar = "0.1.0" + + [replace] + "bar:0.1.0" = {{ git = '{}' }} + "#, + bar.url() + ), + ) + .file( + "src/lib.rs", + "extern crate bar; pub fn foo() { bar::bar(); }", + ) + .build(); + + p.cargo("check") + .with_stderr( + "\ +[UPDATING] `dummy-registry` index +[UPDATING] git repository `[..]` +[ERROR] failed to get `bar` as a dependency of package `foo v0.0.1 ([..]/foo)` + +Caused by: + replacement specification `https://github.com/rust-lang/crates.io-index#bar@0.1.0` matched 0.1.0+a and tried to override it with 0.1.0 + avoid matching unrelated packages by being more specific +", + ) + .with_status(101) + .run(); +} + +#[cargo_test] +fn override_respects_spec_metadata() { + Package::new("bar", "0.1.0+a").publish(); + + let bar = git::repo(&paths::root().join("override")) + .file("Cargo.toml", &basic_manifest("bar", "0.1.0+a")) + .file("src/lib.rs", "pub fn bar() {}") + .build(); + + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [dependencies] + bar = "0.1.0" + + [replace] + "bar:0.1.0+notTheBuild" = {{ git = '{}' }} + "#, + bar.url() + ), + ) + .file( + "src/lib.rs", + "extern crate bar; pub fn foo() { bar::bar(); }", + ) + .build(); + + p.cargo("check") + .with_stderr( + "\ +[UPDATING] `dummy-registry` index +[WARNING] package replacement is not used: https://github.com/rust-lang/crates.io-index#bar@0.1.0+notTheBuild +[DOWNLOADING] crates ... +[DOWNLOADED] bar v0.1.0+a (registry `dummy-registry`) +[CHECKING] bar v0.1.0+a +[CHECKING] foo v0.0.1 ([..]/foo) +[..] +[..] +[..] +[..] +[..] +[..] +[..] +error: could not compile `foo` (lib) due to previous error +", + ) + .with_status(101) + .run(); +} + +#[cargo_test] +fn override_spec_metadata_is_optional() { + Package::new("bar", "0.1.0+a").publish(); + + let bar = git::repo(&paths::root().join("override")) + .file("Cargo.toml", &basic_manifest("bar", "0.1.0+a")) + .file("src/lib.rs", "pub fn bar() {}") + .build(); + + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [dependencies] + bar = "0.1.0" + + [replace] + "bar:0.1.0" = {{ git = '{}' }} + "#, + bar.url() + ), + ) + .file( + "src/lib.rs", + "extern crate bar; pub fn foo() { bar::bar(); }", + ) + .build(); + + p.cargo("check") + .with_stderr( + "\ +[UPDATING] `dummy-registry` index +[UPDATING] git repository `[..]` +[CHECKING] bar v0.1.0+a (file://[..]) +[CHECKING] foo v0.0.1 ([CWD]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +", + ) + .run(); +} diff --git a/tests/testsuite/rustdocflags.rs b/tests/testsuite/rustdocflags.rs index 6992961ce39..c37d5a8266b 100644 --- a/tests/testsuite/rustdocflags.rs +++ b/tests/testsuite/rustdocflags.rs @@ -110,17 +110,19 @@ fn whitespace() { .with_status(101) .run(); - const SPACED_VERSION: &str = "a\nb\tc\u{00a0}d"; p.cargo("doc") .env_remove("__CARGO_TEST_FORCE_ARGFILE") // Not applicable for argfile. .env( "RUSTDOCFLAGS", - format!("--crate-version {}", SPACED_VERSION), + "--crate-version 1111\n2222\t3333\u{00a0}4444", ) .run(); let contents = p.read_file("target/doc/foo/index.html"); - assert!(contents.contains(SPACED_VERSION)); + assert!(contents.contains("1111")); + assert!(contents.contains("2222")); + assert!(contents.contains("3333")); + assert!(contents.contains("4444")); } #[cargo_test]