diff --git a/crates/cargo-test-support/src/registry.rs b/crates/cargo-test-support/src/registry.rs index ba4173afdd0..6d40c62bd5f 100644 --- a/crates/cargo-test-support/src/registry.rs +++ b/crates/cargo-test-support/src/registry.rs @@ -144,6 +144,7 @@ pub struct Package { local: bool, alternative: bool, invalid_json: bool, + min_rust_version: Option, } #[derive(Clone)] @@ -242,6 +243,7 @@ impl Package { local: false, alternative: false, invalid_json: false, + min_rust_version: None, } } @@ -359,6 +361,12 @@ impl Package { self } + /// Sets min_rust_version. + pub fn min_rust_version(&mut self, min_rust_version: Option) -> &mut Package { + self.min_rust_version = min_rust_version; + self + } + /// Creates the package and place it in the registry. /// /// This does not actually use Cargo's publishing system, but instead @@ -413,6 +421,7 @@ impl Package { "cksum": cksum, "features": self.features, "yanked": self.yanked, + "min_rust_version": self.min_rust_version, }) .to_string(); diff --git a/crates/resolver-tests/src/lib.rs b/crates/resolver-tests/src/lib.rs index ef47f11ffc5..74ea155bb5d 100644 --- a/crates/resolver-tests/src/lib.rs +++ b/crates/resolver-tests/src/lib.rs @@ -176,6 +176,7 @@ pub fn resolve_with_config_raw( &BTreeMap::>::new(), None::<&String>, false, + None, ) .unwrap(); let opts = ResolveOpts::everything(); @@ -187,6 +188,7 @@ pub fn resolve_with_config_raw( &HashSet::new(), config, true, + None, ); // The largest test in our suite takes less then 30 sec. @@ -577,6 +579,7 @@ pub fn pkg_dep(name: T, dep: Vec) -> Summary { &BTreeMap::>::new(), link, false, + None, ) .unwrap() } @@ -605,6 +608,7 @@ pub fn pkg_loc(name: &str, loc: &str) -> Summary { &BTreeMap::>::new(), link, false, + None, ) .unwrap() } @@ -619,6 +623,7 @@ pub fn remove_dep(sum: &Summary, ind: usize) -> Summary { &BTreeMap::>::new(), sum.links().map(|a| a.as_str()), sum.namespaced_features(), + sum.min_rust_version().map(|v| v.clone()), ) .unwrap() } diff --git a/crates/resolver-tests/tests/resolve.rs b/crates/resolver-tests/tests/resolve.rs index 93e1ff4a1a9..23e964463fa 100644 --- a/crates/resolver-tests/tests/resolve.rs +++ b/crates/resolver-tests/tests/resolve.rs @@ -66,6 +66,7 @@ proptest! { false, false, false, + true, &None, &["minimal-versions".to_string()], &[], @@ -574,6 +575,7 @@ fn test_resolving_minimum_version_with_transitive_deps() { false, false, false, + true, &None, &["minimal-versions".to_string()], &[], diff --git a/src/bin/cargo/cli.rs b/src/bin/cargo/cli.rs index c30eeb40508..97d5af17349 100644 --- a/src/bin/cargo/cli.rs +++ b/src/bin/cargo/cli.rs @@ -172,6 +172,7 @@ fn config_configure( } else { None }; + let ignore_rust_version = subcommand_args.is_present("ignore-min-rust-version"); config.configure( args.occurrences_of("verbose") as u32, quiet, @@ -179,6 +180,7 @@ fn config_configure( args.is_present("frozen"), args.is_present("locked"), args.is_present("offline"), + !ignore_rust_version, arg_target_dir, &args .values_of_lossy("unstable-features") diff --git a/src/bin/cargo/commands/bench.rs b/src/bin/cargo/commands/bench.rs index b84c8160be3..8266f261503 100644 --- a/src/bin/cargo/commands/bench.rs +++ b/src/bin/cargo/commands/bench.rs @@ -39,6 +39,7 @@ pub fn cli() -> App { .arg_target_triple("Build for the target triple") .arg_target_dir() .arg_manifest_path() + .arg_ignore_min_rust_version() .arg_message_format() .arg(opt( "no-fail-fast", diff --git a/src/bin/cargo/commands/build.rs b/src/bin/cargo/commands/build.rs index df3a69c18d0..2878f316324 100644 --- a/src/bin/cargo/commands/build.rs +++ b/src/bin/cargo/commands/build.rs @@ -39,6 +39,7 @@ pub fn cli() -> App { .value_name("PATH"), ) .arg_manifest_path() + .arg_ignore_min_rust_version() .arg_message_format() .arg_build_plan() .after_help( diff --git a/src/bin/cargo/commands/check.rs b/src/bin/cargo/commands/check.rs index 880b0d91d1a..b1341a90f26 100644 --- a/src/bin/cargo/commands/check.rs +++ b/src/bin/cargo/commands/check.rs @@ -32,6 +32,7 @@ pub fn cli() -> App { .arg_target_triple("Check for the target triple") .arg_target_dir() .arg_manifest_path() + .arg_ignore_min_rust_version() .arg_message_format() .after_help( "\ diff --git a/src/bin/cargo/commands/test.rs b/src/bin/cargo/commands/test.rs index 932cf308a2f..da7e4d88847 100644 --- a/src/bin/cargo/commands/test.rs +++ b/src/bin/cargo/commands/test.rs @@ -53,6 +53,7 @@ pub fn cli() -> App { .arg_target_triple("Build for the target triple") .arg_target_dir() .arg_manifest_path() + .arg_ignore_min_rust_version() .arg_message_format() .after_help( "\ diff --git a/src/bin/cargo/commands/update.rs b/src/bin/cargo/commands/update.rs index 3ffadcefcbe..0c380d8121a 100644 --- a/src/bin/cargo/commands/update.rs +++ b/src/bin/cargo/commands/update.rs @@ -14,6 +14,7 @@ pub fn cli() -> App { .arg_dry_run("Don't actually write the lockfile") .arg(opt("precise", "Update a single dependency to exactly PRECISE").value_name("PRECISE")) .arg_manifest_path() + .arg_ignore_min_rust_version() .after_help( "\ This command requires that a `Cargo.lock` already exists as generated by diff --git a/src/cargo/core/resolver/dep_cache.rs b/src/cargo/core/resolver/dep_cache.rs index 6365b1029e1..ce47b544614 100644 --- a/src/cargo/core/resolver/dep_cache.rs +++ b/src/cargo/core/resolver/dep_cache.rs @@ -73,7 +73,11 @@ impl<'a> RegistryQueryer<'a> { /// any candidates are returned which match an override then the override is /// applied by performing a second query for what the override should /// return. - pub fn query(&mut self, dep: &Dependency) -> CargoResult>> { + pub fn query( + &mut self, + dep: &Dependency, + active_rust_version: Option<&semver::Version>, + ) -> CargoResult>> { if let Some(out) = self.registry_cache.get(dep).cloned() { return Ok(out); } @@ -81,8 +85,11 @@ impl<'a> RegistryQueryer<'a> { let mut ret = Vec::new(); self.registry.query( dep, - &mut |s| { - ret.push(s); + &mut |summary| { + if !summary.compatible_with_rust_version(active_rust_version) { + return; + } + ret.push(summary); }, false, )?; @@ -200,6 +207,7 @@ impl<'a> RegistryQueryer<'a> { parent: Option, candidate: &Summary, opts: &ResolveOpts, + active_rust_version: Option<&semver::Version>, ) -> ActivateResult, Rc>)>> { // if we have calculated a result before, then we can just return it, // as it is a "pure" query of its arguments. @@ -220,7 +228,7 @@ impl<'a> RegistryQueryer<'a> { let mut deps = deps .into_iter() .map(|(dep, features)| { - let candidates = self.query(&dep)?; + let candidates = self.query(&dep, active_rust_version)?; Ok((dep, candidates, features)) }) .collect::>>()?; diff --git a/src/cargo/core/resolver/errors.rs b/src/cargo/core/resolver/errors.rs index 807555c86ee..424298e046e 100644 --- a/src/cargo/core/resolver/errors.rs +++ b/src/cargo/core/resolver/errors.rs @@ -76,6 +76,7 @@ pub(super) fn activation_error( conflicting_activations: &ConflictMap, candidates: &[Summary], config: Option<&Config>, + active_rust_version: Option<&semver::Version>, ) -> ResolveError { let to_resolve_err = |err| { ResolveError::new( @@ -190,67 +191,152 @@ pub(super) fn activation_error( // 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(", ") + // Factored into a standalone function to streamline control flow a bit + fn get_helpful_message( + cx: &Context, + parent: &Summary, + dep: &Dependency, + registry: &mut dyn Registry, + active_rust_version: Option<&semver::Version>, + ) -> Result { + let to_resolve_err = |err| { + Err(ResolveError::new( + err, + cx.parents + .path_to_bottom(&parent.package_id()) + .into_iter() + .cloned() + .collect(), + )) + }; + + // Maybe the package actually exists, but the required rust version + // is higher than what we have right now? + // Note that registry.query() does not take rust version into account - that's done in the resolver. + if let Some(active_rust_version) = active_rust_version { + let candidates = match registry.query_vec(dep, false) { + Ok(candidates) => candidates, + Err(e) => return to_resolve_err(e), }; + if !candidates.is_empty() { + let versions = { + let mut versions = candidates + .iter() + .take(3) + .map(|cand| match cand.min_rust_version() { + Some(ver) => format!("{} (rust>={})", cand.version(), ver), + // Honestly, this should never happen, but let's not panic. + None => format!("{} (rust - any)", cand.version()), + }) + .collect::>(); - 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( - &cx.parents.path_to_bottom(&parent.package_id()), - )); + if candidates.len() > 3 { + versions.push("...".into()); + } - // 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", + versions.join(", ") + }; + + let mut msg = format!( + "failed to select a version for the requirement `{} = \"{}\"`\n \ + which would be compatible with current rust version of {}\n \ + candidate versions found which didn't match: {}\n \ + location searched: {}\n", + dep.package_name(), + dep.version_req(), + active_rust_version, + versions, + registry.describe_source(dep.source_id()), ); + msg.push_str("required by "); + msg.push_str(&describe_path( + &cx.parents.path_to_bottom(&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?"); + } + + return Ok(msg); } + } + + // 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. + // We no longer care about min-rust-version here. This might lead to somewhat + // degraded experience if the 3 latest package versions all require rust version + // newer than what we have currently, but that's a lot of code for a somewhat niche case. + 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())); + 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()); + } - if registry.is_replaced(dep.source_id()) { - msg.push_str("\nperhaps a crate was updated and forgotten to be re-vendored?"); + 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( + &cx.parents.path_to_bottom(&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?"); + } + + return Ok(msg); } + } - 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. + // 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), true) { return to_resolve_err(e); @@ -309,9 +395,14 @@ pub(super) fn activation_error( msg.push_str(&describe_path( &cx.parents.path_to_bottom(&parent.package_id()), )); + return Ok(msg); + } + }; - msg - }; + let mut msg = match get_helpful_message(cx, parent, dep, registry, active_rust_version) { + Ok(m) => m, + Err(e) => return e, + }; if let Some(config) = config { if config.offline() { diff --git a/src/cargo/core/resolver/mod.rs b/src/cargo/core/resolver/mod.rs index 17728120aab..b031ed339f2 100644 --- a/src/cargo/core/resolver/mod.rs +++ b/src/cargo/core/resolver/mod.rs @@ -126,6 +126,7 @@ pub fn resolve( try_to_use: &HashSet, config: Option<&Config>, check_public_visible_dependencies: bool, + active_rust_version: Option<&semver::Version>, ) -> CargoResult { let cx = Context::new(check_public_visible_dependencies); let _p = profile::start("resolving"); @@ -134,7 +135,7 @@ pub fn resolve( None => false, }; let mut registry = RegistryQueryer::new(registry, replacements, try_to_use, minimal_versions); - let cx = activate_deps_loop(cx, &mut registry, summaries, config)?; + let cx = activate_deps_loop(cx, &mut registry, summaries, config, active_rust_version)?; let mut cksums = HashMap::new(); for (summary, _) in cx.activations.values() { @@ -171,6 +172,7 @@ fn activate_deps_loop( registry: &mut RegistryQueryer<'_>, summaries: &[(Summary, ResolveOpts)], config: Option<&Config>, + active_rust_version: Option<&semver::Version>, ) -> CargoResult { let mut backtrack_stack = Vec::new(); let mut remaining_deps = RemainingDeps::new(); @@ -182,7 +184,24 @@ fn activate_deps_loop( // Activate all the initial summaries to kick off some work. for &(ref summary, ref opts) in summaries { debug!("initial activation: {}", summary.package_id()); - let res = activate(&mut cx, registry, None, summary.clone(), opts.clone()); + if !summary.compatible_with_rust_version(active_rust_version) { + return Err(anyhow::format_err!( + "package {} requires rust version {} or greater (currently have {})", + summary.name(), + // unwraps justified: if either toolchain's or package required min version is None, + // it is considered to be "compatible". + summary.min_rust_version().unwrap(), + active_rust_version.unwrap() + )); + } + let res = activate( + &mut cx, + registry, + None, + summary.clone(), + opts.clone(), + active_rust_version.clone(), + ); match res { Ok(Some((frame, _))) => remaining_deps.push(frame), Ok(None) => (), @@ -292,6 +311,7 @@ fn activate_deps_loop( &parent, &dep, &conflicting_activations, + active_rust_version.clone(), ) { generalize_conflicting_activations = Some(c); } @@ -329,6 +349,7 @@ fn activate_deps_loop( &conflicting_activations, &candidates, config, + active_rust_version.clone(), )) } } @@ -379,7 +400,14 @@ fn activate_deps_loop( dep.package_name(), candidate.version() ); - let res = activate(&mut cx, registry, Some((&parent, &dep)), candidate, opts); + let res = activate( + &mut cx, + registry, + Some((&parent, &dep)), + candidate, + opts, + active_rust_version.clone(), + ); let successfully_activated = match res { // Success! We've now activated our `candidate` in our context @@ -592,6 +620,7 @@ fn activate( parent: Option<(&Summary, &Dependency)>, candidate: Summary, opts: ResolveOpts, + active_rust_version: Option<&semver::Version>, ) -> ActivateResult> { let candidate_pid = candidate.package_id(); cx.age += 1; @@ -643,8 +672,12 @@ fn activate( }; let now = Instant::now(); - let (used_features, deps) = - &*registry.build_deps(parent.map(|p| p.0.package_id()), &candidate, &opts)?; + let (used_features, deps) = &*registry.build_deps( + parent.map(|p| p.0.package_id()), + &candidate, + &opts, + active_rust_version, + )?; // Record what list of features is active for this package. if !used_features.is_empty() { @@ -803,6 +836,7 @@ fn generalize_conflicting( parent: &Summary, dep: &Dependency, conflicting_activations: &ConflictMap, + active_rust_version: Option<&semver::Version>, ) -> Option { if conflicting_activations.is_empty() { return None; @@ -841,7 +875,7 @@ fn generalize_conflicting( // Thus, if all the things it can resolve to have already ben determined // to be conflicting, then we can just say that we conflict with the parent. if let Some(others) = registry - .query(critical_parents_dep) + .query(critical_parents_dep, active_rust_version.clone()) .expect("an already used dep now error!?") .iter() .rev() // the last one to be tried is the least likely to be in the cache, so start with that. diff --git a/src/cargo/core/summary.rs b/src/cargo/core/summary.rs index 365c9e29fba..9dafe06d394 100644 --- a/src/cargo/core/summary.rs +++ b/src/cargo/core/summary.rs @@ -30,6 +30,7 @@ struct Inner { checksum: Option, links: Option, namespaced_features: bool, + min_rust_version: Option, } impl Summary { @@ -39,6 +40,7 @@ impl Summary { features: &BTreeMap>>, links: Option>, namespaced_features: bool, + min_rust_version: Option, ) -> CargoResult where K: Borrow + Ord + Display, @@ -68,6 +70,7 @@ impl Summary { checksum: None, links: links.map(|l| l.into()), namespaced_features, + min_rust_version, }), }) } @@ -130,6 +133,30 @@ impl Summary { }; me.map_dependencies(|dep| dep.map_source(to_replace, replace_with)) } + + pub fn min_rust_version(&self) -> Option<&Version> { + self.inner.min_rust_version.as_ref() + } + // Whether the package min-rust-version constraint is satisfied by current toolchain. + pub fn compatible_with_rust_version(&self, rust: Option<&Version>) -> bool { + match (self.min_rust_version(), rust) { + (None, _) => true, // no constraints + (_, None) => true, // unknown rust version. Assume it's good enough. + (Some(pkg_v), Some(rust_v)) => { + // Zealously remove pre-release parts, since they don't work as one would expect. + // These should be pre-stripped already, so we try to optimize for that, + // but we can't blindly assume this to be true. + match (pkg_v.pre.is_empty(), rust_v.pre.is_empty()) { + (true, true) => rust_v >= pkg_v, + (_, _) => { + let pkg_v = Version::new(pkg_v.major, pkg_v.minor, pkg_v.patch); + let rust_v = Version::new(rust_v.major, rust_v.minor, rust_v.patch); + rust_v >= pkg_v + } + } + } + } + } } impl PartialEq for Summary { diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index e09e5cac155..d7877a90fc6 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -352,6 +352,17 @@ pub fn resolve_with_previous<'cfg>( None => root_replace.to_vec(), }; + let rustc_version = if ws.config().honor_min_rust_version() { + ws.config().load_global_rustc(Some(ws)).ok().map(|rustc| { + let mut release = rustc.release; + // Resolver logic doesn't care about toolchain channel, so pre-strip it, as an optimization. + release.pre.clear(); + release + }) + } else { + None + }; + ws.preload(registry); let mut resolved = resolver::resolve( &summaries, @@ -360,6 +371,7 @@ pub fn resolve_with_previous<'cfg>( &try_to_use, Some(ws.config()), ws.features().require(Feature::public_dependency()).is_ok(), + rustc_version.as_ref(), )?; resolved.register_used_patches(®istry.patches()); if register_patches { diff --git a/src/cargo/sources/registry/index.rs b/src/cargo/sources/registry/index.rs index 9bfb15d7e42..ca280a8c714 100644 --- a/src/cargo/sources/registry/index.rs +++ b/src/cargo/sources/registry/index.rs @@ -715,6 +715,7 @@ impl IndexSummary { features, yanked, links, + min_rust_version, } = serde_json::from_slice(line)?; log::trace!("json parsed registry {}/{}", name, vers); let pkgid = PackageId::new(name, &vers, source_id)?; @@ -722,7 +723,7 @@ impl IndexSummary { .into_iter() .map(|dep| dep.into_dep(source_id)) .collect::>>()?; - let mut summary = Summary::new(pkgid, deps, &features, links, false)?; + let mut summary = Summary::new(pkgid, deps, &features, links, false, min_rust_version)?; summary.set_checksum(cksum); Ok(IndexSummary { summary, diff --git a/src/cargo/sources/registry/mod.rs b/src/cargo/sources/registry/mod.rs index 56622902799..811850dbccb 100644 --- a/src/cargo/sources/registry/mod.rs +++ b/src/cargo/sources/registry/mod.rs @@ -227,6 +227,7 @@ pub struct RegistryPackage<'a> { cksum: String, yanked: Option, links: Option, + min_rust_version: Option, } #[test] diff --git a/src/cargo/util/command_prelude.rs b/src/cargo/util/command_prelude.rs index a3477b4575a..4adb824d3e4 100644 --- a/src/cargo/util/command_prelude.rs +++ b/src/cargo/util/command_prelude.rs @@ -190,6 +190,13 @@ pub trait AppExt: Sized { fn arg_dry_run(self, dry_run: &'static str) -> Self { self._arg(opt("dry-run", dry_run)) } + + fn arg_ignore_min_rust_version(self) -> Self { + self._arg(opt( + "ignore-min-rust-version", + "Ignore `min-rust-version` specification in packages", + )) + } } impl AppExt for App { diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index bb30992ba52..4383367aac4 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -146,6 +146,11 @@ pub struct Config { /// `offline` is set if we should never access the network, but otherwise /// continue operating if possible. offline: bool, + /// `honor_min_rust_version` is set if we should honor `min-rust-version` + /// specifications advertized by packages. `true` is default. + /// If set to `false`, the package min-rust-version is ignored and we assume + /// our active toolchain is good enough to satisfy those. + honor_min_rust_version: bool, /// A global static IPC control mechanism (used for managing parallel builds) jobserver: Option, /// Cli flags of the form "-Z something" @@ -221,6 +226,7 @@ impl Config { frozen: false, locked: false, offline: false, + honor_min_rust_version: true, jobserver: unsafe { if GLOBAL_JOBSERVER.is_null() { None @@ -607,6 +613,7 @@ impl Config { frozen: bool, locked: bool, offline: bool, + honor_min_rust_version: bool, target_dir: &Option, unstable_flags: &[String], cli_config: &[&str], @@ -669,6 +676,7 @@ impl Config { .ok() .and_then(|n| n.offline) .unwrap_or(false); + self.honor_min_rust_version = honor_min_rust_version; self.target_dir = cli_target_dir; if nightly_features_allowed() { @@ -704,6 +712,10 @@ impl Config { !self.frozen && !self.locked } + pub fn honor_min_rust_version(&self) -> bool { + self.honor_min_rust_version + } + /// Loads configuration from the filesystem. pub fn load_values(&self) -> CargoResult> { self.load_values_from(&self.cwd) diff --git a/src/cargo/util/rustc.rs b/src/cargo/util/rustc.rs index d17c17af7c1..9d4afbc4019 100644 --- a/src/cargo/util/rustc.rs +++ b/src/cargo/util/rustc.rs @@ -25,6 +25,8 @@ pub struct Rustc { pub verbose_version: String, /// The host triple (arch-platform-OS), this comes from verbose_version. pub host: InternedString, + /// The rustc version (`1.23.4-beta.2`), this comes from verbose_version. + pub release: semver::Version, cache: Mutex, } @@ -61,12 +63,26 @@ impl Rustc { })?; InternedString::new(triple) }; + let release = { + let realease_str = verbose_version + .lines() + .find(|l| l.starts_with("release: ")) + .map(|l| &l[9..]) + .ok_or_else(|| { + anyhow::format_err!( + "`rustc -vV` didn't have a line for `release:`, got:\n{}", + verbose_version + ) + })?; + semver::Version::parse(realease_str)? + }; Ok(Rustc { path, wrapper: wrapper.map(util::process), verbose_version, host, + release, cache: Mutex::new(cache), }) } diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 7aef4cbc010..ef808df93f6 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -769,10 +769,12 @@ impl<'de> de::Deserialize<'de> for VecStringOrBool { /// the field `metadata`, since it is a table and values cannot appear after /// tables. #[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] pub struct TomlProject { edition: Option, name: InternedString, version: semver::Version, + min_rust_version: Option, authors: Option>, build: Option, metabuild: Option, @@ -780,18 +782,14 @@ pub struct TomlProject { exclude: Option>, include: Option>, publish: Option, - #[serde(rename = "publish-lockfile")] publish_lockfile: Option, workspace: Option, - #[serde(rename = "im-a-teapot")] im_a_teapot: Option, autobins: Option, autoexamples: Option, autotests: Option, autobenches: Option, - #[serde(rename = "namespaced-features")] namespaced_features: Option, - #[serde(rename = "default-run")] default_run: Option, // Package metadata. @@ -802,7 +800,6 @@ pub struct TomlProject { keywords: Option>, categories: Option>, license: Option, - #[serde(rename = "license-file")] license_file: Option, repository: Option, metadata: Option, @@ -1126,20 +1123,33 @@ impl TomlManifest { features.require(Feature::namespaced_features())?; } - let summary = Summary::new( - pkgid, - deps, - &me.features - .as_ref() - .map(|x| { - x.iter() - .map(|(k, v)| (k.as_str(), v.iter().collect())) - .collect() - }) - .unwrap_or_else(BTreeMap::new), - project.links.as_ref().map(|x| x.as_str()), - project.namespaced_features.unwrap_or(false), - )?; + let summary = { + let mut min_rust_version = project.min_rust_version.clone(); + if let Some(ver) = &mut min_rust_version { + if !ver.pre.is_empty() { + warnings.push(format!( + "pre-release part of min-rust-version ({:?}) is ignored.", + ver.pre + )); + ver.pre.clear(); + } + }; + Summary::new( + pkgid, + deps, + &me.features + .as_ref() + .map(|x| { + x.iter() + .map(|(k, v)| (k.as_str(), v.iter().collect())) + .collect() + }) + .unwrap_or_else(BTreeMap::new), + project.links.as_ref().map(|x| x.as_str()), + project.namespaced_features.unwrap_or(false), + min_rust_version, + )? + }; let metadata = ManifestMetadata { description: project.description.clone(), homepage: project.homepage.clone(), diff --git a/tests/testsuite/config.rs b/tests/testsuite/config.rs index fdbc9339bca..e1afefd666a 100644 --- a/tests/testsuite/config.rs +++ b/tests/testsuite/config.rs @@ -84,6 +84,7 @@ impl ConfigBuilder { false, false, false, + true, &None, &self.unstable, &config_args, diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index bbea3cc022e..26f8c9b136b 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -63,6 +63,7 @@ mod member_errors; mod message_format; mod metabuild; mod metadata; +mod min_rust_version; mod minimal_versions; mod net_config; mod new; diff --git a/tests/testsuite/min_rust_version.rs b/tests/testsuite/min_rust_version.rs new file mode 100644 index 00000000000..a80f793a0a3 --- /dev/null +++ b/tests/testsuite/min_rust_version.rs @@ -0,0 +1,208 @@ +//! Tests for targets with `min-rust-version`. + +use cargo_test_support::project; +use cargo_test_support::registry::Package; + +#[cargo_test] +fn min_rust_version_satisfied() { + let p = project() + .file( + "Cargo.toml", + r#" + [project] + name = "foo" + version = "0.0.1" + authors = [] + min-rust-version = "1.1.1" + + [[bin]] + name = "foo" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build").run(); + p.cargo("build --ignore-min-rust-version").run(); +} + +#[cargo_test] +fn min_rust_version_too_high() { + let p = project() + .file( + "Cargo.toml", + r#" + [project] + name = "foo" + version = "0.0.1" + authors = [] + min-rust-version = "1.9876.0" + + [[bin]] + name = "foo" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build") + .with_status(101) + .with_stderr( + "\ +error: package foo requires rust version 1.9876.0 or greater (currently have [..]) +", + ) + .run(); + p.cargo("build --ignore-min-rust-version").run(); +} + +#[cargo_test] +fn min_rust_version_pre_release_ignored() { + let p = project() + .file( + "Cargo.toml", + r#" + [project] + name = "foo" + version = "0.0.1" + authors = [] + min-rust-version = "1.2.3-nightly" + + [[bin]] + name = "foo" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build") + .with_status(0) + .with_stderr( + "\ +warning: pre-release part of min-rust-version ([AlphaNumeric(\"nightly\")]) is ignored. + Compiling foo v0.0.1 ([..]) + Finished dev [unoptimized + debuginfo] target(s) in [..] +", + ) + .run(); + p.cargo("build --ignore-min-rust-version").run(); +} + +#[cargo_test] +fn min_rust_version_local_dependency_fails() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [dependencies.bar] + path = "../bar" + #[dependencies.baz] + #path = "../baz" + "#, + ) + .file("src/main.rs", "fn main(){}") + .build(); + let _bar = project() + .at("bar") + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + min-rust-version = "1.2345.0" + "#, + ) + .file("src/lib.rs", "fn other_stuff(){}") + .build(); + + p.cargo("build") + .with_status(101) + .with_stderr( + "\ +error: failed to select a version for the requirement `bar = \"*\"` + which would be compatible with current rust version of [..] + candidate versions found which didn't match: 0.0.1 (rust>=1.2345.0) + location searched: [..] +required by package `foo v0.0.1 ([..])` +", + ) + .run(); + p.cargo("build --ignore-min-rust-version").run(); +} + +#[cargo_test] +fn min_rust_version_registry() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + min-rust-version = "1.0.0" + + [dependencies] + bar = "0.1" + "#, + ) + .file("src/lib.rs", "fn stuff(){}") + .build(); + + Package::new("bar", "0.1.0") + .min_rust_version(Some("1.987.0".to_string())) + .publish(); + + p.cargo("build") + .with_status(101) + .with_stderr( + " \ + Updating `[..]` index +error: failed to select a version for the requirement `bar = \"^0.1\"` + which would be compatible with current rust version of [..] + candidate versions found which didn't match: 0.1.0 (rust>=1.987.0) + location searched: [..] +required by package `foo v0.0.1 ([..])` +perhaps a crate was updated and forgotten to be re-vendored? +", + ) + .run(); + p.cargo("build --ignore-min-rust-version").run(); +} + +#[cargo_test] +fn min_rust_version_registry_dependency_resolution() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + min-rust-version = "1.0.0" + + [dependencies] + bar = "0.1" + "#, + ) + .file("src/lib.rs", "fn stuff(){}") + .build(); + + Package::new("bar", "0.1.0") + .min_rust_version(Some("1.0.0".to_string())) + .publish(); + Package::new("bar", "0.1.1") + .min_rust_version(Some("1.987.0".to_string())) + .publish(); + + p.cargo("build").run(); + p.cargo("build --ignore-min-rust-version").run(); +}