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

Issue:
When '--all' is executed in subcrate, the command does not apply to other crates in the workspace
This is because current cargo-hack doesn't try to find the root manifest after finding the current package's manifest. (i.e., current cargo-hack always handles the current manifest as a root manifest.)

https://github.com/taiki-e/cargo-hack/blob/aae08140a3410b6d70e5d29a7d22a28e8c9e28c3/src/workspace.rs#L22-L25


Discussion / hints:
'--package' flag has the same issue.

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 31639b68..05801994 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,6 +40,7 @@ jobs:
         run: |
           cargo test
       - name: cargo hack
+        shell: bash
         run: |
           cargo run -- hack check --each-feature --no-dev-deps
           cargo run -- hack --remove-dev-deps
diff --git a/Cargo.lock b/Cargo.lock
index d9c6072b..e81df737 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -26,7 +26,8 @@ version = "0.2.1"
 dependencies = [
  "anyhow 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)",
  "easy-ext 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)",
  "termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "toml_edit 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -60,7 +61,7 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -69,12 +70,9 @@ version = "1.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
-name = "indexmap"
-version = "1.3.0"
+name = "itoa"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
-]
 
 [[package]]
 name = "libc"
@@ -129,9 +127,42 @@ name = "redox_syscall"
 version = "0.1.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "ryu"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "serde"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "serde_derive 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "syn"
