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

Issue:
Use the latest stable toolchain to get the metadata if installed
https://github.com/crossbeam-rs/crossbeam/runs/4074910514?check_suite_focus=true


Discussion / hints:

Fix patch:
diff --git a/.clippy.toml b/.clippy.toml
index eb66960a..585e5060 100644
--- a/.clippy.toml
+++ b/.clippy.toml
@@ -1,1 +1,6 @@
 msrv = "1.46"
+disallowed-methods = [
+    # https://internals.rust-lang.org/t/synchronized-ffi-access-to-posix-environment-variable-functions/15475
+    { path = "std::env::remove_var", reason = "this function should be considered `unsafe`" },
+    { path = "std::env::set_var", reason = "this function should be considered `unsafe`" },
+]
diff --git a/.editorconfig b/.editorconfig
index b8df5b20..33edacbe 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -14,6 +14,9 @@ trim_trailing_whitespace = true
 [*.{json,md,yml}]
 indent_size = 2
 
+[Makefile]
+indent_style = tab
+
 [*.yml]
 quote_type = single
 
diff --git a/.gitignore b/.gitignore
index a5a03e45..2c324bff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 target
 Cargo.lock
+tmp
 
 # For platform and editor specific settings, it is recommended to add to
 # a global .gitignore file.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51f191dd..21813c3a 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]
 
+- 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))
+
 ## [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/Makefile b/Makefile
new file mode 100644
index 00000000..6bd3fd3d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,4 @@
+.DEFAULT_GOAL := install
+.PHONY: install
+install:
+	@cargo install -f --path . --debug
diff --git a/src/cargo.rs b/src/cargo.rs
index 2a4c68da..0783bb25 100644
--- a/src/cargo.rs
+++ b/src/cargo.rs
@@ -1,31 +1,7 @@
-use std::{env, ffi::OsString};
-
 use anyhow::{bail, format_err, Result};
 
 use crate::{version::Version, ProcessBuilder};
 
-pub(crate) struct Cargo {
-    path: OsString,
-    pub(crate) version: u32,
-}
-
-impl Cargo {
-    pub(crate) fn new() -> Self {
-        let path = cargo_binary();
-
-        // If failed to determine cargo version, assign 0 to skip all version-dependent decisions.
-        let version = minor_version(process!(&path))
-            .map_err(|e| warn!("unable to determine cargo version: {:#}", e))
-            .unwrap_or(0);
-
-        Self { path, version }
-    }
-
-    pub(crate) fn process<'a>(&self) -> ProcessBuilder<'a> {
-        process!(&self.path)
-    }
-}
-
 // The version detection logic is based on https://github.com/cuviper/autocfg/blob/1.0.1/src/version.rs#L25-L59
 pub(crate) fn minor_version(mut cmd: ProcessBuilder<'_>) -> Result<u32> {
     cmd.args(&["--version", "--verbose"]);
@@ -52,8 +28,3 @@ pub(crate) fn minor_version(mut cmd: ProcessBuilder<'_>) -> Result<u32> {
 
     Ok(version.minor)
 }
-
-fn cargo_binary() -> OsString {
-    env::var_os("CARGO_HACK_CARGO_SRC")
-        .unwrap_or_else(|| env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")))
-}
diff --git a/src/cli.rs b/src/cli.rs
index e5ca2cb4..77b53aab 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,8 +1,14 @@
-use std::{env, ffi::OsString, fmt, iter::Peekable, mem};
+use std::{
+    env,
+    ffi::{OsStr, OsString},
+    fmt,
+    iter::Peekable,
+    mem,
+};
 
 use anyhow::{bail, format_err, Error, Result};
 
-use crate::{rustup, term, Cargo, Feature, Rustup};
+use crate::{rustup, term, Feature, Rustup};
 
 pub(crate) struct Args<'a> {
     pub(crate) leading_args: Vec<&'a str>,
@@ -64,11 +70,9 @@ pub(crate) struct Args<'a> {
     /// --features <FEATURES>...
     pub(crate) features: Vec<&'a str>,
 
-    // propagated (as a part of leading_args) to cargo
+    // propagated to cargo (as a part of leading_args)
     /// --no-default-features
     pub(crate) no_default_features: bool,
-    /// -v, --verbose, -vv
-    pub(crate) verbose: bool,
     // Note: specifying multiple `--target` flags requires unstable `-Z multitarget`,
     // so cargo-hack currently only supports a single `--target`.
     /// --target <TRIPLE>...
@@ -93,7 +97,7 @@ fn handle_args(args: impl IntoIterator<Item = impl Into<OsString>>) -> Result<Ve
         .collect()
 }
 
-pub(crate) fn parse_args<'a>(raw: &'a [String]) -> Result<(Args<'a>, Cargo)> {
+pub(crate) fn parse_args<'a>(raw: &'a [String], cargo: &OsStr) -> Result<Args<'a>> {
     let mut iter = raw.iter();
     let args = &mut iter.by_ref().map(String::as_str).peekable();
     match args.next() {
@@ -448,9 +452,7 @@ pub(crate) fn parse_args<'a>(raw: &'a [String]) -> Result<(Args<'a>, Cargo)> {
             print_version();
             std::process::exit(0);
         } else if leading.contains(&"--list") {
-            let mut line = Cargo::new().process();
-            line.arg("--list");
-            line.run()?;
+            cmd!(cargo, "--list").run()?;
             std::process::exit(0);
         } else if !remove_dev_deps {
             // TODO: improve this
@@ -458,12 +460,8 @@ pub(crate) fn parse_args<'a>(raw: &'a [String]) -> Result<(Args<'a>, Cargo)> {
         }
     }
 
