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

Issue:
Option to run `cargo clean` between different compiler versions
When running a full CI check, I'm running out of space on disk (with GitHub Actions). Given that a single compiler version only consists of 384 checks (not even full compilations), this shouldn't be an issue, but I'm checking on all releases since MSRV, which in my case is six full runs (2304 total checks).

If there were an option to run `cargo clean` between different compiler versions, that would be awesome. It could compliment `--clean-per-run`, which already exists. Perhaps `--clean-per-version`?

(yes, I know I could split the CI into multiple jobs. I likely will if you don't like this idea)


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae834d33..01706925 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com
 
 ## [Unreleased]
 
+- Add `--clean-per-version` flag.
+
 ## [0.5.4] - 2021-02-27
 
 - [Stop commit of `Cargo.lock`.](https://github.com/taiki-e/cargo-hack/pull/117)
diff --git a/README.md b/README.md
index 0ce4cb73..a66913d8 100644
--- a/README.md
+++ b/README.md
@@ -94,12 +94,6 @@ Skip to perform on `publish = false` packages.
 
 Skip passing `--features` to `cargo` if that feature does not exist.
 
-### --clean-per-run
-
-Remove artifacts for that package before running the command.
-
-This also works as a workaround for [rust-clippy#4612].
-
 ### --version-range
 
 Perform commands on a specified (inclusive) range of Rust versions.
@@ -122,6 +116,12 @@ This might be useful for catching issues like [termcolor#35], [regex#685],
 
 Specify the version interval of `--version-range`.
 
+### --clean-per-run
+
+Remove artifacts for that package before running the command.
+
+This also works as a workaround for [rust-clippy#4612].
+
 ### Options for adjusting the behavior of --each-feature and --feature-powerset
 
 The following flags can be used with `--each-feature` and `--feature-powerset`.
diff --git a/src/cli.rs b/src/cli.rs
index ee09cb13..1e50daff 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -32,6 +32,8 @@ pub(crate) struct Args<'a> {
     pub(crate) ignore_unknown_features: bool,
     /// --clean-per-run
     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>>,
 
@@ -111,6 +113,7 @@ pub(crate) fn parse_args<'a>(raw: &'a RawArgs, cargo: &Cargo, rustup: &Rustup) -
     let mut ignore_private = false;
     let mut ignore_unknown_features = false;
     let mut clean_per_run = false;
+    let mut clean_per_version = false;
     let mut version_range = None;
     let mut version_step = None;
 
@@ -241,6 +244,7 @@ pub(crate) fn parse_args<'a>(raw: &'a RawArgs, cargo: &Cargo, rustup: &Rustup) -
                 "--exclude-all-features" => parse_flag!(exclude_all_features),
                 "--include-deps-features" => parse_flag!(include_deps_features),
                 "--clean-per-run" => parse_flag!(clean_per_run),
+                "--clean-per-version" => parse_flag!(clean_per_version),
                 "--ignore-unknown-features" => parse_flag!(ignore_unknown_features),
                 // allow multiple uses
                 "--verbose" | "-v" | "-vv" => {
@@ -318,8 +322,13 @@ pub(crate) fn parse_args<'a>(raw: &'a RawArgs, cargo: &Cargo, rustup: &Rustup) -
             requires("--group-features", &["--feature-powerset"])?;
         }
     }
-    if version_range.is_none() && version_step.is_some() {
-        requires("--version-step", &["--version-range"])?;
+    if version_range.is_none() {
+        if version_step.is_some() {
+            requires("--version-step", &["--version-range"])?;
+        }
+        if clean_per_version {
+            requires("--clean-per-version", &["--version-range"])?;
+        }
     }
 
     let depth = depth.map(str::parse::<usize>).transpose()?;
@@ -474,6 +483,7 @@ pub(crate) fn parse_args<'a>(raw: &'a RawArgs, cargo: &Cargo, rustup: &Rustup) -
         ignore_unknown_features,
         optional_deps,
         clean_per_run,
+        clean_per_version,
         include_features: include_features.into_iter().map(Into::into).collect(),
         include_deps_features,
         version_range,
@@ -603,10 +613,6 @@ const HELP: &[HelpText<'_>] = &[
         "Skip passing --features flag to `cargo` if that feature does not exist in the package",
         &["This flag can only be used together with either --features or --include-features."],
     ),
-    ("", "--clean-per-run", "", "Remove artifacts for that package before running the command", &[
-        "If used this flag with --workspace, --each-feature, or --feature-powerset, artifacts will be removed before each run.",
-        "Note that dependencies artifacts will be preserved.",
-    ]),
     (
         "",
         "--version-range",
@@ -617,7 +623,17 @@ const HELP: &[HelpText<'_>] = &[
             "Note that ranges are always inclusive ranges.",
         ],
     ),
-    ("", "--version-step", "<NUM>", "Specify the version interval of --version-range", &[]),
+    ("", "--version-step", "<NUM>", "Specify the version interval of --version-range", &[
+        "This flag can only be used together with --version-range flag.",
+    ]),
+    ("", "--clean-per-run", "", "Remove artifacts for that package before running the command", &[
+        "If used this flag with --workspace, --each-feature, or --feature-powerset, artifacts will be removed before each run.",
+        "Note that dependencies artifacts will be preserved.",
+    ]),
+    ("", "--clean-per-version", "", "Remove artifacts per Rust version", &[
+        "Note that dependencies artifacts will also be removed.",
+        "This flag can only be used together with --version-range flag.",
+    ]),
     ("-v", "--verbose", "", "Use verbose output", &[]),
     ("", "--color", "<WHEN>", "Coloring: auto, always, never", &[
         "This flag will be propagated to cargo.",
@@ -824,24 +840,24 @@ For more information try --help
     )
 }
 
-// `a` requires one of `b`.
-fn requires(a: &str, b: &[&str]) -> Result<()> {
-    let with = match b.len() {
+// `flag` requires one of `requires`.
+fn requires(flag: &str, requires: &[&str]) -> Result<()> {
+    let with = match requires.len() {
         0 => unreachable!(),
-        1 => b[0].to_string(),
-        2 => format!("either {} or {}", b[0], b[1]),
+        1 => requires[0].to_string(),
+        2 => format!("either {} or {}", requires[0], requires[1]),
         _ => {
             let mut with = String::new();
-            for f in b.iter().take(b.len() - 1) {
+            for f in requires.iter().take(requires.len() - 1) {
                 with += f;
                 with += ", ";
             }
             with += "or ";
-            with += b.last().unwrap();
+            with += requires.last().unwrap();
             with
         }
     };
-    bail!("{} can only be used together with {}", a, with);
+    bail!("{} can only be used together with {}", flag, with);
 }
 
 fn conflicts(a: &str, b: &str) -> Result<()> {
diff --git a/src/main.rs b/src/main.rs
index 6a2ae1c6..2445de03 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -83,6 +83,11 @@ fn exec_on_workspace(cx: &Context<'_>) -> Result<()> {
             if i != 0 {
                 rustup::install_toolchain(toolchain, cx.target, true)?;
             }
+
+            if cx.clean_per_version {
+                cargo_clean(cx, None)?;
+            }
+
             let mut line = line.clone();
             line.leading_arg(toolchain);
             line.with_args(cx);
@@ -357,7 +362,7 @@ fn exec_cargo(
     progress.count += 1;
 
     if cx.clean_per_run {
-        cargo_clean(cx, id)?;
+        cargo_clean(cx, Some(id))?;
     }
 
     // running `<command>` (on <package>) (<count>/<total>)
@@ -373,14 +378,16 @@ fn exec_cargo(
     line.exec()
 }
 
-fn cargo_clean(cx: &Context<'_>, id: &PackageId) -> Result<()> {
+fn cargo_clean(cx: &Context<'_>, id: Option<&PackageId>) -> Result<()> {
     let mut line = cx.cargo();
     line.arg("clean");
-    line.arg("--package");
-    line.arg(&cx.packages(id).name);
+    if let Some(id) = id {
+        line.arg("--package");
+        line.arg(&cx.packages(id).name);
+    }
 
     if cx.verbose {
-        // running `cargo clean --package <package>`
+        // running `cargo clean [--package <package>]`
         info!("running {}", line);
     }
 


Test patch:
diff --git a/tests/long-help.txt b/tests/long-help.txt
index b1d00c3f..333a3bc9 100644
--- a/tests/long-help.txt
+++ b/tests/long-help.txt
@@ -103,13 +103,6 @@ OPTIONS:
 
             This flag can only be used together with either --features or --include-features.
 
-        --clean-per-run
-            Remove artifacts for that package before running the command.
-
-            If used this flag with --workspace, --each-feature, or --feature-powerset, artifacts will be removed before each run.
-
-            Note that dependencies artifacts will be preserved.
-
         --version-range <START>..[END]
             Perform commands on a specified (inclusive) range of Rust versions.
 
@@ -120,6 +113,22 @@ OPTIONS:
         --version-step <NUM>
             Specify the version interval of --version-range.
 
+            This flag can only be used together with --version-range flag.
+
+        --clean-per-run
+            Remove artifacts for that package before running the command.
+
+            If used this flag with --workspace, --each-feature, or --feature-powerset, artifacts will be removed before each run.
+
+            Note that dependencies artifacts will be preserved.
+
+        --clean-per-version
+            Remove artifacts per Rust version.
+
+            Note that dependencies artifacts will also be removed.
+
+            This flag can only be used together with --version-range flag.
+
     -v, --verbose
             Use verbose output.
 
diff --git a/tests/short-help.txt b/tests/short-help.txt
index fb354719..75a3ca22 100644
--- a/tests/short-help.txt
+++ b/tests/short-help.txt
@@ -27,9 +27,10 @@ OPTIONS:
         --remove-dev-deps                Equivalent to --no-dev-deps flag except for does not restore the original `Cargo.toml` after performed
         --ignore-private                 Skip to perform on `publish = false` packages
         --ignore-unknown-features        Skip passing --features flag to `cargo` if that feature does not exist in the package
-        --clean-per-run                  Remove artifacts for that package before running the command
         --version-range <START>..[END]   Perform commands on a specified (inclusive) range of Rust versions
         --version-step <NUM>             Specify the version interval of --version-range
+        --clean-per-run                  Remove artifacts for that package before running the command
+        --clean-per-version              Remove artifacts per Rust version
     -v, --verbose                        Use verbose output
         --color <WHEN>                   Coloring: auto, always, never
     -h, --help                           Prints help information
diff --git a/tests/test.rs b/tests/test.rs
index 93226309..23ec3ec9 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -2,6 +2,8 @@
 
 mod auxiliary;
 
+use std::env;
+
 use auxiliary::{cargo_bin_exe, cargo_hack, target_triple, CommandExt, SEPARATOR};
 
 #[test]
@@ -1327,3 +1329,15 @@ fn version_range_failure() {
             ",
         );
 }
+
+#[test]
+fn clean_per_version_failure() {
+    if env::var_os("CARGO_HACK_TEST_TOOLCHAIN").is_some() {
+        return;
+    }
+
+    // without --version-range
+    cargo_hack(["check", "--clean-per-version"])
+        .assert_failure("real")
+        .stderr_contains("--clean-per-version can only be used together with --version-range");
+}