diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 4bdcc08a8..3ce6a8267 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -285,6 +285,7 @@ pub struct CliUnstable { pub offline: bool, pub no_index_update: bool, pub avoid_dev_deps: bool, + pub minimal_versions: bool, } impl CliUnstable { @@ -317,6 +318,7 @@ impl CliUnstable { "offline" => self.offline = true, "no-index-update" => self.no_index_update = true, "avoid-dev-deps" => self.avoid_dev_deps = true, + "minimal-versions" => self.minimal_versions = true, _ => bail!("unknown `-Z` flag specified: {}", k), } diff --git a/src/cargo/core/resolver/mod.rs b/src/cargo/core/resolver/mod.rs index 63b13cbaa..0e266ecf2 100644 --- a/src/cargo/core/resolver/mod.rs +++ b/src/cargo/core/resolver/mod.rs @@ -430,7 +430,11 @@ pub fn resolve( warnings: RcList::new(), }; let _p = profile::start("resolving"); - let mut registry = RegistryQueryer::new(registry, replacements, try_to_use); + let minimal_versions = match config { + Some(config) => config.cli_unstable().minimal_versions, + 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 mut resolve = Resolve { @@ -683,6 +687,10 @@ struct RegistryQueryer<'a> { try_to_use: &'a HashSet<&'a PackageId>, // TODO: with nll the Rc can be removed cache: HashMap>>, + // If set the list of dependency candidates will be sorted by minimal + // versions first. That allows `cargo update -Z minimal-versions` which will + // specify minimum depedency versions to be used. + minimal_versions: bool, } impl<'a> RegistryQueryer<'a> { @@ -690,12 +698,14 @@ impl<'a> RegistryQueryer<'a> { registry: &'a mut Registry, replacements: &'a [(PackageIdSpec, Dependency)], try_to_use: &'a HashSet<&'a PackageId>, + minimal_versions: bool, ) -> Self { RegistryQueryer { registry, replacements, cache: HashMap::new(), try_to_use, + minimal_versions, } } @@ -795,9 +805,20 @@ impl<'a> RegistryQueryer<'a> { ret.sort_unstable_by(|a, b| { let a_in_previous = self.try_to_use.contains(a.summary.package_id()); let b_in_previous = self.try_to_use.contains(b.summary.package_id()); - let a = (a_in_previous, a.summary.version()); - let b = (b_in_previous, b.summary.version()); - a.cmp(&b).reverse() + let previous_cmp = a_in_previous.cmp(&b_in_previous).reverse(); + match previous_cmp { + Ordering::Equal => { + let cmp = a.summary.version().cmp(&b.summary.version()); + if self.minimal_versions == true { + // Lower version ordered first. + cmp + } else { + // Higher version ordered first. + cmp.reverse() + } + } + _ => previous_cmp, + } }); let out = Rc::new(ret); diff --git a/tests/testsuite/resolve.rs b/tests/testsuite/resolve.rs index c8dfc4903..892ec948d 100644 --- a/tests/testsuite/resolve.rs +++ b/tests/testsuite/resolve.rs @@ -5,13 +5,26 @@ use hamcrest::{assert_that, contains, is_not}; use cargo::core::source::{GitReference, SourceId}; use cargo::core::dependency::Kind::{self, Development}; use cargo::core::{Dependency, PackageId, Registry, Summary}; -use cargo::util::{CargoResult, ToUrl}; +use cargo::util::{CargoResult, Config, ToUrl}; use cargo::core::resolver::{self, Method}; +use cargotest::ChannelChanger; +use cargotest::support::{execs, project}; +use cargotest::support::registry::Package; + fn resolve( pkg: &PackageId, deps: Vec, registry: &[Summary], +) -> CargoResult> { + resolve_with_config(pkg, deps, registry, None) +} + +fn resolve_with_config( + pkg: &PackageId, + deps: Vec, + registry: &[Summary], + config: Option<&Config>, ) -> CargoResult> { struct MyRegistry<'a>(&'a [Summary]); impl<'a> Registry for MyRegistry<'a> { @@ -38,7 +51,7 @@ fn resolve( &[], &mut registry, &HashSet::new(), - None, + config, false, )?; let res = resolve.iter().cloned().collect(); @@ -327,6 +340,87 @@ fn test_resolving_maximum_version_with_transitive_deps() { assert_that(&res, is_not(contains(names(&[("util", "1.1.1")])))); } +#[test] +fn test_resolving_minimum_version_with_transitive_deps() { + // When the minimal-versions config option is specified then the lowest + // possible version of a package should be selected. "util 1.0.0" can't be + // selected because of the requirements of "bar", so the minimum version + // must be 1.1.1. + let reg = registry(vec![ + pkg!(("util", "1.2.2")), + pkg!(("util", "1.0.0")), + pkg!(("util", "1.1.1")), + pkg!("foo" => [dep_req("util", "1.0.0")]), + pkg!("bar" => [dep_req("util", ">=1.0.1")]), + ]); + + let mut config = Config::default().unwrap(); + config + .configure( + 1, + None, + &None, + false, + false, + &["minimal-versions".to_string()], + ) + .unwrap(); + + let res = resolve_with_config( + &pkg_id("root"), + vec![dep_req("foo", "1.0.0"), dep_req("bar", "1.0.0")], + ®, + Some(&config), + ).unwrap(); + + assert_that( + &res, + contains(names(&[ + ("root", "1.0.0"), + ("foo", "1.0.0"), + ("bar", "1.0.0"), + ("util", "1.1.1"), + ])), + ); + assert_that(&res, is_not(contains(names(&[("util", "1.2.2")])))); + assert_that(&res, is_not(contains(names(&[("util", "1.0.0")])))); +} + +// Ensure that the "-Z minimal-versions" CLI option works and the minimal +// version of a dependency ends up in the lock file. +#[test] +fn minimal_version_cli() { + Package::new("dep", "1.0.0").publish(); + Package::new("dep", "1.1.0").publish(); + + let p = project("foo") + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + authors = [] + version = "0.0.1" + + [dependencies] + dep = "1.0" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + assert_that( + p.cargo("generate-lockfile") + .masquerade_as_nightly_cargo() + .arg("-Zminimal-versions"), + execs().with_status(0), + ); + + let lock = p.read_lockfile(); + + assert!(lock.contains("dep 1.0.0")); +} + #[test] fn resolving_incompat_versions() { let reg = registry(vec![