-version = "1.0.7"
+version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -230,7 +261,7 @@ dependencies = [
 "checksum combine 3.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680"
 "checksum easy-ext 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ed79d40452cc72aa676b63ddde7f415865b412984738a1ba11096615ab0b262c"
 "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
-"checksum indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712d7b3ea5827fcb9d4fda14bf4da5f136f0db2ae9c8f4bd4e2d1c6fde4e6db2"
+"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
 "checksum libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8"
 "checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83"
 "checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
@@ -239,7 +270,11 @@ dependencies = [
 "checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27"
 "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
 "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
-"checksum syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0e7bedb3320d0f3035594b0b723c8a28d7d336a3eda3881db79e61d676fb644c"
+"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8"
+"checksum serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)" = "0c4b39bd9b0b087684013a792c59e3e07a46a01d2322518d8a1104641a0b1be0"
+"checksum serde_derive 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)" = "ca13fc1a832f793322228923fbb3aba9f3f44444898f835d31ad1b74fa0a2bf8"
+"checksum serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)" = "2f72eb2a68a7dc3f9a691bfda9305a1c017a6215e5a4545c258500d2099a37c2"
+"checksum syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "661641ea2aa15845cddeb97dad000d22070bb5c1fb456b96c1cba883ec691e92"
 "checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e"
 "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
 "checksum toml_edit 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87f53b1aca7d5fe2e17498a38cac0e1f5a33234d5b980fb36b9402bb93b98ae4"
diff --git a/Cargo.toml b/Cargo.toml
index 202afe03..7b52094f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,9 +17,10 @@ A tool to work around some limitations on cargo.
 
 [dependencies]
 anyhow = "1.0.19"
+serde = { version = "1.0.102", features = ["derive"] }
+serde_json = "1.0.41"
 termcolor = "1.0.5"
 toml_edit = "0.1.5"
-indexmap = "1.3.0"
 
 [dev-dependencies]
 easy-ext = "0.1.6"
diff --git a/README.md b/README.md
index 27bfd7ac..0a4455fd 100644
--- a/README.md
+++ b/README.md
@@ -24,17 +24,15 @@ cargo install cargo-hack
 
 To install the current cargo-hack requires Rust 1.36 or later.
 
-*Note: cargo-hack is currently only tested on Linux and macOS. It may not work well on other platforms. See [#3] for Windows support.*
-
 ## Usage
 
 `cargo-hack` is basically wrapper of `cargo` that propagates subcommand and most of the passed flags to `cargo`, but provides additional flags and changes the behavior of some existing flags.
 
 * **`--each-feature`**
 
-  Perform for each feature *which includes `--no-default-features` and default features* of the package.
+  Perform for each feature which includes default features and `--no-default-features` of the package.
 
-  This is useful to check that each feature is working properly. (*When used for this purpose, it is recommended to use with `--no-dev-deps`.*)
+  This is useful to check that each feature is working properly. (When used for this purpose, it is recommended to use with `--no-dev-deps` to avoid [cargo#4866].)
 
   ```sh
   cargo hack check --each-feature --no-dev-deps
@@ -48,20 +46,22 @@ To install the current cargo-hack requires Rust 1.36 or later.
 
   This is a workaround for an issue that dev-dependencies leaking into normal build ([cargo#4866]).
 
+  Also, this can be used as a workaround for an issue that `cargo` does not allow publishing a package with cyclic dev-dependencies. ([cargo#4242])
+
+  ```sh
+  cargo hack publish --no-dev-deps --dry-run --allow-dirty
+  ```
+
+  Currently, using `--no-dev-deps` flag removes dev-dependencies from real manifest while cargo-hack is running and restores it when finished. See [cargo#4242] for why this is necessary.
+  Also, this behavior may change in the future on some subcommands. See also [#15].
+
 * **`--remove-dev-deps`**
 
   Equivalent to `--no-dev-deps` except for does not restore the original `Cargo.toml` after execution.
 
   This is useful to know what Cargo.toml that cargo-hack is actually using with `--no-dev-deps`.
 
-  Also, this can be used as a workaround for an issue that `cargo` does not allow publishing a package with cyclic dev-dependencies. ([cargo#4242])
-
-  ```sh
-  # This flag also works without subcommands.
-  cargo hack --remove-dev-deps
-  cargo publish --dry-run --allow-dirty
-  # Equivalent to `cargo hack publish --no-dev-deps --dry-run --allow-dirty`
-  ```
+  *This flag also works without subcommands.*
 
 * **`--ignore-private`**
 
@@ -79,7 +79,7 @@ To install the current cargo-hack requires Rust 1.36 or later.
 
 * **`--features`**, **`--no-default-features`**
 
-  Unlike `cargo` ([cargo#3620], [cargo#4106], [cargo#4463], [cargo#4753], [cargo#5015], [cargo#5364], [cargo#6195]), it can also be applied to sub-crate.
+  Unlike `cargo` ([cargo#3620], [cargo#4106], [cargo#4463], [cargo#4753], [cargo#5015], [cargo#5364], [cargo#6195]), it can also be applied to sub-crates.
 
 * **`--all`**, **`--workspace`**
 
@@ -92,13 +92,14 @@ To install the current cargo-hack requires Rust 1.36 or later.
   members=("foo" "bar")
 
   for member in ${members[@]}; do
-    cargo check --manifest-path "${member}"
+      cargo check --manifest-path "${member}"
   done
   ```
 
-  *Note that there is currently no guarantee in which order workspace members will be performed.* (This means that `cargo hack publish --all --ignore-private` does not necessarily function as you intended.)
+  Workspace members will be performed according to the order of the 'packages' fields of [cargo-metadata].
 
 [#3]: https://github.com/taiki-e/cargo-hack/issues/3
+[#15]: https://github.com/taiki-e/cargo-hack/issues/15
 [cargo#3620]: https://github.com/rust-lang/cargo/issues/3620
 [cargo#4106]: https://github.com/rust-lang/cargo/issues/4106
 [cargo#4463]: https://github.com/rust-lang/cargo/issues/4463
@@ -108,6 +109,7 @@ To install the current cargo-hack requires Rust 1.36 or later.
 [cargo#5364]: https://github.com/rust-lang/cargo/issues/5364
 [cargo#6195]: https://github.com/rust-lang/cargo/issues/6195
 [cargo#4242]: https://github.com/rust-lang/cargo/issues/4242
+[cargo-metadata]: https://doc.rust-lang.org/cargo/commands/cargo-metadata.html
 
 ## License
 
diff --git a/src/cli.rs b/src/cli.rs
index b518c0ba..c1bb0d10 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,13 +1,13 @@
-use std::{env, fs, path::PathBuf, str::FromStr};
+use std::{env, fs, path::PathBuf, rc::Rc, str::FromStr};
 
 use anyhow::{bail, format_err, Error, Result};
 use termcolor::ColorChoice;
 
-pub(crate) fn print_version() {
+fn print_version() {
     println!("{0} {1}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"),)
 }
 
-pub(crate) fn print_help() {
+fn print_help() {
     println!(
         "\
 {0} {1}
@@ -45,6 +45,7 @@ OPTIONS:
     -v, --verbose                   Use verbose output
                                     (this flag will be propagated to cargo)
         --color <WHEN>              Coloring: auto, always, never
+                                    (this flag will be propagated to cargo)
     -h, --help                      Prints help information
     -V, --version                   Prints version information
 
@@ -63,27 +64,39 @@ Some common cargo commands are (see all commands with --list):
 
 #[derive(Debug)]
 pub(crate) struct Args {
-    pub(crate) first: Vec<String>,
-    pub(crate) second: Vec<String>,
+    pub(crate) leading_args: Rc<[String]>,
+    pub(crate) trailing_args: Rc<[String]>,
 
     pub(crate) subcommand: Option<String>,
 
+    /// --manifest-path <PATH>
     pub(crate) manifest_path: Option<String>,
-    // canonicalized target-dir
+    /// (canonicalized) --target-dir <DIRECTORY>
     pub(crate) target_dir: Option<PathBuf>,
+    /// --features <FEATURES>...
+    pub(crate) features: Vec<String>,
 
+    /// -p, --package <SPEC>...
     pub(crate) package: Vec<String>,
+    /// --exclude <SPEC>...
     pub(crate) exclude: Vec<String>,
-    pub(crate) features: Vec<String>,
-
-    pub(crate) workspace: Option<String>,
+    /// --all, --workspace
+    pub(crate) workspace: bool,
+    /// --each-feature
     pub(crate) each_feature: bool,
+    /// --no-dev-deps
     pub(crate) no_dev_deps: bool,
+    /// --remove-dev-deps
     pub(crate) remove_dev_deps: bool,
+    /// --ignore-private
     pub(crate) ignore_private: bool,
+    /// --ignore-unknown-features, (--ignore-non-exist-features)
     pub(crate) ignore_unknown_features: bool,
 
+    // flags that will be propagated to cargo
+    /// --color <WHEN>
     pub(crate) color: Option<Coloring>,
+    /// -v, --verbose, -vv
     pub(crate) verbose: bool,
 }
 
@@ -102,6 +115,14 @@ impl Coloring {
             Some(Coloring::Never) => ColorChoice::Never,
         }
     }
+
+    pub(crate) fn as_str(self) -> &'static str {
+        match self {
+            Coloring::Auto => "auto",
+            Coloring::Always => "always",
+            Coloring::Never => "never",
+        }
+    }
 }
 
 impl FromStr for Coloring {
@@ -123,12 +144,15 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
     let _ = args.next(); // executable name
     match &args.next() {
         Some(a) if a == "hack" => {}
-        Some(_) | None => return Ok(None),
+        Some(_) | None => {
+            print_help();
+            return Ok(None);
+        }
     }
 
-    let mut first = Vec::new();
-
+    let mut leading = Vec::new();
     let mut subcommand: Option<String> = None;
+
     let mut target_dir = None;
     let mut manifest_path = None;
     let mut color = None;
@@ -162,7 +186,7 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
                 if subcommand.is_none() {
                     subcommand = Some(arg.clone());
                 }
-                first.push(arg);
+                leading.push(arg);
                 continue;
             }
 
@@ -177,8 +201,8 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
                             Some(next) => {
                                 if $propagate {
                                     $ident = Some(next.clone());
-                                    first.push(arg);
-                                    first.push(next);
+                                    leading.push(arg);
+                                    leading.push(next);
                                 } else {
                                     $ident = Some(next);
                                 }
@@ -189,12 +213,12 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
                         if $ident.is_some() {
                             return Err(multi_arg($help, subcommand.as_ref()));
                         }
-                        match arg.splitn(2, '=').nth(1).map(|s| s.to_string()) {
+                        match arg.splitn(2, '=').nth(1).map(ToString::to_string) {
                             None => return Err(req_arg($help, subcommand.as_ref())),
                             arg @ Some(_) => $ident = arg,
                         }
                         if $propagate {
-                            first.push(arg);
+                            leading.push(arg);
                         }
                         continue;
                     }
@@ -205,7 +229,7 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
                     if arg == $pat1 {
                         if let Some(arg) = args.next() {
                             if $allow_split {
-                                $ident.extend(arg.split(',').map(|s| s.to_string()));
+                                $ident.extend(arg.split(',').map(ToString::to_string));
                             } else {
                                 $ident.push(arg);
                             }
@@ -216,7 +240,7 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
                     } else if arg.starts_with($pat2) {
                         if let Some(arg) = arg.splitn(2, '=').nth(1) {
                             if $allow_split {
-                                $ident.extend(arg.split(',').map(|s| s.to_string()));
+                                $ident.extend(arg.split(',').map(ToString::to_string));
                             } else {
                                 $ident.push(arg.to_string());
                             }
@@ -292,7 +316,7 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
                     }
                     ignore_non_exist_features = true;
                 }
-                _ => first.push(arg),
+                _ => leading.push(arg),
             }
         }
 
@@ -302,14 +326,19 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
     let color = color.map(|c| c.parse()).transpose()?;
     *coloring = color;
 
-    if first.is_empty() && !remove_dev_deps
-        || subcommand.is_none() && first.iter().any(|a| a == "--help" || a == "-h")
+    if leading.is_empty() && !remove_dev_deps
+        || subcommand.is_none() && leading.iter().any(|a| a == "--help" || a == "-h")
     {
+        print_help();
+        return Ok(None);
+    }
+    if leading.iter().any(|a| a == "--version" || a == "-V" || a == "-vV") {
+        print_version();
         return Ok(None);
     }
 
     let target_dir = target_dir.map(fs::canonicalize).transpose()?;
-    let verbose = first.iter().any(|a| a == "--verbose" || a == "-v" || a == "-vv");
+    let verbose = leading.iter().any(|a| a == "--verbose" || a == "-v" || a == "-vv");
     if ignore_non_exist_features {
         warn!(
             color,
@@ -328,32 +357,52 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
             }
         }
     }
-    if let Some(pos) = first.iter().position(|a| match a.as_str() {
+    if let Some(pos) = leading.iter().position(|a| match a.as_str() {
         "--example" | "--examples" | "--test" | "--tests" | "--bench" | "--benches"
         | "--all-targets" => true,
         _ => false,
     }) {
         if remove_dev_deps {
-            bail!("--remove-dev-deps may not be used together with {}", first[pos])
+            bail!("--remove-dev-deps may not be used together with {}", leading[pos])
         } else if no_dev_deps {
-            bail!("--no-dev-deps may not be used together with {}", first[pos])
+            bail!("--no-dev-deps may not be used together with {}", leading[pos])
+        }
+    }
+
+    if subcommand.is_none() {
+        if leading.iter().any(|a| a == "--list") {
+            let mut line = crate::ProcessBuilder::new(crate::cargo_binary());
+            line.arg("--list");
+            line.exec()?;
+            return Ok(None);
+        } else if !remove_dev_deps {
+            // TODO: improve this
+            bail!(
+                "\
+No subcommand or valid flag specified.
+
+USAGE:
+    cargo hack [OPTIONS] [SUBCOMMAND]
+
+For more information try --help
+"
+            );
         }
     }
 
     res.map(|()| {
         Some(Args {
-            first,
-            second: args.collect(),
+            leading_args: leading.into(),
+            // shared_from_iter requires Rust 1.37
+            trailing_args: args.collect::<Vec<_>>().into(),
 
             subcommand,
             manifest_path,
             target_dir,
-
             package,
             exclude,
             features,
-
-            workspace,
+            workspace: workspace.is_some(),
             each_feature,
             no_dev_deps,
             remove_dev_deps,
diff --git a/src/main.rs b/src/main.rs
index 194a107a..2e0c40a7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,18 +7,18 @@ mod term;
 
 mod cli;
 mod manifest;
+mod metadata;
 mod process;
-mod workspace;
 
-use std::{env, ffi::OsString, fs};
+use std::{env, ffi::OsString, fs, path::Path};
 
 use anyhow::{bail, Context, Result};
 
 use crate::{
     cli::{Args, Coloring},
     manifest::{find_root_manifest_for_wd, Manifest},
+    metadata::{Metadata, Package},
     process::ProcessBuilder,
-    workspace::Workspace,
 };
 
 fn main() {
@@ -31,97 +31,92 @@ fn main() {
 
 fn try_main(coloring: &mut Option<Coloring>) -> Result<()> {
     let args = match cli::args(coloring)? {
-        None => {
-            cli::print_help();
-            return Ok(());
-        }
+        None => return Ok(()),
         Some(args) => args,
     };
 
-    if args.first.iter().any(|a| a == "--version" || a == "-V") {
-        cli::print_version();
-        return Ok(());
-    }
-    if args.subcommand.is_none() {
-        if args.first.iter().any(|a| a == "--list") {
-            let mut line = ProcessBuilder::new(cargo_binary());
-            line.arg("--list");
-            line.exec()?;
-            return Ok(());
-        } else if !args.remove_dev_deps {
-            // TODO: improve this
-            bail!(
-                "\
-No subcommand or valid flag specified.
-
-USAGE:
-    cargo hack [OPTIONS] [SUBCOMMAND]
-
-For more information try --help
-"
-            );
-        }
-    }
-
-    let current_dir = &env::current_dir()?;
+    let metadata = Metadata::new(&args)?;
 
     let current_manifest = match &args.manifest_path {
-        Some(path) => Manifest::with_manifest_path(path)?,
-        None => Manifest::new(&find_root_manifest_for_wd(&current_dir)?)?,
+        Some(path) => Manifest::new(Path::new(path))?,
+        None => Manifest::new(find_root_manifest_for_wd(&env::current_dir()?)?)?,
     };
-    let workspace = Workspace::new(&current_manifest)?;
 
-    exec_on_workspace(&args, &workspace)
+    exec_on_workspace(&args, &current_manifest, &metadata)
 }
 
-fn exec_on_workspace(args: &Args, workspace: &Workspace<'_>) -> Result<()> {
-    if args.workspace.is_some() {
+fn exec_on_workspace(args: &Args, current_manifest: &Manifest, metadata: &Metadata) -> Result<()> {
+    let mut line = ProcessBuilder::from_args(cargo_binary(), &args);
+
+    line.arg("--target-dir");
+    line.arg(args.target_dir.as_ref().unwrap_or(&metadata.target_directory));
+
+    if let Some(color) = args.color {
+        line.arg("--color");
+        line.arg(color.as_str());
+    }
+
+    if args.workspace {
         for spec in &args.exclude {
-            if !workspace.members.contains_key(spec) {
+            if !metadata.packages.iter().any(|package| package.name == *spec) {
                 warn!(
                     args.color,
                     "excluded package(s) {} not found in workspace `{}`",
                     spec,
-                    workspace.current_manifest.dir().display()
+                    metadata.workspace_root.display()
                 );
             }
         }
 
-        for (_, manifest) in
-            workspace.members.iter().filter(|(spec, _)| !args.exclude.contains(spec))
+        for package in
+            metadata.packages.iter().filter(|package| !args.exclude.contains(&package.name))
         {
-            exec_on_package(manifest, args)?;
+            exec_on_package(args, package, &line)?;
         }
     } else if !args.package.is_empty() {
         for spec in &args.package {
-            if let Some(manifest) = workspace.members.get(spec) {
-                exec_on_package(manifest, args)?;
-            } else {
+            if !metadata.packages.iter().any(|package| package.name == *spec) {
                 bail!("package ID specification `{}` matched no packages", spec);
             }
         }
-    } else if workspace.current_manifest.is_virtual() {
-        for (_, manifest) in workspace.members.iter() {
-            exec_on_package(manifest, args)?;
+
+        for package in
+            metadata.packages.iter().filter(|package| args.package.contains(&package.name))
+        {
+            exec_on_package(args, package, &line)?;
+        }
+    } else if current_manifest.is_virtual() {
+        for package in &metadata.packages {
+            exec_on_package(args, package, &line)?;
         }
-    } else if !workspace.current_manifest.is_virtual() {
-        exec_on_package(workspace.current_manifest, args)?;
+    } else if !current_manifest.is_virtual() {
+        let current_package = current_manifest.package_name();
+        let package =
+            metadata.packages.iter().find(|package| package.name == *current_package).unwrap();
+        exec_on_package(args, package, &line)?;
     }
 
     Ok(())
 }
 
-fn exec_on_package(manifest: &Manifest, args: &Args) -> Result<()> {
+fn exec_on_package(args: &Args, package: &Package, line: &ProcessBuilder) -> Result<()> {
+    let manifest = Manifest::new(&package.manifest_path)?;
+
     if args.ignore_private && manifest.is_private() {
-        info!(args.color, "skipped running on {}", manifest.package_name_verbose(args));
+        info!(args.color, "skipped running on {}", package.name_verbose(args));
     } else if args.subcommand.is_some() || args.remove_dev_deps {
-        no_dev_deps(manifest, args)?;
+        no_dev_deps(args, package, manifest, line)?;
     }
 
     Ok(())
 }
 
-fn no_dev_deps(manifest: &Manifest, args: &Args) -> Result<()> {
+fn no_dev_deps(
+    args: &Args,
+    package: &Package,
+    mut manifest: Manifest,
+    line: &ProcessBuilder,
+) -> Result<()> {
     struct Bomb<'a> {
         manifest: &'a Manifest,
         args: &'a Args,
@@ -145,139 +140,73 @@ fn no_dev_deps(manifest: &Manifest, args: &Args) -> Result<()> {
         }
     }
 
+    let f = |args: &Args, package: &Package, line: &ProcessBuilder| {
+        let mut line = line.clone();
+        line.features(args, package);
+        line.arg("--manifest-path");
+        line.arg(&package.manifest_path);
+
+        if args.each_feature {
+            exec_for_each_feature(args, package, &line)
+        } else {
+            exec_cargo(args, package, &line)
+        }
+    };
+
     if args.no_dev_deps || args.remove_dev_deps {
         let mut res = Ok(());
-        let mut bomb = Bomb { manifest, args, done: false, res: &mut res };
+        let new = manifest.remove_dev_deps();
+        let mut bomb = Bomb { manifest: &manifest, args, done: false, res: &mut res };
 
-        fs::write(&manifest.path, remove_dev_deps(manifest)).with_context(|| {
-            format!("failed to update manifest file: {}", manifest.path.display())
+        fs::write(&package.manifest_path, new).with_context(|| {
+            format!("failed to update manifest file: {}", package.manifest_path.display())
         })?;
 
         if args.subcommand.is_some() {
-            each_feature(manifest, args)?;
+            f(args, package, line)?;
         }
 
         bomb.done = true;
         drop(bomb);
         res?;
     } else if args.subcommand.is_some() {
-        each_feature(manifest, args)?;
+        f(args, package, line)?;
     }
 
     Ok(())
 }
 
-fn each_feature(manifest: &Manifest, args: &Args) -> Result<()> {
-    let mut features = String::new();
-    if args.ignore_unknown_features {
-        let f: Vec<_> = args
-            .features
-            .iter()
-            .filter(|f| {
-                if manifest.features.contains(f) {
-                    true
-                } else {
-                    // ignored
-                    info!(
-                        args.color,
-                        "skipped applying unknown `{}` feature to {}",
-                        f,
-                        manifest.package_name_verbose(args)
-                    );
-                    false
-                }
-            })
-            .map(String::as_str)
-            .collect();
-        if !f.is_empty() {
-            features.push_str("--features=");
-            features.push_str(&f.join(","));
-        }
-    } else if !args.features.is_empty() {
-        features.push_str("--features=");
-        features.push_str(&args.features.join(","));
-    }
-
-    let features = if features.is_empty() { None } else { Some(&*features) };
-
-    if args.each_feature {
-        exec_each_feature(manifest, args, features)
-    } else {
-        exec_cargo_command(manifest, args, features, &[])
-    }
-}
-
-fn remove_dev_deps(manifest: &Manifest) -> String {
-    let mut doc = manifest.toml.clone();
-    manifest::remove_key_and_target_key(doc.as_table_mut(), "dev-dependencies");
-    doc.to_string_in_original_order()
-}
-
-fn exec_each_feature(manifest: &Manifest, args: &Args, features: Option<&str>) -> Result<()> {
+fn exec_for_each_feature(args: &Args, package: &Package, line: &ProcessBuilder) -> Result<()> {
     // run with default features
-    exec_cargo_command(manifest, args, features, &[])?;
+    exec_cargo(args, package, line)?;
 
-    if manifest.features.is_empty() {
+    if package.features.is_empty() {
         return Ok(());
     }
 
+    let mut line = line.clone();
+    line.arg("--no-default-features");
+
     // run with no default features if the package has other features
     //
     // `default` is not skipped because `cfg(feature = "default")` is work
     // if `default` feature specified.
-    exec_cargo_command(manifest, args, features, &["--no-default-features"])?;
+    exec_cargo(args, package, &line)?;
 
     // run with each feature
-    manifest.features.iter().filter(|&k| k != "default").try_for_each(|feature| {
-        let features = match features {
-            Some(features) => String::from(features) + "," + feature,
-            None => String::from("--features=") + feature,
-        };
-        exec_cargo_command(manifest, args, Some(&*features), &["--no-default-features"])
+    package.features.iter().filter(|(k, _)| *k != "default").try_for_each(|(feature, _)| {
+        let mut line = line.clone();
+        line.append_features(&[feature]);
+        exec_cargo(args, package, &line)
     })
 }
 
-fn exec_cargo_command(
-    manifest: &Manifest,
-    args: &Args,
-    features: Option<&str>,
-    extra_args: &[&str],
-) -> Result<()> {
-    let mut line = ProcessBuilder::new(cargo_binary());
-
-    line.args(&args.first);
-
-    if let Some(features) = features {
-        line.arg(features);
-    }
-
-    if let Some(target_dir) = args.target_dir.as_ref() {
-        line.arg("--target-dir");
-        line.arg(target_dir);
-    }
-
-    line.args(extra_args);
-
-    line.args2(&args.second);
-
-    if args.verbose {
-        line.arg("--manifest-path");
-        line.arg(&manifest.path);
-
-        info!(args.color, "running {} on {}", line, manifest.package_name_verbose(args));
-    } else {
-        info!(args.color, "running {} on {}", line, manifest.package_name_verbose(args));
-
-        // Displaying --manifest-path is redundant.
-        line.arg("--manifest-path");
-        line.arg(&manifest.path);
-    }
-
+fn exec_cargo(args: &Args, package: &Package, line: &ProcessBuilder) -> Result<()> {
+    info!(args.color, "running {} on {}", line, package.name_verbose(args));
     line.exec()
 }
 
 fn cargo_binary() -> OsString {
-    let cargo_src = env::var_os("CARGO_HACK_CARGO_SRC");
-    let cargo = env::var_os("CARGO");
-    cargo_src.unwrap_or_else(|| cargo.unwrap_or_else(|| OsString::from("cargo")))
+    env::var_os("CARGO_HACK_CARGO_SRC")
+        .unwrap_or_else(|| env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")))
 }
diff --git a/src/manifest.rs b/src/manifest.rs
index ce829b3c..352a0481 100644
--- a/src/manifest.rs
+++ b/src/manifest.rs
@@ -1,69 +1,31 @@
 use std::{
-    borrow::Cow,
     fs, ops,
     path::{Path, PathBuf},
 };
 
 use anyhow::{bail, Context, Result};
-use toml_edit::{Array, Document, Item as Value, Table};
-
-use crate::Args;
+use toml_edit::{Document, Table};
 
 #[derive(Clone, Debug)]
 pub(crate) struct Manifest {
     pub(crate) path: PathBuf,
     pub(crate) raw: String,
-    pub(crate) toml: Document,
-    pub(crate) features: Vec<String>,
+    pub(crate) doc: Document,
 }
 
 impl Manifest {
-    pub(crate) fn new(path: &Path) -> Result<Self> {
-        let path = path
-            .canonicalize()
-            .with_context(|| format!("failed to canonicalize manifest path: {}", path.display()))?;
+    pub(crate) fn new(path: impl Into<PathBuf>) -> Result<Self> {
+        let path = path.into();
         let raw = fs::read_to_string(&path)
             .with_context(|| format!("failed to read manifest from {}", path.display()))?;
-        let toml: Document = raw
+        let doc: Document = raw
             .parse()
             .with_context(|| format!("failed to parse manifest file: {}", path.display()))?;
-        let features = toml
-            .as_table()
-            .get("features")
-            .and_then(Value::as_table)
-            .into_iter()
-            .flat_map(|f| f.iter().map(|(k, _)| k.to_string()))
-            .collect::<Vec<_>>();
-
-        let manifest = Self { path, raw, toml, features };
-
-        if manifest.package().is_none() && manifest.members().is_none() {
-            // TODO: improve error message
-            bail!("expected 'package' or 'workspace'");
-        }
-
-        Ok(manifest)
-    }
-
-    pub(crate) fn with_manifest_path(path: &str) -> Result<Self> {
-        if !path.ends_with("Cargo.toml") {
-            bail!("the manifest-path must be a path to a Cargo.toml file");
-        }
-
-        let path = Path::new(path);
-        if !path.exists() {
-            bail!("manifest path `{}` does not exist", path.display());
-        }
-
-        Self::new(path)
-    }
-
-    pub(crate) fn dir(&self) -> &Path {
-        self.path.parent().unwrap()
+        Ok(Self { path, raw, doc })
     }
 
     pub(crate) fn package(&self) -> Option<&Table> {
-        self.toml.as_table().get("package")?.as_table()
+        self.doc.as_table().get("package")?.as_table()
     }
 
     pub(crate) fn package_name(&self) -> &str {
@@ -71,33 +33,22 @@ impl Manifest {
         (|| self.package()?.get("name")?.as_str())().unwrap()
     }
 
-    pub(crate) fn package_name_verbose(&self, args: &Args) -> Cow<'_, str> {
-        assert!(!self.is_virtual());
-        if args.verbose {
-            Cow::Owned(format!("{} ({})", self.package_name(), self.dir().display()))
-        } else {
-            Cow::Borrowed(self.package_name())
-        }
-    }
-
     pub(crate) fn is_virtual(&self) -> bool {
         self.package().is_none()
     }
 
+    // `metadata.package.publish` requires Rust 1.39
     pub(crate) fn is_private(&self) -> bool {
         (|| self.package()?.get("publish")?.as_bool())().map_or(false, ops::Not::not)
     }
 
-    pub(crate) fn workspace(&self) -> Option<&Table> {
-        self.toml.as_table().get("workspace")?.as_table()
-    }
-
-    pub(crate) fn members(&self) -> Option<&Array> {
-        (|| self.workspace()?.get("members")?.as_array())()
+    pub(crate) fn remove_dev_deps(&mut self) -> String {
+        remove_key_and_target_key(self.doc.as_table_mut(), "dev-dependencies");
+        self.doc.to_string_in_original_order()
     }
 }
 
-pub(crate) fn remove_key_and_target_key(table: &mut Table, key: &str) {
+fn remove_key_and_target_key(table: &mut Table, key: &str) {
     table.remove(key);
     if let Some(table) = table.entry("target").as_table_mut() {
         // `toml_edit::Table` does not have `.iter_mut()`, so collect keys.
@@ -122,13 +73,3 @@ pub(crate) fn find_root_manifest_for_wd(cwd: &Path) -> Result<PathBuf> {
 
     bail!("could not find `Cargo.toml` in `{}` or any parent directory", cwd.display())
 }
-
-/// Returns the path to the `Cargo.toml` in `pwd`, if it exists.
-pub(crate) fn find_project_manifest_exact(pwd: &Path) -> Result<PathBuf> {
-    let manifest = pwd.join("Cargo.toml");
-    if manifest.exists() {
-        Ok(manifest)
-    } else {
-        bail!("Could not find `Cargo.toml` in `{}`", pwd.display())
-    }
-}
diff --git a/src/metadata.rs b/src/metadata.rs
new file mode 100644
index 00000000..aacbb276
--- /dev/null
+++ b/src/metadata.rs
@@ -0,0 +1,74 @@
+use std::{
+    borrow::Cow,
+    collections::BTreeMap,
+    env,
+    ffi::OsString,
+    io::{self, Write},
+    path::PathBuf,
+    process::Command,
+};
+
+use anyhow::{Context, Result};
+use serde::Deserialize;
+
+use crate::Args;
+
+// Refs:
+// * https://github.com/oli-obk/cargo_metadata
+// * https://github.com/rust-lang/cargo/blob/0.40.0/src/cargo/ops/cargo_output_metadata.rs#L79
+// * https://github.com/rust-lang/cargo/blob/0.40.0/src/cargo/core/package.rs#L57
+
+#[derive(Debug, Deserialize)]
+pub(crate) struct Metadata {
+    /// A list of all crates referenced by this crate (and the crate itself)
+    pub(crate) packages: Vec<Package>,
+    /// Build directory
+    pub(crate) target_directory: PathBuf,
+    /// Workspace root
+    pub(crate) workspace_root: PathBuf,
+}
+
+#[derive(Debug, Deserialize)]
+pub(crate) struct Package {
+    /// Name as given in the `Cargo.toml`
+    pub(crate) name: String,
+    /// Features provided by the crate, mapped to the features required by that feature.
+    pub(crate) features: BTreeMap<String, Vec<String>>,
+    /// Path containing the `Cargo.toml`
+    pub(crate) manifest_path: PathBuf,
+}
+
+impl Metadata {
+    pub(crate) fn new(args: &Args) -> Result<Self> {
+        let cargo = env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo"));
+        let mut command = Command::new(cargo);
+        command.args(&["metadata", "--no-deps", "--format-version=1"]);
+        if let Some(manifest_path) = &args.manifest_path {
+            command.arg("--manifest-path");
+            command.arg(manifest_path);
+        }
+
+        let output = command.output().context("failed to run 'cargo metadata'")?;
+        if !output.status.success() {
+            let _ = io::stderr().write_all(&output.stderr);
+            let code = output.status.code().unwrap_or(1);
+            std::process::exit(code);
+        }
+
+        serde_json::from_slice(&output.stdout).context("failed to parse metadata")
+    }
+}
+
+impl Package {
+    pub(crate) fn name_verbose(&self, args: &Args) -> Cow<'_, str> {
+        if args.verbose {
+            Cow::Owned(format!(
+                "{} ({})",
+                self.name,
+                self.manifest_path.parent().unwrap().display()
+            ))
+        } else {
+            Cow::Borrowed(&self.name)
+        }
+    }
+}
diff --git a/src/process.rs b/src/process.rs
index 3ea5149a..c6426f8d 100644
--- a/src/process.rs
+++ b/src/process.rs
@@ -4,40 +4,95 @@ use std::{
     fmt,
     path::Path,
     process::{Command, ExitStatus, Output},
+    rc::Rc,
     str,
 };
 
 use anyhow::{Context, Result};
 
+use crate::{Args, Package};
+
 // Based on https://github.com/rust-lang/cargo/blob/0.39.0/src/cargo/util/process_builder.rs
 
 /// A builder object for an external process, similar to `std::process::Command`.
 #[derive(Clone, Debug)]
 pub(crate) struct ProcessBuilder {
     /// The program to execute.
-    program: OsString,
+    program: Rc<OsString>,
     /// A list of arguments to pass to the program (until '--').
-    args: Vec<OsString>,
+    leading_args: Rc<[String]>,
     /// A list of arguments to pass to the program (after '--').
-    args2: Vec<OsString>,
+    trailing_args: Rc<[String]>,
+    /// A list of arguments to pass to the program (between `leading_args` and '--').
+    args: Vec<OsString>,
+    // cargo less than Rust 1.38 cannot handle multiple '--features' flags, so it creates another String.
+    features: String,
+
     /// Any environment variables that should be set for the program.
     env: HashMap<String, Option<OsString>>,
     /// The directory to run the program from.
     cwd: Option<OsString>,
+    /// Use verbose output.
+    verbose: bool,
 }
 
 impl ProcessBuilder {
     /// Creates a new `ProcessBuilder`.
-    pub(crate) fn new(cmd: impl Into<OsString>) -> Self {
+    pub(crate) fn new(program: OsString) -> Self {
+        Self {
+            program: Rc::new(program),
+            leading_args: Rc::from(&[][..]),
+            trailing_args: Rc::from(&[][..]),
+            args: Vec::new(),
+            features: String::new(),
+            cwd: None,
+            env: HashMap::new(),
+            verbose: false,
+        }
+    }
+
+    /// Creates a new `ProcessBuilder` from `Args`.
+    pub(crate) fn from_args(program: OsString, args: &Args) -> Self {
         Self {
-            program: cmd.into(),
+            program: Rc::new(program),
+            leading_args: args.leading_args.clone(),
+            trailing_args: args.trailing_args.clone(),
             args: Vec::new(),
-            args2: Vec::new(),
+            features: String::new(),
             cwd: None,
             env: HashMap::new(),
+            verbose: args.verbose,
         }
     }
 
+    pub(crate) fn append_features(&mut self, features: impl IntoIterator<Item = impl AsRef<str>>) {
+        for feature in features {
+            self.features.push_str(feature.as_ref());
+            self.features.push(',');
+        }
+    }
+
+    /// (chainable) Adds `--features` flag to the args list.
+    pub(crate) fn features(&mut self, args: &Args, package: &Package) -> &mut Self {
+        if args.ignore_unknown_features {
+            self.append_features(args.features.iter().filter(|f| {
+                if package.features.get(*f).is_some() {
+                    true
+                } else {
+                    // ignored
+                    info!(
+                        args.color,
+                        "skipped applying unknown `{}` feature to {}", f, package.name
+                    );
+                    false
+                }
+            }))
+        } else if !args.features.is_empty() {
+            self.append_features(&args.features);
+        }
+        self
+    }
+
     // /// (chainable) Sets the executable for the process.
     // pub(crate) fn program(&mut self, program: impl AsRef<OsStr>) -> &mut Self {
     //     self.program = program.as_ref().to_os_string();
@@ -50,11 +105,11 @@ impl ProcessBuilder {
         self
     }
 
-    /// (chainable) Adds multiple `args` to the args list.
-    pub(crate) fn args(&mut self, args: &[impl AsRef<OsStr>]) -> &mut Self {
-        self.args.extend(args.iter().map(|t| t.as_ref().to_os_string()));
-        self
-    }
+    // /// (chainable) Adds multiple `args` to the args list.
+    // pub(crate) fn args(&mut self, args: &[impl AsRef<OsStr>]) -> &mut Self {
+    //     self.args.extend(args.iter().map(|t| t.as_ref().to_os_string()));
+    //     self
+    // }
 
     // /// (chainable) Replaces the args list with the given `args`.
     // pub(crate) fn args_replace(&mut self, args: &[impl AsRef<OsStr>]) -> &mut Self {
@@ -62,24 +117,6 @@ impl ProcessBuilder {
     //     self
     // }
 
-    // /// (chainable) Adds `arg` to the args2 list.
-    // pub(crate) fn arg2(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
-    //     self.args2.push(arg.as_ref().to_os_string());
-    //     self
-    // }
-
-    /// (chainable) Adds multiple `args` to the args2 list.
-    pub(crate) fn args2(&mut self, args: &[impl AsRef<OsStr>]) -> &mut Self {
-        self.args2.extend(args.iter().map(|t| t.as_ref().to_os_string()));
-        self
-    }
-
-    // /// (chainable) Replaces the args2 list with the given `args`.
-    // pub(crate) fn args2_replace(&mut self, args: &[impl AsRef<OsStr>]) -> &mut Self {
-    //     self.args2 = args.iter().map(|t| t.as_ref().to_os_string()).collect();
-    //     self
-    // }
-
     // /// (chainable) Sets the current working directory of the process.
     // pub(crate) fn cwd(&mut self, path: impl AsRef<OsStr>) -> &mut Self {
     //     self.cwd = Some(path.as_ref().to_os_string());
@@ -167,15 +204,22 @@ impl ProcessBuilder {
     /// Converts `ProcessBuilder` into a `std::process::Command`, and handles the jobserver, if
     /// present.
     fn build_command(&self) -> Command {
-        let mut command = Command::new(&self.program);
+        let mut command = Command::new(&*self.program);
         if let Some(cwd) = self.get_cwd() {
             command.current_dir(cwd);
         }
+
+        command.args(&*self.leading_args);
         command.args(&self.args);
-        if !self.args2.is_empty() {
+        if !self.features.is_empty() {
+            command.arg("--features");
+            command.arg(&self.features[..self.features.len() - 1]);
+        }
+        if !self.trailing_args.is_empty() {
             command.arg("--");
-            command.args(&self.args2);
+            command.args(&*self.trailing_args);
         }
+
         for (k, v) in &self.env {
             match v {
                 Some(v) => {
@@ -194,19 +238,39 @@ impl fmt::Display for ProcessBuilder {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(f, "`")?;
 
-        write!(f, "{}", Path::new(&self.program).file_stem().unwrap().to_string_lossy())?;
+        write!(f, "{}", Path::new(&*self.program).file_stem().unwrap().to_string_lossy())?;
 
-        for arg in &self.args {
-            write!(f, " {}", arg.to_string_lossy())?;
+        for arg in &*self.leading_args {
+            write!(f, " {}", arg)?;
         }
 
-        if !self.args2.is_empty() {
-            write!(f, " --")?;
-            for arg in &self.args2 {
+        if self.verbose {
+            for arg in &self.args {
+                write!(f, " {}", arg.to_string_lossy())?;
+            }
+        } else {
+            let mut args = self.args.iter();
+            while let Some(arg) = args.next() {
+                // Displaying `--target-dir` and `--manifest-path` is redundant.
+                if arg == "--target-dir" || arg == "--manifest-path" {
+                    let _ = args.next();
+                    continue;
+                }
                 write!(f, " {}", arg.to_string_lossy())?;
             }
         }
 
+        if !self.features.is_empty() {
+            write!(f, " --features {}", &self.features[..self.features.len() - 1])?;
+        }
+
+        if !self.trailing_args.is_empty() {
+            write!(f, " --")?;
+            for arg in &*self.trailing_args {
+                write!(f, " {}", arg)?;
+            }
+        }
+
         write!(f, "`")
     }
 }
diff --git a/src/workspace.rs b/src/workspace.rs
deleted file mode 100644
index 29ba2586..00000000
--- a/src/workspace.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-use std::path::Path;
-
-use anyhow::Result;
-use indexmap::IndexMap;
-use toml_edit::Item as Value;
-
-use crate::manifest::{find_project_manifest_exact, Manifest};
-
-pub(crate) struct Workspace<'a> {
-    /// This manifest is a manifest to where the current cargo subcommand was
-    /// invoked from.
-    pub(crate) current_manifest: &'a Manifest,
-
-    // Map of members in this workspace. Keys are their package names and values
-    // are their manifests.
-    pub(crate) members: IndexMap<String, Manifest>,
-}
-
-impl<'a> Workspace<'a> {
-    pub(crate) fn new(current_manifest: &'a Manifest) -> Result<Self> {
-        let mut members = IndexMap::new();
-        // TODO: The current cargo-hack doesn't try to find the root manifest
-        // after finding the current package's manifest.
-        // https://github.com/taiki-e/cargo-hack/issues/11
-        let root_dir = current_manifest.dir();
-        let mut inserted = false;
-
-        if let Some(workspace) = current_manifest.workspace() {
-            for mut dir in workspace
-                .get("members")
-                .and_then(Value::as_array)
-                .into_iter()
-                .flat_map(|v| v.iter().filter_map(|v| v.as_str()))
-                .map(Path::new)
-            {
-                if let Ok(new) = dir.strip_prefix(".") {
-                    dir = new;
-                }
-
-                let path = find_project_manifest_exact(&root_dir.join(dir))?;
-                let manifest = Manifest::new(&path)?;
-
-                if current_manifest.path == manifest.path {
-                    inserted = true;
-                }
-                members.insert(manifest.package_name().to_string(), manifest);
-            }
-        }
-
-        if !current_manifest.is_virtual() && !inserted {
-            members.insert(current_manifest.package_name().to_string(), current_manifest.clone());
-        }
-
-        Ok(Self { current_manifest, members })
-    }
-}


Test patch:
diff --git a/tests/fixtures/windows_not_find_manifest/.cargo/config b/tests/fixtures/package_collision/.cargo/config
similarity index 100%
rename from tests/fixtures/windows_not_find_manifest/.cargo/config
rename to tests/fixtures/package_collision/.cargo/config
diff --git a/tests/fixtures/windows_package_collision/Cargo.toml b/tests/fixtures/package_collision/Cargo.toml
similarity index 100%
rename from tests/fixtures/windows_package_collision/Cargo.toml
rename to tests/fixtures/package_collision/Cargo.toml
diff --git a/tests/fixtures/windows_package_collision/member1/Cargo.toml b/tests/fixtures/package_collision/member1/Cargo.toml
similarity index 100%
rename from tests/fixtures/windows_package_collision/member1/Cargo.toml
rename to tests/fixtures/package_collision/member1/Cargo.toml
diff --git a/tests/fixtures/windows_not_find_manifest/dir/member2/src/main.rs b/tests/fixtures/package_collision/member1/src/main.rs
similarity index 100%
rename from tests/fixtures/windows_not_find_manifest/dir/member2/src/main.rs
rename to tests/fixtures/package_collision/member1/src/main.rs
diff --git a/tests/fixtures/windows_not_find_manifest/dir/member2/Cargo.toml b/tests/fixtures/package_collision/member2/Cargo.toml
similarity index 100%
rename from tests/fixtures/windows_not_find_manifest/dir/member2/Cargo.toml
rename to tests/fixtures/package_collision/member2/Cargo.toml
diff --git a/tests/fixtures/windows_not_find_manifest/member1/src/main.rs b/tests/fixtures/package_collision/member2/src/main.rs
similarity index 100%
rename from tests/fixtures/windows_not_find_manifest/member1/src/main.rs
rename to tests/fixtures/package_collision/member2/src/main.rs
diff --git a/tests/fixtures/real/Cargo.toml b/tests/fixtures/real/Cargo.toml
index 1951e3ca..c785720a 100644
--- a/tests/fixtures/real/Cargo.toml
+++ b/tests/fixtures/real/Cargo.toml
@@ -9,6 +9,7 @@ publish = false
 members = [
     "member1",
     "member2",
+    "member3",
     ".",
 ]
 
diff --git a/tests/fixtures/windows_not_find_manifest/member1/Cargo.toml b/tests/fixtures/real/member3/Cargo.toml
similarity index 76%
rename from tests/fixtures/windows_not_find_manifest/member1/Cargo.toml
rename to tests/fixtures/real/member3/Cargo.toml
index bd3ded4c..9ce5ee5c 100644
--- a/tests/fixtures/windows_not_find_manifest/member1/Cargo.toml
+++ b/tests/fixtures/real/member3/Cargo.toml
@@ -1,11 +1,12 @@
 [package]
-name = "member1"
+name = "member3"
 version = "0.1.0"
 authors = ["Taiki Endo <te316e89@gmail.com>"]
 edition = "2018"
+publish = true
 
 [dependencies]
-# member2 = { path = "../member2" }
+member2 = { path = "../member2" }
 
 [dev-dependencies]
 
diff --git a/tests/fixtures/windows_package_collision/member1/src/main.rs b/tests/fixtures/real/member3/src/main.rs
similarity index 100%
rename from tests/fixtures/windows_package_collision/member1/src/main.rs
rename to tests/fixtures/real/member3/src/main.rs
diff --git a/tests/fixtures/virtual/Cargo.toml b/tests/fixtures/virtual/Cargo.toml
index 5a39ea26..17789553 100644
--- a/tests/fixtures/virtual/Cargo.toml
+++ b/tests/fixtures/virtual/Cargo.toml
@@ -2,4 +2,5 @@
 members = [
     "member1",
     "member2",
+    "dir/not_find_manifest",
 ]
diff --git a/tests/fixtures/windows_package_collision/member2/Cargo.toml b/tests/fixtures/virtual/dir/not_find_manifest/Cargo.toml
similarity index 89%
rename from tests/fixtures/windows_package_collision/member2/Cargo.toml
rename to tests/fixtures/virtual/dir/not_find_manifest/Cargo.toml
index 59f10cf7..8d9c1759 100644
--- a/tests/fixtures/windows_package_collision/member2/Cargo.toml
+++ b/tests/fixtures/virtual/dir/not_find_manifest/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "member2"
+name = "not_find_manifest"
 version = "0.1.0"
 authors = ["Taiki Endo <te316e89@gmail.com>"]
 edition = "2018"
diff --git a/tests/fixtures/windows_package_collision/member2/src/main.rs b/tests/fixtures/virtual/dir/not_find_manifest/src/main.rs
similarity index 100%
rename from tests/fixtures/windows_package_collision/member2/src/main.rs
rename to tests/fixtures/virtual/dir/not_find_manifest/src/main.rs
diff --git a/tests/fixtures/virtual/member2/Cargo.toml b/tests/fixtures/virtual/member2/Cargo.toml
index 59f10cf7..21dd6e06 100644
--- a/tests/fixtures/virtual/member2/Cargo.toml
+++ b/tests/fixtures/virtual/member2/Cargo.toml
@@ -16,3 +16,4 @@ default = ["a"]
 a = []
 b = []
 c = []
+f = []
diff --git a/tests/fixtures/windows_not_find_manifest/Cargo.toml b/tests/fixtures/windows_not_find_manifest/Cargo.toml
deleted file mode 100644
index 483d0334..00000000
--- a/tests/fixtures/windows_not_find_manifest/Cargo.toml
+++ /dev/null
@@ -1,5 +0,0 @@
-[workspace]
-members = [
-    "member1",
-    "dir/member2",
-]
diff --git a/tests/fixtures/windows_package_collision/.cargo/config b/tests/fixtures/windows_package_collision/.cargo/config
deleted file mode 100644
index 88403f3b..00000000
--- a/tests/fixtures/windows_package_collision/.cargo/config
+++ /dev/null
@@ -1,2 +0,0 @@
-[build]
-target-dir = "../../../target"
diff --git a/tests/test.rs b/tests/test.rs
index cd07d672..c94aa9d9 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -100,11 +100,9 @@ fn test_real() {
         .assert_success()
         .assert_stderr_not_contains("running `cargo check` on member1")
         .assert_stderr_not_contains("running `cargo check` on member2")
+        .assert_stderr_not_contains("running `cargo check` on member3")
         .assert_stderr_contains("running `cargo check` on real");
-}
 
-#[test]
-fn test_real_all() {
     let output = cargo_hack()
         .args(&["hack", "check", "--all"])
         .current_dir(test_dir("tests/fixtures/real"))
@@ -115,50 +113,80 @@ fn test_real_all() {
         .assert_success()
         .assert_stderr_contains("running `cargo check` on member1")
         .assert_stderr_contains("running `cargo check` on member2")
+        .assert_stderr_contains("running `cargo check` on member3")
         .assert_stderr_contains("running `cargo check` on real");
 }
 
 #[test]
-fn test_real_ignore_private() {
+fn test_virtual() {
     let output = cargo_hack()
-        .args(&["hack", "check", "--ignore-private"])
-        .current_dir(test_dir("tests/fixtures/real"))
+        .args(&["hack", "check"])
+        .current_dir(test_dir("tests/fixtures/virtual"))
         .output()
         .unwrap();
 
     output
         .assert_success()
-        .assert_stderr_not_contains("running `cargo check` on member1")
-        .assert_stderr_not_contains("running `cargo check` on member2")
-        .assert_stderr_not_contains("running `cargo check` on real")
-        .assert_stderr_not_contains("skipped running on member1")
-        .assert_stderr_not_contains("skipped running on member2")
-        .assert_stderr_contains("skipped running on real");
+        .assert_stderr_contains("running `cargo check` on member1")
+        .assert_stderr_contains("running `cargo check` on member2");
+
+    let output = cargo_hack()
+        .args(&["hack", "check", "--all"])
+        .current_dir(test_dir("tests/fixtures/virtual"))
+        .output()
+        .unwrap();
+
+    output
+        .assert_success()
+        .assert_stderr_contains("running `cargo check` on member1")
+        .assert_stderr_contains("running `cargo check` on member2");
 }
 
 #[test]
-fn test_real_ignore_private_all() {
+fn test_real_all_in_subcrate() {
     let output = cargo_hack()
-        .args(&["hack", "check", "--all", "--ignore-private"])
-        .current_dir(test_dir("tests/fixtures/real"))
+        .args(&["hack", "check"])
+        .current_dir(test_dir("tests/fixtures/real/member2"))
+        .output()
+        .unwrap();
+
+    output
+        .assert_success()
+        .assert_stderr_not_contains("running `cargo check` on member1")
+        .assert_stderr_contains("running `cargo check` on member2")
+        .assert_stderr_not_contains("running `cargo check` on member3")
+        .assert_stderr_not_contains("running `cargo check` on real");
+
+    let output = cargo_hack()
+        .args(&["hack", "check", "--all"])
+        .current_dir(test_dir("tests/fixtures/real/member2"))
         .output()
         .unwrap();
 
     output
         .assert_success()
         .assert_stderr_contains("running `cargo check` on member1")
-        .assert_stderr_not_contains("running `cargo check` on member2")
-        .assert_stderr_not_contains("running `cargo check` on real")
-        .assert_stderr_not_contains("skipped running on member1")
-        .assert_stderr_contains("skipped running on member2")
-        .assert_stderr_contains("skipped running on real");
+        .assert_stderr_contains("running `cargo check` on member2")
+        .assert_stderr_contains("running `cargo check` on member3")
+        .assert_stderr_contains("running `cargo check` on real");
 }
 
 #[test]
-fn test_virtual() {
+fn test_virtual_all_in_subcrate() {
     let output = cargo_hack()
         .args(&["hack", "check"])
-        .current_dir(test_dir("tests/fixtures/virtual"))
+        .current_dir(test_dir("tests/fixtures/virtual/member1"))
+        .output()
+        .unwrap();
+
+    output
+        .assert_success()
+        .assert_stderr_contains("running `cargo check` on member1")
+        .assert_stderr_not_contains("running `cargo check` on member2");
+
+    let output = cargo_hack()
+        .args(&["hack", "check", "--all"])
+        .current_dir(test_dir("tests/fixtures/virtual/member1"))
         .output()
         .unwrap();
 
@@ -169,17 +197,38 @@ fn test_virtual() {
 }
 
 #[test]
-fn test_virtual_all() {
+fn test_real_ignore_private() {
     let output = cargo_hack()
-        .args(&["hack", "check", "--all"])
-        .current_dir(test_dir("tests/fixtures/virtual"))
+        .args(&["hack", "check", "--ignore-private"])
+        .current_dir(test_dir("tests/fixtures/real"))
+        .output()
+        .unwrap();
+
+    output
+        .assert_success()
+        .assert_stderr_not_contains("running `cargo check` on member1")
+        .assert_stderr_not_contains("skipped running on member1")
+        .assert_stderr_not_contains("running `cargo check` on member2")
+        .assert_stderr_not_contains("skipped running on member2")
+        .assert_stderr_not_contains("running `cargo check` on real")
+        .assert_stderr_contains("skipped running on real");
+
+    let output = cargo_hack()
+        .args(&["hack", "check", "--all", "--ignore-private"])
+        .current_dir(test_dir("tests/fixtures/real"))
         .output()
         .unwrap();
 
     output
         .assert_success()
         .assert_stderr_contains("running `cargo check` on member1")
-        .assert_stderr_contains("running `cargo check` on member2");
+        .assert_stderr_not_contains("skipped running on member1")
+        .assert_stderr_not_contains("running `cargo check` on member2")
+        .assert_stderr_contains("skipped running on member2")
+        .assert_stderr_contains("running `cargo check` on member3")
+        .assert_stderr_not_contains("skipped running on member3")
+        .assert_stderr_not_contains("running `cargo check` on real")
+        .assert_stderr_contains("skipped running on real");
 }
 
 #[test]
@@ -196,10 +245,7 @@ fn test_virtual_ignore_private() {
         .assert_stderr_not_contains("running `cargo check` on member2")
         .assert_stderr_not_contains("skipped running on member1")
         .assert_stderr_contains("skipped running on member2");
-}
 
-#[test]
-fn test_virtual_ignore_private_all() {
     let output = cargo_hack()
         .args(&["hack", "check", "--all", "--ignore-private"])
         .current_dir(test_dir("tests/fixtures/virtual"))
@@ -271,7 +317,7 @@ fn test_exclude_not_found() {
 }
 
 #[test]
-fn test_exclude_not_all() {
+fn test_exclude_not_with_all() {
     let output = cargo_hack()
         .args(&["hack", "check", "--exclude", "member1"])
         .current_dir(test_dir("tests/fixtures/virtual"))
@@ -362,7 +408,14 @@ fn test_no_dev_deps_with_devs() {
 #[test]
 fn test_ignore_unknown_features() {
     let output = cargo_hack()
-        .args(&["hack", "check", "--ignore-unknown-features", "--features=f"])
+        .args(&[
+            "hack",
+            "check",
+            "--ignore-unknown-features",
+            "--no-default-features",
+            "--features",
+            "f",
+        ])
         .current_dir(test_dir("tests/fixtures/virtual"))
         .output()
         .unwrap();
@@ -370,7 +423,11 @@ fn test_ignore_unknown_features() {
     output
         .assert_success()
         .assert_stderr_contains("skipped applying unknown `f` feature to member1")
-        .assert_stderr_contains("skipped applying unknown `f` feature to member2");
+        .assert_stderr_contains("running `cargo check --no-default-features` on member1")
+        .assert_stderr_not_contains("skipped applying unknown `f` feature to member2")
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features f` on member2",
+        );
 }
 
 #[test]
@@ -385,7 +442,9 @@ fn test_ignore_non_exist_features() {
         .assert_success()
         .assert_stderr_contains("'--ignore-non-exist-features' flag is deprecated, use '--ignore-unknown-features' flag instead")
         .assert_stderr_contains("skipped applying unknown `f` feature to member1")
-        .assert_stderr_contains("skipped applying unknown `f` feature to member2");
+        .assert_stderr_contains("running `cargo check` on member1")
+        .assert_stderr_not_contains("skipped applying unknown `f` feature to member2")
+        .assert_stderr_contains("running `cargo check --features f` on member2");
 }
 
 #[test]
@@ -400,9 +459,32 @@ fn test_each_feature() {
         .assert_success()
         .assert_stderr_contains("running `cargo check` on real")
         .assert_stderr_contains("running `cargo check --no-default-features` on real")
-        .assert_stderr_contains("running `cargo check --features=a --no-default-features` on real")
-        .assert_stderr_contains("running `cargo check --features=b --no-default-features` on real")
-        .assert_stderr_contains("running `cargo check --features=c --no-default-features` on real");
+        .assert_stderr_contains("running `cargo check --no-default-features --features a` on real")
+        .assert_stderr_contains("running `cargo check --no-default-features --features b` on real")
+        .assert_stderr_contains("running `cargo check --no-default-features --features c` on real");
+}
+
+#[test]
+fn test_each_feature2() {
+    let output = cargo_hack()
+        .args(&["hack", "check", "--each-feature", "--features=a"])
+        .current_dir(test_dir("tests/fixtures/real"))
+        .output()
+        .unwrap();
+
+    output
+        .assert_success()
+        .assert_stderr_contains("running `cargo check --features a` on real")
+        .assert_stderr_contains("running `cargo check --no-default-features --features a` on real")
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features a,a` on real",
+        )
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features a,b` on real",
+        )
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features a,c` on real",
+        );
 }
 
 #[test]
@@ -421,39 +503,66 @@ fn test_args2() {
 }
 
 #[test]
-fn windows_package_collision() {
+fn test_package_collision() {
     let output = cargo_hack()
         .args(&["hack", "check"])
-        .current_dir(test_dir("tests/fixtures/windows_package_collision"))
+        .current_dir(test_dir("tests/fixtures/package_collision"))
         .output()
         .unwrap();
 
-    if cfg!(windows) {
-        output
-            .assert_failure()
-            .assert_stderr_contains("package collision in the lockfile: packages member2");
-    } else {
-        output
-            .assert_success()
-            .assert_stderr_contains("running `cargo check` on member1")
-            .assert_stderr_contains("running `cargo check` on member2");
-    }
+    output
+        .assert_success()
+        .assert_stderr_contains("running `cargo check` on member1")
+        .assert_stderr_contains("running `cargo check` on member2");
 }
 
 #[test]
-fn windows_not_find_manifest() {
+fn test_not_find_manifest() {
     let output = cargo_hack()
         .args(&["hack", "check"])
-        .current_dir(test_dir("tests/fixtures/windows_not_find_manifest"))
+        .current_dir(test_dir("tests/fixtures/virtual/dir/not_find_manifest"))
         .output()
         .unwrap();
 
-    if cfg!(windows) {
-        output.assert_failure().assert_stderr_contains("Could not find `Cargo.toml` in `");
-    } else {
-        output
-            .assert_success()
-            .assert_stderr_contains("running `cargo check` on member1")
-            .assert_stderr_contains("running `cargo check` on member2");
-    }
+    output
+        .assert_success()
+        .assert_stderr_not_contains("running `cargo check` on member1")
+        .assert_stderr_not_contains("running `cargo check` on member2")
+        .assert_stderr_contains("running `cargo check` on not_find_manifest");
+
+    let output = cargo_hack()
+        .args(&["hack", "check", "--all"])
+        .current_dir(test_dir("tests/fixtures/virtual/dir/not_find_manifest"))
+        .output()
+        .unwrap();
+
+    output
+        .assert_success()
+        .assert_stderr_contains("running `cargo check` on member1")
+        .assert_stderr_contains("running `cargo check` on member2")
+        .assert_stderr_contains("running `cargo check` on not_find_manifest");
+
+    let output = cargo_hack()
+        .args(&["hack", "check", "--manifest-path", "dir/not_find_manifest/Cargo.toml"])
+        .current_dir(test_dir("tests/fixtures/virtual"))
+        .output()
+        .unwrap();
+
+    output
+        .assert_success()
+        .assert_stderr_not_contains("running `cargo check` on member1")
+        .assert_stderr_not_contains("running `cargo check` on member2")
+        .assert_stderr_contains("running `cargo check` on not_find_manifest");
+
+    let output = cargo_hack()
+        .args(&["hack", "check", "--all", "--manifest-path", "dir/not_find_manifest/Cargo.toml"])
+        .current_dir(test_dir("tests/fixtures/virtual"))
+        .output()
+        .unwrap();
+
+    output
+        .assert_success()
+        .assert_stderr_contains("running `cargo check` on member1")
+        .assert_stderr_contains("running `cargo check` on member2")
+        .assert_stderr_contains("running `cargo check` on not_find_manifest");
 }