-    let cargo = Cargo::new();
     let rustup = Rustup::new();
 
-    if cargo.version < 41 && include_deps_features {
-        bail!("--include-deps-features requires Cargo 1.41 or later");
-    }
     if rustup.version < 23 && version_range.is_some() {
         bail!("--version-range requires rustup 1.23 or later");
     }
@@ -478,45 +476,42 @@ pub(crate) fn parse_args<'a>(raw: &'a [String]) -> Result<(Args<'a>, Cargo)> {
     exclude_all_features |= !include_features.is_empty() || !exclude_features.is_empty();
     exclude_features.extend_from_slice(&features);
 
-    Ok((
-        Args {
-            leading_args: leading,
-            trailing_args: iter.as_slice(),
-
-            subcommand,
-
-            manifest_path,
-            package,
-            exclude,
-            workspace: workspace.is_some(),
-            each_feature,
-            feature_powerset,
-            no_dev_deps,
-            remove_dev_deps,
-            ignore_private,
-            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,
-
-            depth,
-            group_features,
-
-            exclude_features,
-            exclude_no_default_features,
-            exclude_all_features,
-
-            features,
-
-            no_default_features,
-            verbose,
-            target,
-        },
-        cargo,
-    ))
+    term::set_verbose(verbose);
+    Ok(Args {
+        leading_args: leading,
+        trailing_args: iter.as_slice(),
+
+        subcommand,
+
+        manifest_path,
+        package,
+        exclude,
+        workspace: workspace.is_some(),
+        each_feature,
+        feature_powerset,
+        no_dev_deps,
+        remove_dev_deps,
+        ignore_private,
+        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,
+
+        depth,
+        group_features,
+
+        exclude_features,
+        exclude_no_default_features,
+        exclude_all_features,
+
+        features,
+
+        no_default_features,
+        target,
+    })
 }
 
 fn parse_opt<'a>(
