Repository: taiki-e/cargo-hack (language: rust)

Issue:
Get lower bound of --version-range from rust-version field in Cargo.toml



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f0ba9729..2fdeb816 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -77,8 +77,9 @@ jobs:
         with:
           persist-credentials: false
       - uses: taiki-e/github-actions/install-rust@main
-        with:
-          toolchain: stable
+      - run: CARGO_HACK_TEST_TOOLCHAIN=${{ matrix.rust }} cargo test --all
+      # Remove stable toolchain to disable https://github.com/taiki-e/cargo-hack/pull/138's behavior.
+      - run: rustup toolchain remove stable
       - run: CARGO_HACK_TEST_TOOLCHAIN=${{ matrix.rust }} cargo test --all
 
   build:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21813c3a..3ec4baa6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com
 
 - Fix an error when using old cargo with a dependency graph containing 2021 edition crates. ([#138](https://github.com/taiki-e/cargo-hack/pull/138))
 
+- Support omitting lower bound of --version-range. ([#139](https://github.com/taiki-e/cargo-hack/pull/139))
+
 ## [0.5.8] - 2021-10-13
 
 - Distribute statically linked binary on Windows MSVC. ([#131](https://github.com/taiki-e/cargo-hack/pull/131))
diff --git a/src/cli.rs b/src/cli.rs
index 77b53aab..db4962d2 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -8,7 +8,7 @@ use std::{
 
 use anyhow::{bail, format_err, Error, Result};
 
-use crate::{rustup, term, Feature, Rustup};
+use crate::{term, Feature, Rustup};
 
 pub(crate) struct Args<'a> {
     pub(crate) leading_args: Vec<&'a str>,
@@ -40,8 +40,10 @@ pub(crate) struct Args<'a> {
     pub(crate) clean_per_run: bool,
     /// --clean-per-version
     pub(crate) clean_per_version: bool,
-    /// --version-range and --version-step
-    pub(crate) version_range: Option<Vec<String>>,
+    /// --version-range
+    pub(crate) version_range: Option<&'a str>,
+    /// --version-step
+    pub(crate) version_step: Option<&'a str>,
 
     // options for --each-feature and --feature-powerset
     /// --optional-deps [DEPS]...
@@ -361,8 +363,6 @@ pub(crate) fn parse_args<'a>(raw: &'a [String], cargo: &OsStr) -> Result<Args<'a
             v.push(Feature::group(g));
             Ok(v)
         })?;
-    let version_range =
-        version_range.map(|range| rustup::version_range(range, version_step)).transpose()?;
 
     if let Some(subcommand) = subcommand {
         match subcommand {
@@ -499,6 +499,7 @@ pub(crate) fn parse_args<'a>(raw: &'a [String], cargo: &OsStr) -> Result<Args<'a
         include_features: include_features.into_iter().map(Into::into).collect(),
         include_deps_features,
         version_range,
+        version_step,
 
         depth,
         group_features,
diff --git a/src/context.rs b/src/context.rs
index d5bd9a78..ee21ad22 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -14,7 +14,7 @@ use crate::{
     features::Features,
     manifest::Manifest,
     metadata::{Metadata, Package, PackageId},
-    restore, term, ProcessBuilder,
+    restore, rustup, term, ProcessBuilder,
 };
 
 pub(crate) struct Context<'a> {
@@ -25,6 +25,7 @@ pub(crate) struct Context<'a> {
     cargo: PathBuf,
     pub(crate) restore: restore::Manager,
     pub(crate) current_dir: PathBuf,
+    pub(crate) version_range: Option<Vec<String>>,
 }
 
 impl<'a> Context<'a> {
@@ -45,6 +46,11 @@ impl<'a> Context<'a> {
             bail!("--include-deps-features requires Cargo 1.41 or later");
         }
 
+        let version_range = args
+            .version_range
+            .map(|range| rustup::version_range(range, args.version_step, &metadata))
+            .transpose()?;
+
         let mut pkg_features = HashMap::with_capacity(metadata.workspace_members.len());
         for id in &metadata.workspace_members {
             let features = Features::new(&metadata, id);
@@ -59,6 +65,7 @@ impl<'a> Context<'a> {
             cargo: cargo.into(),
             restore,
             current_dir: env::current_dir()?,
+            version_range,
         };
 
         // Only a few options require information from cargo manifest.
diff --git a/src/rustup.rs b/src/rustup.rs
index f7c4e8cf..455ad489 100644
--- a/src/rustup.rs
+++ b/src/rustup.rs
@@ -2,7 +2,7 @@ use std::str;
 
 use anyhow::{bail, format_err, Result};
 
-use crate::{cargo, version::Version};
+use crate::{cargo, metadata::Metadata, version::Version};
 
 pub(crate) struct Rustup {
     pub(crate) version: u32,
@@ -19,7 +19,11 @@ impl Rustup {
     }
 }
 
-pub(crate) fn version_range(range: &str, step: Option<&str>) -> Result<Vec<String>> {
+pub(crate) fn version_range(
+    range: &str,
+    step: Option<&str>,
+    metadata: &Metadata,
+) -> Result<Vec<String>> {
     let check = |version: &Version| {
         if version.major != 1 {
             bail!("major version must be 1");
@@ -35,7 +39,26 @@ pub(crate) fn version_range(range: &str, step: Option<&str>) -> Result<Vec<Strin
     };
 
     let mut split = range.splitn(2, "..");
-    let start = split.next().map(str::parse).unwrap()?;
+    let start = match split.next().unwrap() {
+        "" => {
+            let mut rust_version = None;
+            for id in &metadata.workspace_members {
+                let v = metadata.packages[id].rust_version.as_deref();
+                if v.is_none() || v == rust_version {
+                    // no-op
+                } else if rust_version.is_none() {
+                    rust_version = v;
+                } else {
+                    bail!("automatic detection of the lower bound of the version range is not yet supported when the minimum supported Rust version of the crates in the workspace do not match")
+                }
+            }
+            match rust_version {
+                Some(v) => v.parse()?,
+                None => bail!("no rust-version field in Cargo.toml is specified"),
+            }
+        }
+        s => s.parse()?,
+    };
     check(&start)?;
 
     let end = match split.next() {


Test patch:
diff --git a/tests/auxiliary/mod.rs b/tests/auxiliary/mod.rs
index 18e840ff..d657afcb 100644
--- a/tests/auxiliary/mod.rs
+++ b/tests/auxiliary/mod.rs
@@ -45,6 +45,14 @@ fn test_version() -> Option<u32> {
     *TEST_VERSION
 }
 
+pub fn has_stable_toolchain() -> bool {
+    static HAS_STABLE_TOOLCHAIN: Lazy<Option<bool>> = Lazy::new(|| {
+        let output = Command::new("rustup").args(&["toolchain", "list"]).output().ok()?;
+        Some(String::from_utf8(output.stdout).ok()?.contains("stable"))
+    });
+    HAS_STABLE_TOOLCHAIN.unwrap_or_default()
+}
+
 pub fn cargo_hack<O: AsRef<OsStr>>(args: impl AsRef<[O]>) -> Command {
     let args = args.as_ref();
     let mut cmd = cargo_bin_exe();
diff --git a/tests/test.rs b/tests/test.rs
index d671345e..12adabd4 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -4,7 +4,9 @@ mod auxiliary;
 
 use std::env;
 
-use auxiliary::{cargo_bin_exe, cargo_hack, target_triple, CommandExt, SEPARATOR};
+use auxiliary::{
+    cargo_bin_exe, cargo_hack, has_stable_toolchain, target_triple, CommandExt, SEPARATOR,
+};
 
 #[test]
 fn failures() {
@@ -590,7 +592,7 @@ fn powerset_deduplication_include_deps_features() {
     // TODO: Since easytime/default depends on easytime/std, their combination should be excluded,
     // but it's not working yet because include-deps-features itself isn't fully implemented.
     cargo_hack(["check", "--feature-powerset", "--include-deps-features"])
-        .assert_success2("powerset_deduplication", Some(41))
+        .assert_success2("powerset_deduplication",  Some(if has_stable_toolchain() { 34 } else { 41 }))
         .stderr_contains(
             "
             running `cargo check --no-default-features` on deduplication (1/41)
@@ -985,7 +987,7 @@ fn each_feature_all() {
 #[test]
 fn include_deps_features() {
     cargo_hack(["check", "--each-feature", "--include-deps-features"])
-        .assert_success2("powerset_deduplication", Some(41))
+        .assert_success2("powerset_deduplication",  Some(if has_stable_toolchain() { 34 } else { 41 }))
         .stderr_contains(
             "
             running `cargo check --no-default-features` on deduplication (1/9)