@@ -881,7 +876,6 @@ mod tests {
     use std::{env, panic, path::Path, process::Command};
 
     use anyhow::Result;
-    use tempfile::Builder;
 
     use super::Help;
     use crate::fs;
@@ -911,16 +905,18 @@ mod tests {
         let expected = fs::read_to_string(expected_path).unwrap();
         if expected != actual {
             if env::var_os("CI").is_some() {
-                let outdir = Builder::new().prefix("assert_diff").tempdir().unwrap();
+                let outdir = tempfile::tempdir().unwrap();
                 let actual_path = &outdir.path().join(expected_path.file_name().unwrap());
                 fs::write(actual_path, actual).unwrap();
                 let status = Command::new("git")
                     .args(&["--no-pager", "diff", "--no-index", "--"])
-                    .args(&[expected_path, actual_path])
+                    .args(&[expected_path.strip_prefix(manifest_dir).unwrap(), actual_path])
+                    .current_dir(manifest_dir)
                     .status()
                     .unwrap();
                 assert!(!status.success());
-                panic!("assertion failed");
+                // patch -p1 <<'EOF' ... EOF
+                panic!("assertion failed; please run test locally and commit resulting changes, or apply above diff as patch");
             } else {
                 fs::write(expected_path, actual).unwrap();
             }
diff --git a/src/context.rs b/src/context.rs
index b86d3556..d5bd9a78 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -1,18 +1,20 @@
 use std::{
     borrow::Cow,
     collections::HashMap,
-    env, ops,
+    env,
+    ffi::OsString,
+    ops,
     path::{Path, PathBuf},
 };
 
-use anyhow::Result;
+use anyhow::{bail, Result};
 
 use crate::{
     cli::{self, Args},
     features::Features,
     manifest::Manifest,
     metadata::{Metadata, Package, PackageId},
-    Cargo, ProcessBuilder,
+    restore, term, ProcessBuilder,
 };
 
 pub(crate) struct Context<'a> {
@@ -20,19 +22,28 @@ pub(crate) struct Context<'a> {
     metadata: Metadata,
     manifests: HashMap<PackageId, Manifest>,
     pkg_features: HashMap<PackageId, Features>,
-    cargo: Cargo,
+    cargo: PathBuf,
+    pub(crate) restore: restore::Manager,
     pub(crate) current_dir: PathBuf,
 }
 
 impl<'a> Context<'a> {
     pub(crate) fn new(args: &'a [String]) -> Result<Self> {
-        let (args, cargo) = cli::parse_args(args)?;
+        let cargo = env::var_os("CARGO_HACK_CARGO_SRC")
+            .unwrap_or_else(|| env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")));
+        let args = cli::parse_args(args, &cargo)?;
         assert!(
             args.subcommand.is_some() || args.remove_dev_deps,
             "no subcommand or valid flag specified"
         );
 
-        let metadata = Metadata::new(&args, &cargo)?;
+        let mut restore = restore::Manager::new(true);
+        let metadata = Metadata::new(&args, &cargo, &restore)?;
+        // if `--remove-dev-deps` flag is off, restore manifest file.
+        restore.needs_restore = args.no_dev_deps && !args.remove_dev_deps;
+        if metadata.cargo_version < 41 && args.include_deps_features {
+            bail!("--include-deps-features requires Cargo 1.41 or later");
+        }
 
         let mut pkg_features = HashMap::with_capacity(metadata.workspace_members.len());
         for id in &metadata.workspace_members {
@@ -45,7 +56,8 @@ impl<'a> Context<'a> {
             metadata,
             manifests: HashMap::new(),
             pkg_features,
-            cargo,
+            cargo: cargo.into(),
+            restore,
             current_dir: env::current_dir()?,
         };
 
@@ -91,7 +103,7 @@ impl<'a> Context<'a> {
     }
 
     pub(crate) fn is_private(&self, id: &PackageId) -> bool {
-        if self.cargo.version >= 39 {
+        if self.metadata.cargo_version >= 39 {
             !self.packages(id).publish
         } else {
             !self.manifests(id).publish
@@ -100,7 +112,7 @@ impl<'a> Context<'a> {
 
     pub(crate) fn name_verbose(&self, id: &PackageId) -> Cow<'_, str> {
         let package = self.packages(id);
-        if self.verbose {
+        if term::verbose() {
             Cow::Owned(format!(
                 "{} ({})",
                 package.name,
@@ -113,15 +125,13 @@ impl<'a> Context<'a> {
 
     /// Return `true` if options that require information from cargo manifest is specified.
     pub(crate) fn require_manifest_info(&self) -> bool {
-        (self.cargo.version < 39 && self.ignore_private) || self.no_dev_deps || self.remove_dev_deps
+        (self.metadata.cargo_version < 39 && self.ignore_private)
+            || self.no_dev_deps
+            || self.remove_dev_deps
     }
 
     pub(crate) fn cargo(&self) -> ProcessBuilder<'_> {
-        let mut cmd = self.cargo.process();
-        if self.verbose {
-            cmd.display_manifest_path();
-        }
-        cmd
+        cmd!(&self.cargo)
     }
 }
 
diff --git a/src/main.rs b/src/main.rs
index 9e7c4aa6..52088855 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,11 @@
 #![forbid(unsafe_code)]
 #![warn(rust_2018_idioms, single_use_lifetimes, unreachable_pub)]
-#![warn(clippy::default_trait_access, clippy::wildcard_imports)]
+#![warn(
+    clippy::default_trait_access,
+    clippy::disallowed_methods,
+    clippy::disallowed_types,
+    clippy::wildcard_imports
+)]
 
 #[macro_use]
 mod term;
@@ -25,13 +30,15 @@ use std::fmt::Write;
 use anyhow::{bail, Result};
 
 use crate::{
-    cargo::Cargo, context::Context, features::Feature, metadata::PackageId,
-    process::ProcessBuilder, restore::Restore, rustup::Rustup,
+    context::Context, features::Feature, metadata::PackageId, process::ProcessBuilder,
+    rustup::Rustup,
 };
 
 fn main() {
     if let Err(e) = try_main() {
         error!("{:#}", e);
+    }
+    if term::has_error() {
         std::process::exit(1)
     }
 }
@@ -54,13 +61,9 @@ fn exec_on_workspace(cx: &Context<'_>) -> Result<()> {
 
     let mut progress = Progress::default();
     let packages = determine_package_list(cx, &mut progress)?;
-    let restore = Restore::new(cx);
     if let Some(range) = &cx.version_range {
         progress.total *= range.len();
-        let mut line = process!("cargo");
-        if cx.verbose {
-            line.display_manifest_path();
-        }
+        let line = cmd!("cargo");
         {
             // First, generate the lockfile using the oldest cargo specified.
             // https://github.com/taiki-e/cargo-hack/issues/105
@@ -68,7 +71,7 @@ fn exec_on_workspace(cx: &Context<'_>) -> Result<()> {
             rustup::install_toolchain(toolchain, cx.target, true)?;
             let mut line = line.clone();
             line.leading_arg(toolchain);
-            line.args(&["generate-lockfile"]);
+            line.arg("generate-lockfile");
             if let Some(pid) = cx.current_package() {
                 let package = cx.packages(pid);
                 line.arg("--manifest-path");
@@ -94,16 +97,16 @@ fn exec_on_workspace(cx: &Context<'_>) -> Result<()> {
             let mut line = line.clone();
             line.leading_arg(toolchain);
             line.apply_context(cx);
-            packages.iter().try_for_each(|(id, kind)| {
-                exec_on_package(cx, id, kind, &line, &restore, &mut progress)
-            })
+            packages
+                .iter()
+                .try_for_each(|(id, kind)| exec_on_package(cx, id, kind, &line, &mut progress))
         })
     } else {
         let mut line = cx.cargo();
         line.apply_context(cx);
-        packages.iter().try_for_each(|(id, kind)| {
-            exec_on_package(cx, id, kind, &line, &restore, &mut progress)
-        })
+        packages
+            .iter()
+            .try_for_each(|(id, kind)| exec_on_package(cx, id, kind, &line, &mut progress))
     }
 }
 
@@ -255,7 +258,6 @@ fn exec_on_package(
     id: &PackageId,
     kind: &Kind<'_>,
     line: &ProcessBuilder<'_>,
-    restore: &Restore,
     progress: &mut Progress,
 ) -> Result<()> {
     if let Kind::SkipAsPrivate = kind {
@@ -272,7 +274,7 @@ fn exec_on_package(
 
     if cx.no_dev_deps || cx.remove_dev_deps {
         let new = cx.manifests(id).remove_dev_deps();
-        let mut handle = restore.set_manifest(cx, id);
+        let mut handle = cx.restore.set(&cx.manifests(id).raw, &cx.packages(id).manifest_path);
 
         fs::write(&package.manifest_path, new)?;
 
@@ -368,7 +370,7 @@ fn exec_cargo(
 
     // running `<command>` (on <package>) (<count>/<total>)
     let mut msg = String::new();
-    if cx.verbose {
+    if term::verbose() {
         write!(msg, "running {}", line).unwrap();
     } else {
         write!(msg, "running {} on {}", line, cx.packages(id).name).unwrap();
@@ -387,7 +389,7 @@ fn cargo_clean(cx: &Context<'_>, id: Option<&PackageId>) -> Result<()> {
         line.arg(&cx.packages(id).name);
     }
 
-    if cx.verbose {
+    if term::verbose() {
         // running `cargo clean [--package <package>]`
         info!("running {}", line);
     }
diff --git a/src/metadata.rs b/src/metadata.rs
index a4c0cb89..5aa58efd 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -6,14 +6,15 @@
 
 use std::{
     collections::{BTreeMap, HashMap},
-    path::PathBuf,
+    ffi::OsStr,
+    path::{Path, PathBuf},
     rc::Rc,
 };
 
 use anyhow::{format_err, Context as _, Result};
 use serde_json::{Map, Value};
 
-use crate::{cli::Args, Cargo};
+use crate::{cargo, cli::Args, fs, restore, term};
 
 type Object = Map<String, Value>;
 type ParseResult<T> = Result<T, &'static str>;
@@ -33,6 +34,7 @@ impl From<String> for PackageId {
 }
 
 pub(crate) struct Metadata {
+    pub(crate) cargo_version: u32,
     /// List of all packages in the workspace and all feature-enabled dependencies.
     pub(crate) packages: HashMap<PackageId, Package>,
     /// List of members of the workspace.
@@ -44,35 +46,91 @@ pub(crate) struct Metadata {
 }
 
 impl Metadata {
-    pub(crate) fn new(args: &Args<'_>, cargo: &Cargo) -> Result<Self> {
-        let mut cmd = cargo.process();
-        cmd.args(&["metadata", "--format-version=1"]);
-        if let Some(manifest_path) = &args.manifest_path {
-            cmd.arg("--manifest-path");
-            cmd.arg(manifest_path);
-        }
-        let output = cmd.run_with_output()?;
-
-        let map =
-            serde_json::from_slice(&output.stdout).context("failed to parse metadata as json")?;
-        Self::from_obj(map, cargo)
+    pub(crate) fn new(args: &Args<'_>, cargo: &OsStr, restore: &restore::Manager) -> Result<Self> {
+        // If failed to determine cargo version, assign 0 to skip all version-dependent decisions.
+        let mut cargo_version = cargo::minor_version(cmd!(cargo))
+            .map_err(|e| warn!("unable to determine cargo version: {:#}", e))
+            .unwrap_or(0);
+        let stable_cargo_version = cargo::minor_version(cmd!("cargo", "+stable")).unwrap_or(0);
+
+        let mut cmd;
+        let json = if stable_cargo_version > cargo_version {
+            cmd = cmd!(cargo, "metadata", "--format-version=1", "--no-deps");
+            if let Some(manifest_path) = &args.manifest_path {
+                cmd.arg("--manifest-path");
+                cmd.arg(manifest_path);
+            }
+            let no_deps: Object = serde_json::from_str(&cmd.read()?)
+                .with_context(|| format!("failed to parse output from {}", cmd))?;
+            let lockfile =
+                Path::new(no_deps["workspace_root"].as_str().unwrap()).join("Cargo.lock");
+            if !lockfile.exists() {
+                let mut cmd = cmd!(cargo, "generate-lockfile");
+                if let Some(manifest_path) = &args.manifest_path {
+                    cmd.arg("--manifest-path");
+                    cmd.arg(manifest_path);
+                }
+                cmd.run_with_output()?;
+            }
+            let guard = term::scoped_verbose(false);
+            let mut handle = restore.set(&fs::read_to_string(&lockfile)?, lockfile);
+
+            // Try with stable cargo because if workspace member has
+            // a dependency that requires newer cargo features, `cargo metadata`
+            // with older cargo may fail.
+            cmd = cmd!("cargo", "+stable", "metadata", "--format-version=1");
+            if let Some(manifest_path) = &args.manifest_path {
+                cmd.arg("--manifest-path");
+                cmd.arg(manifest_path);
+            }
+            let json = cmd.read();
+            handle.close()?;
+            drop(guard);
+            match json {
+                Ok(json) => {
+                    cargo_version = stable_cargo_version;
+                    json
+                }
+                Err(_e) => {
+                    // If failed, try again with the version of cargo we will actually use.
+                    cmd = cmd!(cargo, "metadata", "--format-version=1");
+                    if let Some(manifest_path) = &args.manifest_path {
+                        cmd.arg("--manifest-path");
+                        cmd.arg(manifest_path);
+                    }
+                    cmd.read()?
+                }
+            }
+        } else {
+            cmd = cmd!(cargo, "metadata", "--format-version=1");
+            if let Some(manifest_path) = &args.manifest_path {
+                cmd.arg("--manifest-path");
+                cmd.arg(manifest_path);
+            }
+            cmd.read()?
+        };
+
+        let map = serde_json::from_str(&json)
+            .with_context(|| format!("failed to parse output from {}", cmd))?;
+        Self::from_obj(map, cargo_version)
             .map_err(|s| format_err!("failed to parse `{}` field from metadata", s))
     }
 
-    fn from_obj(mut map: Object, cargo: &Cargo) -> ParseResult<Self> {
+    fn from_obj(mut map: Object, cargo_version: u32) -> ParseResult<Self> {
         let workspace_members: Vec<_> = map
             .remove_array("workspace_members")?
             .into_iter()
             .map(|v| into_string(v).ok_or("workspace_members"))
             .collect::<Result<_, _>>()?;
         Ok(Self {
+            cargo_version,
             packages: map
                 .remove_array("packages")?
                 .into_iter()
-                .map(|v| Package::from_value(v, cargo))
+                .map(|v| Package::from_value(v, cargo_version))
                 .collect::<Result<_, _>>()?,
             workspace_members,
-            resolve: Resolve::from_obj(map.remove_object("resolve")?, cargo)?,
+            resolve: Resolve::from_obj(map.remove_object("resolve")?, cargo_version)?,
             workspace_root: map.remove_string("workspace_root")?,
         })
     }
@@ -88,12 +146,12 @@ pub(crate) struct Resolve {
 }
 
 impl Resolve {
-    fn from_obj(mut map: Object, cargo: &Cargo) -> ParseResult<Self> {
+    fn from_obj(mut map: Object, cargo_version: u32) -> ParseResult<Self> {
         Ok(Self {
             nodes: map
                 .remove_array("nodes")?
                 .into_iter()
-                .map(|v| Node::from_value(v, cargo))
+                .map(|v| Node::from_value(v, cargo_version))
                 .collect::<Result<_, _>>()?,
             root: map.remove_nullable("root", into_string)?,
         })
@@ -109,16 +167,16 @@ pub(crate) struct Node {
 }
 
 impl Node {
-    fn from_value(mut value: Value, cargo: &Cargo) -> ParseResult<(PackageId, Self)> {
+    fn from_value(mut value: Value, cargo_version: u32) -> ParseResult<(PackageId, Self)> {
         let map = value.as_object_mut().ok_or("nodes")?;
 
         let id = map.remove_string("id")?;
         Ok((id, Self {
             // This field was added in Rust 1.30.
-            deps: if cargo.version >= 30 {
+            deps: if cargo_version >= 30 {
                 map.remove_array("deps")?
                     .into_iter()
-                    .map(|v| NodeDep::from_value(v, cargo))
+                    .map(|v| NodeDep::from_value(v, cargo_version))
                     .collect::<Result<_, _>>()?
             } else {
                 Vec::new()
@@ -138,13 +196,13 @@ pub(crate) struct NodeDep {
 }
 
 impl NodeDep {
-    fn from_value(mut value: Value, cargo: &Cargo) -> ParseResult<Self> {
+    fn from_value(mut value: Value, cargo_version: u32) -> ParseResult<Self> {
         let map = value.as_object_mut().ok_or("deps")?;
 
         Ok(Self {
             pkg: map.remove_string("pkg")?,
             // This field was added in Rust 1.41.
-            dep_kinds: if cargo.version >= 41 {
+            dep_kinds: if cargo_version >= 41 {
                 map.remove_array("dep_kinds")?
                     .into_iter()
                     .map(DepKindInfo::from_value)
@@ -191,10 +249,15 @@ pub(crate) struct Package {
     ///
     /// This is always `true` if running with a version of Cargo older than 1.39.
     pub(crate) publish: bool,
+    /// The minimum supported Rust version of this package.
+    ///
+    /// This is always `None` if running with a version of Cargo older than 1.58.
+    #[allow(dead_code)]
+    pub(crate) rust_version: Option<String>,
 }
 
 impl Package {
-    fn from_value(mut value: Value, cargo: &Cargo) -> ParseResult<(PackageId, Self)> {
+    fn from_value(mut value: Value, cargo_version: u32) -> ParseResult<(PackageId, Self)> {
         let map = value.as_object_mut().ok_or("packages")?;
 
         let id = map.remove_string("id")?;
@@ -218,12 +281,18 @@ impl Package {
                 .ok_or("features")?,
             manifest_path: map.remove_string("manifest_path")?,
             // This field was added in Rust 1.39.
-            publish: if cargo.version >= 39 {
+            publish: if cargo_version >= 39 {
                 // Publishing is unrestricted if null, and forbidden if an empty array.
                 map.remove_nullable("publish", into_array)?.map_or(true, |a| !a.is_empty())
             } else {
                 true
             },
+            // This field was added in Rust 1.58.
+            rust_version: if cargo_version >= 58 {
+                map.remove_nullable("rust_version", into_string)?
+            } else {
+                None
+            },
         }))
     }
 }
diff --git a/src/process.rs b/src/process.rs
index 80904f89..cbe12bfd 100644
--- a/src/process.rs
+++ b/src/process.rs
@@ -10,9 +10,9 @@ use std::{
 
 use anyhow::{Context as _, Result};
 
-use crate::{Context, PackageId};
+use crate::{term, Context, PackageId};
 
-macro_rules! process {
+macro_rules! cmd {
     ($program:expr $(, $arg:expr)* $(,)?) => {{
         let mut _cmd = crate::process::ProcessBuilder::new($program);
         $(
@@ -61,8 +61,8 @@ impl<'a> ProcessBuilder<'a> {
             leading_args: Vec::new(),
             args: Vec::new(),
             features: String::new(),
-            display_program_path: false,
-            display_manifest_path: false,
+            display_program_path: term::verbose(),
+            display_manifest_path: term::verbose(),
         }
     }
 
@@ -90,7 +90,6 @@ impl<'a> ProcessBuilder<'a> {
     pub(crate) fn apply_context(&mut self, cx: &'a Context<'_>) -> &mut Self {
         self.propagated_leading_args = &cx.leading_args;
         self.trailing_args = cx.trailing_args;
-        self.display_manifest_path = cx.verbose;
         self
     }
 
@@ -117,22 +116,10 @@ impl<'a> ProcessBuilder<'a> {
         }
     }
 
-    /// Enables full program path display.
-    pub(crate) fn display_program_path(&mut self) -> &mut Self {
-        self.display_program_path = true;
-        self
-    }
-
-    /// Enables manifest path display.
-    pub(crate) fn display_manifest_path(&mut self) -> &mut Self {
-        self.display_manifest_path = true;
-        self
-    }
-
     /// Enables all display-related flags.
     fn display_all(&mut self) {
-        self.display_program_path();
-        self.display_manifest_path();
+        self.display_program_path = true;
+        self.display_manifest_path = true;
     }
 
     /// Gets the comma-separated features list
@@ -148,7 +135,6 @@ impl<'a> ProcessBuilder<'a> {
             self.display_all();
             ProcessError::new(&format!("could not execute process {}", self), None, None)
         })?;
-
         if status.success() {
             Ok(())
         } else {
@@ -169,7 +155,6 @@ impl<'a> ProcessBuilder<'a> {
             self.display_all();
             ProcessError::new(&format!("could not execute process {}", self), None, None)
         })?;
-
         if output.status.success() {
             Ok(output)
         } else {
@@ -186,8 +171,10 @@ impl<'a> ProcessBuilder<'a> {
     /// Executes a process, captures its stdio output, returning the captured
     /// standard output as a `String`.
     pub(crate) fn read(&mut self) -> Result<String> {
-        let mut output = String::from_utf8(self.run_with_output()?.stdout)
-            .with_context(|| format!("failed to parse output from {}", self))?;
+        let mut output = String::from_utf8(self.run_with_output()?.stdout).with_context(|| {
+            self.display_all();
+            format!("failed to parse output from {}", self)
+        })?;
         while output.ends_with('\n') || output.ends_with('\r') {
             output.pop();
         }
diff --git a/src/restore.rs b/src/restore.rs
index 580bc144..62063dfa 100644
--- a/src/restore.rs
+++ b/src/restore.rs
@@ -5,37 +5,24 @@ use std::{
 
 use anyhow::Result;
 
-use crate::{fs, Context, PackageId};
+use crate::{fs, term};
 
 #[derive(Clone)]
-pub(crate) struct Restore {
+pub(crate) struct Manager {
     // A flag that indicates restore is needed.
-    needs_restore: bool,
-    // Information on manifest that needs to be restored next.
+    pub(crate) needs_restore: bool,
+    // Information on file that needs to be restored next.
     // If `needs_restore` is `false`, this is always `None`.
-    current: Arc<Mutex<Option<Current>>>,
+    current: Arc<Mutex<Option<File>>>,
 }
 
-struct Current {
-    manifest: String,
-    manifest_path: PathBuf,
-}
-
-impl Restore {
-    pub(crate) fn new(cx: &Context<'_>) -> Self {
-        let this = Self {
-            // if `--remove-dev-deps` flag is off, restore manifest file.
-            needs_restore: cx.no_dev_deps && !cx.remove_dev_deps,
-            current: Arc::new(Mutex::new(None)),
-        };
+impl Manager {
+    pub(crate) fn new(needs_restore: bool) -> Self {
+        let this = Self { needs_restore, current: Arc::new(Mutex::new(None)) };
 
-        if !this.needs_restore {
-            return this;
-        }
-
-        let x = this.clone();
+        let cloned = this.clone();
         ctrlc::set_handler(move || {
-            if let Err(e) = x.restore_dev_deps() {
+            if let Err(e) = cloned.restore() {
                 error!("{:#}", e);
                 std::process::exit(1)
             }
@@ -46,34 +33,48 @@ impl Restore {
         this
     }
 
-    pub(crate) fn set_manifest(&self, cx: &Context<'_>, id: &PackageId) -> Handle<'_> {
+    pub(crate) fn set(&self, text: impl Into<String>, path: impl Into<PathBuf>) -> Handle<'_> {
         if !self.needs_restore {
             return Handle(None);
         }
 
-        *self.current.lock().unwrap() = Some(Current {
-            manifest: cx.manifests(id).raw.clone(),
-            manifest_path: cx.packages(id).manifest_path.clone(),
-        });
+        *self.current.lock().unwrap() = Some(File { text: text.into(), path: path.into() });
 
         Handle(Some(self))
     }
 
-    fn restore_dev_deps(&self) -> Result<()> {
+    fn restore(&self) -> Result<()> {
         let mut current = self.current.lock().unwrap();
-        if let Some(current) = current.take() {
-            fs::write(&current.manifest_path, &current.manifest)?;
+        if let Some(file) = current.take() {
+            file.restore()?;
         }
         Ok(())
     }
 }
 
-pub(crate) struct Handle<'a>(Option<&'a Restore>);
+struct File {
+    /// The original text of this file.
+    text: String,
+    /// Path to this file.
+    path: PathBuf,
+}
+
+impl File {
+    fn restore(self) -> Result<()> {
+        if term::verbose() {
+            info!("restoring {}", self.path.display());
+        }
+        fs::write(&self.path, &self.text)
+    }
+}
+
+#[must_use]
+pub(crate) struct Handle<'a>(Option<&'a Manager>);
 
 impl Handle<'_> {
     pub(crate) fn close(&mut self) -> Result<()> {
-        if let Some(this) = self.0.take() {
-            this.restore_dev_deps()?;
+        if let Some(manager) = self.0.take() {
+            manager.restore()?;
         }
         Ok(())
     }
diff --git a/src/rustup.rs b/src/rustup.rs
index f6d898bd..f7c4e8cf 100644
--- a/src/rustup.rs
+++ b/src/rustup.rs
@@ -41,7 +41,7 @@ pub(crate) fn version_range(range: &str, step: Option<&str>) -> Result<Vec<Strin
     let end = match split.next() {
         Some("") | None => {
             install_toolchain("stable", None, false)?;
-            cargo::minor_version(process!("cargo", "+stable"))?
+            cargo::minor_version(cmd!("cargo", "+stable"))?
         }
         Some(end) => {
             let end = end.parse()?;
@@ -73,7 +73,7 @@ pub(crate) fn install_toolchain(
     }
 
     if target.is_none()
-        && process!("cargo", format!("+{}", toolchain), "--version").run_with_output().is_ok()
+        && cmd!("cargo", format!("+{}", toolchain), "--version").run_with_output().is_ok()
     {
         // Do not run `rustup toolchain install` if the toolchain already has installed.
         return Ok(());
@@ -81,7 +81,7 @@ pub(crate) fn install_toolchain(
 
     // In Github Actions and Azure Pipelines, --no-self-update is necessary
     // because the windows environment cannot self-update rustup.exe.
-    let mut cmd = process!("rustup", "toolchain", "install", toolchain, "--no-self-update");
+    let mut cmd = cmd!("rustup", "toolchain", "add", toolchain, "--no-self-update");
     if let Some(target) = target {
         cmd.args(&["--target", target]);
     }
@@ -97,7 +97,7 @@ pub(crate) fn install_toolchain(
 }
 
 fn minor_version() -> Result<u32> {
-    let mut cmd = process!("rustup", "--version");
+    let mut cmd = cmd!("rustup", "--version");
     let output = cmd.read()?;
 
     let version = (|| {
diff --git a/src/term.rs b/src/term.rs
index 23d66643..94228107 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -2,18 +2,12 @@ use std::{
     env,
     io::Write,
     str::FromStr,
-    sync::atomic::{AtomicU8, Ordering::Relaxed},
+    sync::atomic::{AtomicBool, AtomicU8, Ordering},
 };
 
 use anyhow::{format_err, Result};
 use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
 
-static COLORING: AtomicU8 = AtomicU8::new(AUTO);
-
-const AUTO: u8 = Coloring::Auto as _;
-const ALWAYS: u8 = Coloring::Always as _;
-const NEVER: u8 = Coloring::Never as _;
-
 #[derive(PartialEq, Eq)]
 #[repr(u8)]
 enum Coloring {
@@ -22,6 +16,12 @@ enum Coloring {
     Never,
 }
 
+impl Coloring {
+    const AUTO: u8 = Coloring::Auto as _;
+    const ALWAYS: u8 = Coloring::Always as _;
+    const NEVER: u8 = Coloring::Never as _;
+}
+
 impl FromStr for Coloring {
     type Err = String;
 
@@ -35,6 +35,7 @@ impl FromStr for Coloring {
     }
 }
 
+static COLORING: AtomicU8 = AtomicU8::new(Coloring::AUTO);
 pub(crate) fn set_coloring(color: Option<&str>) -> Result<()> {
     let mut coloring = match color {
         Some(color) => color.parse().map_err(|e| format_err!("argument for --color {}", e))?,
@@ -50,32 +51,66 @@ pub(crate) fn set_coloring(color: Option<&str>) -> Result<()> {
     if coloring == Coloring::Auto && !atty::is(atty::Stream::Stderr) {
         coloring = Coloring::Never;
     }
-    COLORING.store(coloring as _, Relaxed);
+    // Relaxed is fine because only the argument parsing step updates this value.
+    COLORING.store(coloring as _, Ordering::Relaxed);
     Ok(())
 }
-
 fn coloring() -> ColorChoice {
-    match COLORING.load(Relaxed) {
-        AUTO => ColorChoice::Auto,
-        ALWAYS => ColorChoice::Always,
-        NEVER => ColorChoice::Never,
+    match COLORING.load(Ordering::Relaxed) {
+        Coloring::AUTO => ColorChoice::Auto,
+        Coloring::ALWAYS => ColorChoice::Always,
+        Coloring::NEVER => ColorChoice::Never,
         _ => unreachable!(),
     }
 }
 
-pub(crate) fn print_inner(color: Option<Color>, kind: &str) -> StandardStream {
+static HAS_ERROR: AtomicBool = AtomicBool::new(false);
+pub(crate) fn set_error() {
+    HAS_ERROR.store(true, Ordering::SeqCst)
+}
+pub(crate) fn has_error() -> bool {
+    HAS_ERROR.load(Ordering::SeqCst)
+}
+
+static VERBOSE: AtomicBool = AtomicBool::new(false);
+pub(crate) struct VerboseGuard {
+    prev: bool,
+}
+impl Drop for VerboseGuard {
+    fn drop(&mut self) {
+        set_verbose(self.prev);
+    }
+}
+pub(crate) fn set_verbose(verbose: bool) {
+    // TODO: CARGO_TERM_VERBOSE
+    // https://doc.rust-lang.org/nightly/cargo/reference/config.html#termverbose
+    VERBOSE.store(verbose, Ordering::Relaxed)
+}
+pub(crate) fn scoped_verbose(verbose: bool) -> VerboseGuard {
+    // TODO: CARGO_TERM_VERBOSE
+    // https://doc.rust-lang.org/nightly/cargo/reference/config.html#termverbose
+    VerboseGuard { prev: VERBOSE.swap(verbose, Ordering::Relaxed) }
+}
+pub(crate) fn verbose() -> bool {
+    VERBOSE.load(Ordering::Relaxed)
+}
+
+pub(crate) fn print_status(status: &str, color: Option<Color>) -> StandardStream {
     let mut stream = StandardStream::stderr(coloring());
     let _ = stream.set_color(ColorSpec::new().set_bold(true).set_fg(color));
-    let _ = write!(stream, "{}", kind);
+    let _ = write!(stream, "{}", status);
+    let _ = stream.set_color(ColorSpec::new().set_bold(true));
+    let _ = write!(stream, ":");
     let _ = stream.reset();
-    let _ = write!(stream, ": ");
+    let _ = write!(stream, " ");
     stream
 }
 
 macro_rules! error {
     ($($msg:expr),* $(,)?) => {{
         use std::io::Write;
-        let mut stream = crate::term::print_inner(Some(termcolor::Color::Red), "error");
+        crate::term::set_error();
+        let mut stream = crate::term::print_status("error", Some(termcolor::Color::Red));
         let _ = writeln!(stream, $($msg),*);
     }};
 }
@@ -83,7 +118,7 @@ macro_rules! error {
 macro_rules! warn {
     ($($msg:expr),* $(,)?) => {{
         use std::io::Write;
-        let mut stream = crate::term::print_inner(Some(termcolor::Color::Yellow), "warning");
+        let mut stream = crate::term::print_status("warning", Some(termcolor::Color::Yellow));
         let _ = writeln!(stream, $($msg),*);
     }};
 }
@@ -91,7 +126,7 @@ macro_rules! warn {
 macro_rules! info {
     ($($msg:expr),* $(,)?) => {{
         use std::io::Write;
-        let mut stream = crate::term::print_inner(None, "info");
+        let mut stream = crate::term::print_status("info", None);
         let _ = writeln!(stream, $($msg),*);
     }};
 }


Test patch:
diff --git a/tests/auxiliary/mod.rs b/tests/auxiliary/mod.rs
index 19a80daf..18e840ff 100644
--- a/tests/auxiliary/mod.rs
+++ b/tests/auxiliary/mod.rs
@@ -26,7 +26,7 @@ pub fn cargo_bin_exe() -> Command {
 
 fn test_toolchain() -> String {
     if let Some(toolchain) = test_version() {
-        format!(" +1.{}", toolchain)
+        format!("+1.{} ", toolchain)
     } else {
         String::new()
     }
@@ -133,10 +133,12 @@ struct AssertOutputInner {
 
 #[track_caller]
 fn line_separated(lines: &str, f: impl FnMut(&str)) {
-    let lines = if lines.contains("`cargo +") {
+    let lines = if lines.contains("cargo +")
+        || lines.contains(&format!("cargo{} +", env::consts::EXE_SUFFIX))
+    {
         lines.to_string()
     } else {
-        lines.replace("`cargo", &format!("`cargo{}", test_toolchain()))
+        lines.replace("cargo ", &format!("cargo {}", test_toolchain()))
     };
     lines.split('\n').map(str::trim).filter(|line| !line.is_empty()).for_each(f);
 }
diff --git a/tests/test.rs b/tests/test.rs
index be5a6de8..d671345e 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -1197,11 +1197,12 @@ fn list_separator() {
 fn verbose() {
     cargo_hack(["check", "--verbose"]).assert_success("virtual").stderr_contains(format!(
         "
-        running `cargo check --manifest-path member1{0}Cargo.toml`
-        running `cargo check --manifest-path member2{0}Cargo.toml`
-        running `cargo check --manifest-path dir{0}not_find_manifest{0}Cargo.toml`
+        cargo{1} check --manifest-path member1{0}Cargo.toml`
+        cargo{1} check --manifest-path member2{0}Cargo.toml`
+        cargo{1} check --manifest-path dir{0}not_find_manifest{0}Cargo.toml`
         ",
-        SEPARATOR
+        SEPARATOR,
+        env::consts::EXE_SUFFIX
     ));
 }