about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2023-05-07 02:36:15 +0000
committerbors <bors@rust-lang.org>2023-05-07 02:36:15 +0000
commit70a779cb7d827c0007dde1751d272bfdb58dded5 (patch)
treedc6a81124bea026b4be326b02697f76917cb4cd1
parent34bee196cb85b5e7f45118a791360161c46cf000 (diff)
parent32e27cc60765ae21426b31073ba9ac2bda499c8d (diff)
downloadrust-70a779cb7d827c0007dde1751d272bfdb58dded5.tar.gz
rust-70a779cb7d827c0007dde1751d272bfdb58dded5.zip
Auto merge of #110693 - clubby789:x-clap-take-2, r=Mark-Simulacrum
Migrate bootstrap to Clap-based argument parsing

Supercedes #108083

I chose to re-do the work rather than rebase the onto the large changes since the original PR. If it's preferred I can instead force-push the original PR to this version.

cc `@jyn514` `@albertlarsan68`
-rw-r--r--src/bootstrap/Cargo.lock92
-rw-r--r--src/bootstrap/Cargo.toml3
-rw-r--r--src/bootstrap/builder.rs31
-rw-r--r--src/bootstrap/builder/tests.rs19
-rw-r--r--src/bootstrap/check.rs18
-rw-r--r--src/bootstrap/config.rs39
-rw-r--r--src/bootstrap/config/tests.rs8
-rw-r--r--src/bootstrap/flags.rs995
-rw-r--r--src/bootstrap/lib.rs4
-rw-r--r--src/bootstrap/test.rs2
10 files changed, 481 insertions, 730 deletions
diff --git a/src/bootstrap/Cargo.lock b/src/bootstrap/Cargo.lock
index 19d67e80a61..dfe6bb7f057 100644
--- a/src/bootstrap/Cargo.lock
+++ b/src/bootstrap/Cargo.lock
@@ -12,6 +12,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "anstyle"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
+
+[[package]]
 name = "autocfg"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -38,10 +44,10 @@ version = "0.0.0"
 dependencies = [
  "build_helper",
  "cc",
+ "clap",
  "cmake",
  "fd-lock",
  "filetime",
- "getopts",
  "hex",
  "ignore",
  "is-terminal",
@@ -92,6 +98,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
+name = "clap"
+version = "4.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "956ac1f6381d8d82ab4684768f89c0ea3afe66925ceadb4eeb3fc452ffc55d62"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+ "once_cell",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84080e799e54cff944f4b4a4b0e71630b0e0443b25b985175c7dddc1a859b749"
+dependencies = [
+ "anstyle",
+ "bitflags",
+ "clap_lex",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1"
+
+[[package]]
 name = "cmake"
 version = "0.1.48"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -175,7 +221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c"
 dependencies = [
  "quote",
- "syn",
+ "syn 1.0.102",
 ]
 
 [[package]]
@@ -261,15 +307,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "getopts"
-version = "0.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
-dependencies = [
- "unicode-width",
-]
-
-[[package]]
 name = "globset"
 version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -283,6 +320,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
 name = "hermit-abi"
 version = "0.1.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -486,18 +529,18 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.46"
+version = "1.0.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
+checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.18"
+version = "1.0.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
 dependencies = [
  "proc-macro2",
 ]
@@ -606,7 +649,7 @@ checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.102",
 ]
 
 [[package]]
@@ -643,6 +686,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "syn"
+version = "2.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
 name = "sysinfo"
 version = "0.26.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -708,12 +762,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
 
 [[package]]
-name = "unicode-width"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
-
-[[package]]
 name = "version_check"
 version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/src/bootstrap/Cargo.toml b/src/bootstrap/Cargo.toml
index 7b9eaceb00f..fd5eb740630 100644
--- a/src/bootstrap/Cargo.toml
+++ b/src/bootstrap/Cargo.toml
@@ -34,7 +34,6 @@ is-terminal = "0.4"
 build_helper = { path = "../tools/build_helper" }
 cmake = "0.1.38"
 filetime = "0.2"
-getopts = "0.2.19"
 cc = "1.0.69"
 libc = "0.2"
 hex = "0.4"
@@ -56,6 +55,7 @@ walkdir = "2"
 
 # Dependencies needed by the build-metrics feature
 sysinfo = { version = "0.26.0", optional = true }
+clap = { version = "4.2.4", default-features = false, features = ["std", "usage", "help", "derive", "error-context"] }
 
 # Solaris doesn't support flock() and thus fd-lock is not option now
 [target.'cfg(not(target_os = "solaris"))'.dependencies]
@@ -86,6 +86,7 @@ build-metrics = ["sysinfo"]
 # dependencies, only bootstrap itself.
 [profile.dev]
 debug = 0
+
 [profile.dev.package]
 # Only use debuginfo=1 to further reduce compile times.
 bootstrap.debug = 1
diff --git a/src/bootstrap/builder.rs b/src/bootstrap/builder.rs
index d9d4685dfc7..1267c0be719 100644
--- a/src/bootstrap/builder.rs
+++ b/src/bootstrap/builder.rs
@@ -33,6 +33,7 @@ pub use crate::Compiler;
 // - use std::lazy for `Lazy`
 // - use std::cell for `OnceCell`
 // Once they get stabilized and reach beta.
+use clap::ValueEnum;
 use once_cell::sync::{Lazy, OnceCell};
 
 pub struct Builder<'a> {
@@ -576,19 +577,24 @@ impl<'a> ShouldRun<'a> {
     }
 }
 
-#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, ValueEnum)]
 pub enum Kind {
+    #[clap(alias = "b")]
     Build,
+    #[clap(alias = "c")]
     Check,
     Clippy,
     Fix,
     Format,
+    #[clap(alias = "t")]
     Test,
     Bench,
+    #[clap(alias = "d")]
     Doc,
     Clean,
     Dist,
     Install,
+    #[clap(alias = "r")]
     Run,
     Setup,
     Suggest,
@@ -887,18 +893,19 @@ impl<'a> Builder<'a> {
     }
 
     pub fn new(build: &Build) -> Builder<'_> {
+        let paths = &build.config.paths;
         let (kind, paths) = match build.config.cmd {
-            Subcommand::Build { ref paths } => (Kind::Build, &paths[..]),
-            Subcommand::Check { ref paths } => (Kind::Check, &paths[..]),
-            Subcommand::Clippy { ref paths, .. } => (Kind::Clippy, &paths[..]),
-            Subcommand::Fix { ref paths } => (Kind::Fix, &paths[..]),
-            Subcommand::Doc { ref paths, .. } => (Kind::Doc, &paths[..]),
-            Subcommand::Test { ref paths, .. } => (Kind::Test, &paths[..]),
-            Subcommand::Bench { ref paths, .. } => (Kind::Bench, &paths[..]),
-            Subcommand::Dist { ref paths } => (Kind::Dist, &paths[..]),
-            Subcommand::Install { ref paths } => (Kind::Install, &paths[..]),
-            Subcommand::Run { ref paths, .. } => (Kind::Run, &paths[..]),
-            Subcommand::Clean { ref paths, .. } => (Kind::Clean, &paths[..]),
+            Subcommand::Build => (Kind::Build, &paths[..]),
+            Subcommand::Check { .. } => (Kind::Check, &paths[..]),
+            Subcommand::Clippy { .. } => (Kind::Clippy, &paths[..]),
+            Subcommand::Fix => (Kind::Fix, &paths[..]),
+            Subcommand::Doc { .. } => (Kind::Doc, &paths[..]),
+            Subcommand::Test { .. } => (Kind::Test, &paths[..]),
+            Subcommand::Bench { .. } => (Kind::Bench, &paths[..]),
+            Subcommand::Dist => (Kind::Dist, &paths[..]),
+            Subcommand::Install => (Kind::Install, &paths[..]),
+            Subcommand::Run { .. } => (Kind::Run, &paths[..]),
+            Subcommand::Clean { .. } => (Kind::Clean, &paths[..]),
             Subcommand::Format { .. } => (Kind::Format, &[][..]),
             Subcommand::Suggest { .. } => (Kind::Suggest, &[][..]),
             Subcommand::Setup { profile: ref path } => (
diff --git a/src/bootstrap/builder/tests.rs b/src/bootstrap/builder/tests.rs
index 72ac46b6bfd..c32fe59bbf0 100644
--- a/src/bootstrap/builder/tests.rs
+++ b/src/bootstrap/builder/tests.rs
@@ -236,7 +236,7 @@ mod defaults {
     fn doc_default() {
         let mut config = configure("doc", &["A"], &["A"]);
         config.compiler_docs = true;
-        config.cmd = Subcommand::Doc { paths: Vec::new(), open: false, json: false };
+        config.cmd = Subcommand::Doc { open: false, json: false };
         let mut cache = run_build(&[], config);
         let a = TargetSelection::from_user("A");
 
@@ -545,12 +545,13 @@ mod dist {
     fn test_with_no_doc_stage0() {
         let mut config = configure(&["A"], &["A"]);
         config.stage = 0;
+        config.paths = vec!["library/std".into()];
         config.cmd = Subcommand::Test {
-            paths: vec!["library/std".into()],
             test_args: vec![],
             rustc_args: vec![],
-            fail_fast: true,
-            doc_tests: DocTests::No,
+            no_fail_fast: false,
+            no_doc: true,
+            doc: false,
             bless: false,
             force_rerun: false,
             compare_mode: None,
@@ -558,6 +559,7 @@ mod dist {
             pass: None,
             run: None,
             only_modified: false,
+            skip: vec![],
         };
 
         let build = Build::new(config);
@@ -587,7 +589,7 @@ mod dist {
     fn doc_ci() {
         let mut config = configure(&["A"], &["A"]);
         config.compiler_docs = true;
-        config.cmd = Subcommand::Doc { paths: Vec::new(), open: false, json: false };
+        config.cmd = Subcommand::Doc { open: false, json: false };
         let build = Build::new(config);
         let mut builder = Builder::new(&build);
         builder.run_step_descriptions(&Builder::get_step_descriptions(Kind::Doc), &[]);
@@ -616,11 +618,12 @@ mod dist {
         // Behavior of `x.py test` doing various documentation tests.
         let mut config = configure(&["A"], &["A"]);
         config.cmd = Subcommand::Test {
-            paths: vec![],
             test_args: vec![],
             rustc_args: vec![],
-            fail_fast: true,
-            doc_tests: DocTests::Yes,
+            no_fail_fast: false,
+            doc: true,
+            no_doc: false,
+            skip: vec![],
             bless: false,
             force_rerun: false,
             compare_mode: None,
diff --git a/src/bootstrap/check.rs b/src/bootstrap/check.rs
index 956b82385f6..b11be96cefe 100644
--- a/src/bootstrap/check.rs
+++ b/src/bootstrap/check.rs
@@ -20,15 +20,7 @@ fn args(builder: &Builder<'_>) -> Vec<String> {
         arr.iter().copied().map(String::from)
     }
 
-    if let Subcommand::Clippy {
-        fix,
-        clippy_lint_allow,
-        clippy_lint_deny,
-        clippy_lint_warn,
-        clippy_lint_forbid,
-        ..
-    } = &builder.config.cmd
-    {
+    if let Subcommand::Clippy { fix, allow, deny, warn, forbid, .. } = &builder.config.cmd {
         // disable the most spammy clippy lints
         let ignored_lints = vec![
             "many_single_char_names", // there are a lot in stdarch
@@ -53,10 +45,10 @@ fn args(builder: &Builder<'_>) -> Vec<String> {
         args.extend(strings(&["--", "--cap-lints", "warn"]));
         args.extend(ignored_lints.iter().map(|lint| format!("-Aclippy::{}", lint)));
         let mut clippy_lint_levels: Vec<String> = Vec::new();
-        clippy_lint_allow.iter().for_each(|v| clippy_lint_levels.push(format!("-A{}", v)));
-        clippy_lint_deny.iter().for_each(|v| clippy_lint_levels.push(format!("-D{}", v)));
-        clippy_lint_warn.iter().for_each(|v| clippy_lint_levels.push(format!("-W{}", v)));
-        clippy_lint_forbid.iter().for_each(|v| clippy_lint_levels.push(format!("-F{}", v)));
+        allow.iter().for_each(|v| clippy_lint_levels.push(format!("-A{}", v)));
+        deny.iter().for_each(|v| clippy_lint_levels.push(format!("-D{}", v)));
+        warn.iter().for_each(|v| clippy_lint_levels.push(format!("-W{}", v)));
+        forbid.iter().for_each(|v| clippy_lint_levels.push(format!("-F{}", v)));
         args.extend(clippy_lint_levels);
         args.extend(builder.config.free_args.clone());
         args
diff --git a/src/bootstrap/config.rs b/src/bootstrap/config.rs
index ac51dc51aeb..f4e97d7dfed 100644
--- a/src/bootstrap/config.rs
+++ b/src/bootstrap/config.rs
@@ -21,7 +21,7 @@ use crate::cache::{Interned, INTERNER};
 use crate::cc_detect::{ndk_compiler, Language};
 use crate::channel::{self, GitInfo};
 pub use crate::flags::Subcommand;
-use crate::flags::{Color, Flags};
+use crate::flags::{Color, Flags, Warnings};
 use crate::util::{exe, output, t};
 use once_cell::sync::OnceCell;
 use serde::{Deserialize, Deserializer};
@@ -237,6 +237,8 @@ pub struct Config {
     initial_rustfmt: RefCell<RustfmtState>,
     #[cfg(test)]
     pub initial_rustfmt: RefCell<RustfmtState>,
+
+    pub paths: Vec<PathBuf>,
 }
 
 #[derive(Default, Deserialize, Clone)]
@@ -376,6 +378,16 @@ pub struct TargetSelection {
     file: Option<Interned<String>>,
 }
 
+/// Newtype over `Vec<TargetSelection>` so we can implement custom parsing logic
+#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
+pub struct TargetSelectionList(Vec<TargetSelection>);
+
+pub fn target_selection_list(s: &str) -> Result<TargetSelectionList, String> {
+    Ok(TargetSelectionList(
+        s.split(",").filter(|s| !s.is_empty()).map(TargetSelection::from_user).collect(),
+    ))
+}
+
 impl TargetSelection {
     pub fn from_user(selection: &str) -> Self {
         let path = Path::new(selection);
@@ -871,26 +883,24 @@ impl Config {
     }
 
     fn parse_inner<'a>(args: &[String], get_toml: impl 'a + Fn(&Path) -> TomlConfig) -> Config {
-        let flags = Flags::parse(&args);
+        let mut flags = Flags::parse(&args);
         let mut config = Config::default_opts();
 
         // Set flags.
+        config.paths = std::mem::take(&mut flags.paths);
         config.exclude = flags.exclude.into_iter().map(|path| TaskPath::parse(path)).collect();
         config.include_default_paths = flags.include_default_paths;
         config.rustc_error_format = flags.rustc_error_format;
         config.json_output = flags.json_output;
         config.on_fail = flags.on_fail;
-        config.jobs = flags.jobs.map(threads_from_config);
+        config.jobs = Some(threads_from_config(flags.jobs as u32));
         config.cmd = flags.cmd;
         config.incremental = flags.incremental;
         config.dry_run = if flags.dry_run { DryRun::UserSelected } else { DryRun::Disabled };
         config.keep_stage = flags.keep_stage;
         config.keep_stage_std = flags.keep_stage_std;
         config.color = flags.color;
-        config.free_args = flags.free_args.clone().unwrap_or_default();
-        if let Some(value) = flags.deny_warnings {
-            config.deny_warnings = value;
-        }
+        config.free_args = std::mem::take(&mut flags.free_args);
         config.llvm_profile_use = flags.llvm_profile_use;
         config.llvm_profile_generate = flags.llvm_profile_generate;
         config.llvm_bolt_profile_generate = flags.llvm_bolt_profile_generate;
@@ -1021,14 +1031,14 @@ impl Config {
             config.out = dir;
         }
 
-        config.hosts = if let Some(arg_host) = flags.host {
+        config.hosts = if let Some(TargetSelectionList(arg_host)) = flags.host {
             arg_host
         } else if let Some(file_host) = build.host {
             file_host.iter().map(|h| TargetSelection::from_user(h)).collect()
         } else {
             vec![config.build]
         };
-        config.targets = if let Some(arg_target) = flags.target {
+        config.targets = if let Some(TargetSelectionList(arg_target)) = flags.target {
             arg_target
         } else if let Some(file_target) = build.target {
             file_target.iter().map(|h| TargetSelection::from_user(h)).collect()
@@ -1064,7 +1074,7 @@ impl Config {
         set(&mut config.print_step_rusage, build.print_step_rusage);
         set(&mut config.patch_binaries_for_nix, build.patch_binaries_for_nix);
 
-        config.verbose = cmp::max(config.verbose, flags.verbose);
+        config.verbose = cmp::max(config.verbose, flags.verbose as usize);
 
         if let Some(install) = toml.install {
             config.prefix = install.prefix.map(PathBuf::from);
@@ -1137,7 +1147,14 @@ impl Config {
             config.rustc_default_linker = rust.default_linker;
             config.musl_root = rust.musl_root.map(PathBuf::from);
             config.save_toolstates = rust.save_toolstates.map(PathBuf::from);
-            set(&mut config.deny_warnings, flags.deny_warnings.or(rust.deny_warnings));
+            set(
+                &mut config.deny_warnings,
+                match flags.warnings {
+                    Warnings::Deny => Some(true),
+                    Warnings::Warn => Some(false),
+                    Warnings::Default => rust.deny_warnings,
+                },
+            );
             set(&mut config.backtrace_on_ice, rust.backtrace_on_ice);
             set(&mut config.rust_verify_llvm_ir, rust.verify_llvm_ir);
             config.rust_thin_lto_import_instr_limit = rust.thin_lto_import_instr_limit;
diff --git a/src/bootstrap/config/tests.rs b/src/bootstrap/config/tests.rs
index 50569eb4f37..d913ca295e2 100644
--- a/src/bootstrap/config/tests.rs
+++ b/src/bootstrap/config/tests.rs
@@ -1,4 +1,5 @@
-use super::{Config, TomlConfig};
+use super::{Config, Flags, TomlConfig};
+use clap::CommandFactory;
 use std::{env, path::Path};
 
 fn toml(config: &str) -> impl '_ + Fn(&Path) -> TomlConfig {
@@ -88,3 +89,8 @@ fn detect_src_and_out() {
         test(parse("build.build-dir = \"/tmp\""), build_dir);
     }
 }
+
+#[test]
+fn clap_verify() {
+    Flags::command().debug_assert();
+}
diff --git a/src/bootstrap/flags.rs b/src/bootstrap/flags.rs
index 2a0ebee9a6b..c79a1bf9563 100644
--- a/src/bootstrap/flags.rs
+++ b/src/bootstrap/flags.rs
@@ -5,724 +5,409 @@
 
 use std::path::PathBuf;
 
-use getopts::Options;
+use clap::{Parser, ValueEnum};
 
 use crate::builder::{Builder, Kind};
-use crate::config::{Config, TargetSelection};
+use crate::config::{target_selection_list, Config, TargetSelectionList};
 use crate::setup::Profile;
-use crate::util::t;
 use crate::{Build, DocTests};
 
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Default, Debug, ValueEnum)]
 pub enum Color {
     Always,
     Never,
+    #[default]
     Auto,
 }
 
-impl Default for Color {
-    fn default() -> Self {
-        Self::Auto
-    }
-}
-
-impl std::str::FromStr for Color {
-    type Err = ();
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s.to_lowercase().as_str() {
-            "always" => Ok(Self::Always),
-            "never" => Ok(Self::Never),
-            "auto" => Ok(Self::Auto),
-            _ => Err(()),
-        }
-    }
+/// Whether to deny warnings, emit them as warnings, or use the default behavior
+#[derive(Copy, Clone, Default, Debug, ValueEnum)]
+pub enum Warnings {
+    Deny,
+    Warn,
+    #[default]
+    Default,
 }
 
 /// Deserialized version of all flags for this compile.
+#[derive(Debug, Parser)]
+#[clap(
+    override_usage = "x.py <subcommand> [options] [<paths>...]",
+    disable_help_subcommand(true),
+    about = "",
+    next_line_help(false)
+)]
 pub struct Flags {
-    pub verbose: usize, // number of -v args; each extra -v after the first is passed to Cargo
-    pub on_fail: Option<String>,
-    pub stage: Option<u32>,
-    pub keep_stage: Vec<u32>,
-    pub keep_stage_std: Vec<u32>,
+    #[command(subcommand)]
+    pub cmd: Subcommand,
 
-    pub host: Option<Vec<TargetSelection>>,
-    pub target: Option<Vec<TargetSelection>>,
+    #[arg(global(true), short, long, action = clap::ArgAction::Count)]
+    /// use verbose output (-vv for very verbose)
+    pub verbose: u8, // each extra -v after the first is passed to Cargo
+    #[arg(global(true), short, long)]
+    /// use incremental compilation
+    pub incremental: bool,
+    #[arg(global(true), long, value_hint = clap::ValueHint::FilePath, value_name = "FILE")]
+    /// TOML configuration file for build
     pub config: Option<PathBuf>,
+    #[arg(global(true), long, value_hint = clap::ValueHint::DirPath, value_name = "DIR")]
+    /// Build directory, overrides `build.build-dir` in `config.toml`
     pub build_dir: Option<PathBuf>,
-    pub jobs: Option<u32>,
-    pub cmd: Subcommand,
-    pub incremental: bool,
+
+    #[arg(global(true), long, value_name = "BUILD")]
+    /// build target of the stage0 compiler
+    pub build: Option<String>,
+
+    #[arg(global(true), long, value_name = "HOST", value_parser = target_selection_list)]
+    /// host targets to build
+    pub host: Option<TargetSelectionList>,
+
+    #[arg(global(true), long, value_name = "TARGET", value_parser = target_selection_list)]
+    /// target targets to build
+    pub target: Option<TargetSelectionList>,
+
+    #[arg(global(true), long, value_name = "PATH")]
+    /// build paths to exclude
     pub exclude: Vec<PathBuf>,
+    #[arg(global(true), long)]
+    /// include default paths in addition to the provided ones
     pub include_default_paths: bool,
+
+    #[arg(global(true), long)]
     pub rustc_error_format: Option<String>,
-    pub json_output: bool,
+
+    #[arg(global(true), long, value_hint = clap::ValueHint::CommandString, value_name = "CMD")]
+    /// command to run on failure
+    pub on_fail: Option<String>,
+    #[arg(global(true), long)]
+    /// dry run; don't build anything
     pub dry_run: bool,
-    pub color: Color,
+    #[arg(global(true), long, value_name = "N")]
+    /// stage to build (indicates compiler to use/test, e.g., stage 0 uses the
+    /// bootstrap compiler, stage 1 the stage 0 rustc artifacts, etc.)
+    pub stage: Option<u32>,
 
+    #[arg(global(true), long, value_name = "N")]
+    /// stage(s) to keep without recompiling
+    /// (pass multiple times to keep e.g., both stages 0 and 1)
+    pub keep_stage: Vec<u32>,
+    #[arg(global(true), long, value_name = "N")]
+    /// stage(s) of the standard library to keep without recompiling
+    /// (pass multiple times to keep e.g., both stages 0 and 1)
+    pub keep_stage_std: Vec<u32>,
+    #[arg(global(true), long, value_hint = clap::ValueHint::DirPath, value_name = "DIR")]
+    /// path to the root of the rust checkout
+    pub src: Option<PathBuf>,
+
+    #[arg(
+        global(true),
+        short,
+        long,
+        default_value_t = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get),
+        value_name = "JOBS"
+    )]
+    /// number of jobs to run in parallel
+    pub jobs: usize,
     // This overrides the deny-warnings configuration option,
     // which passes -Dwarnings to the compiler invocations.
-    //
-    // true => deny, false => warn
-    pub deny_warnings: Option<bool>,
+    #[arg(global(true), long)]
+    #[clap(value_enum, default_value_t=Warnings::Default, value_name = "deny|warn")]
+    /// if value is deny, will deny warnings
+    /// if value is warn, will emit warnings
+    /// otherwise, use the default configured behaviour
+    pub warnings: Warnings,
+
+    #[arg(global(true), long, value_name = "FORMAT")]
+    /// rustc error format
+    pub error_format: Option<String>,
+    #[arg(global(true), long)]
+    /// use message-format=json
+    pub json_output: bool,
 
-    pub rust_profile_use: Option<String>,
-    pub rust_profile_generate: Option<String>,
+    #[arg(global(true), long, value_name = "STYLE")]
+    #[clap(value_enum, default_value_t = Color::Auto)]
+    /// whether to use color in cargo and rustc output
+    pub color: Color,
 
+    /// whether rebuilding llvm should be skipped, overriding `skip-rebuld` in config.toml
+    #[arg(global(true), long, value_name = "VALUE")]
+    pub llvm_skip_rebuild: Option<bool>,
+    /// generate PGO profile with rustc build
+    #[arg(global(true), long, value_name = "PROFILE")]
+    pub rust_profile_generate: Option<String>,
+    /// use PGO profile for rustc build
+    #[arg(global(true), long, value_name = "PROFILE")]
+    pub rust_profile_use: Option<String>,
+    /// use PGO profile for LLVM build
+    #[arg(global(true), long, value_name = "PROFILE")]
     pub llvm_profile_use: Option<String>,
     // LLVM doesn't support a custom location for generating profile
     // information.
     //
     // llvm_out/build/profiles/ is the location this writes to.
+    /// generate PGO profile with llvm built for rustc
+    #[arg(global(true), long)]
     pub llvm_profile_generate: bool,
+    /// generate BOLT profile for LLVM build
+    #[arg(global(true), long)]
     pub llvm_bolt_profile_generate: bool,
+    /// use BOLT profile for LLVM build
+    #[arg(global(true), long, value_name = "PROFILE")]
     pub llvm_bolt_profile_use: Option<String>,
+    #[arg(global(true))]
+    /// paths for the subcommand
+    pub paths: Vec<PathBuf>,
+    /// arguments passed to subcommands
+    #[arg(global(true), last(true), value_name = "ARGS")]
+    pub free_args: Vec<String>,
+}
+
+impl Flags {
+    pub fn parse(args: &[String]) -> Self {
+        let first = String::from("x.py");
+        let it = std::iter::once(&first).chain(args.iter());
+        // We need to check for `<cmd> -h -v`, in which case we list the paths
+        #[derive(Parser)]
+        #[clap(disable_help_flag(true))]
+        struct HelpVerboseOnly {
+            #[arg(short, long)]
+            help: bool,
+            #[arg(global(true), short, long, action = clap::ArgAction::Count)]
+            pub verbose: u8,
+            #[arg(value_enum)]
+            cmd: Kind,
+        }
+        if let Ok(HelpVerboseOnly { help: true, verbose: 1.., cmd: subcommand }) =
+            HelpVerboseOnly::try_parse_from(it.clone())
+        {
+            println!("note: updating submodules before printing available paths");
+            let config = Config::parse(&[String::from("build")]);
+            let build = Build::new(config);
+            let paths = Builder::get_help(&build, subcommand);
+            if let Some(s) = paths {
+                println!("{}", s);
+            } else {
+                panic!("No paths available for subcommand `{}`", subcommand.as_str());
+            }
+            crate::detail_exit(0);
+        }
 
-    /// Arguments appearing after `--` to be forwarded to tools,
-    /// e.g. `--fix-broken` or test arguments.
-    pub free_args: Option<Vec<String>>,
+        Flags::parse_from(it)
+    }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default, clap::Subcommand)]
 pub enum Subcommand {
-    Build {
-        paths: Vec<PathBuf>,
-    },
+    #[clap(aliases = ["b"], long_about = "\n
+    Arguments:
+        This subcommand accepts a number of paths to directories to the crates
+        and/or artifacts to compile. For example, for a quick build of a usable
+        compiler:
+            ./x.py build --stage 1 library/std
+        This will build a compiler and standard library from the local source code.
+        Once this is done, build/$ARCH/stage1 contains a usable compiler.
+        If no arguments are passed then the default artifacts for that stage are
+        compiled. For example:
+            ./x.py build --stage 0
+            ./x.py build ")]
+    /// Compile either the compiler or libraries
+    #[default]
+    Build,
+    #[clap(aliases = ["c"], long_about = "\n
+    Arguments:
+        This subcommand accepts a number of paths to directories to the crates
+        and/or artifacts to compile. For example:
+            ./x.py check library/std
+        If no arguments are passed then many artifacts are checked.")]
+    /// Compile either the compiler or libraries, using cargo check
     Check {
-        paths: Vec<PathBuf>,
+        #[arg(long)]
+        /// Check all targets
+        all_targets: bool,
     },
+    /// Run Clippy (uses rustup/cargo-installed clippy binary)
+    #[clap(long_about = "\n
+    Arguments:
+        This subcommand accepts a number of paths to directories to the crates
+        and/or artifacts to run clippy against. For example:
+            ./x.py clippy library/core
+            ./x.py clippy library/core library/proc_macro")]
     Clippy {
+        #[arg(long)]
         fix: bool,
-        paths: Vec<PathBuf>,
-        clippy_lint_allow: Vec<String>,
-        clippy_lint_deny: Vec<String>,
-        clippy_lint_warn: Vec<String>,
-        clippy_lint_forbid: Vec<String>,
-    },
-    Fix {
-        paths: Vec<PathBuf>,
+        /// clippy lints to allow
+        #[arg(global(true), short = 'A', action = clap::ArgAction::Append, value_name = "LINT")]
+        allow: Vec<String>,
+        /// clippy lints to deny
+        #[arg(global(true), short = 'D', action = clap::ArgAction::Append, value_name = "LINT")]
+        deny: Vec<String>,
+        /// clippy lints to warn on
+        #[arg(global(true), short = 'W', action = clap::ArgAction::Append, value_name = "LINT")]
+        warn: Vec<String>,
+        /// clippy lints to forbid
+        #[arg(global(true), short = 'F', action = clap::ArgAction::Append, value_name = "LINT")]
+        forbid: Vec<String>,
     },
+    /// Run cargo fix
+    #[clap(long_about = "\n
+    Arguments:
+        This subcommand accepts a number of paths to directories to the crates
+        and/or artifacts to run `cargo fix` against. For example:
+            ./x.py fix library/core
+            ./x.py fix library/core library/proc_macro")]
+    Fix,
+    #[clap(
+        name = "fmt",
+        long_about = "\n
+    Arguments:
+        This subcommand optionally accepts a `--check` flag which succeeds if formatting is correct and
+        fails if it is not. For example:
+            ./x.py fmt
+            ./x.py fmt --check"
+    )]
+    /// Run rustfmt
     Format {
-        paths: Vec<PathBuf>,
+        /// check formatting instead of applying
+        #[arg(long)]
         check: bool,
     },
+    #[clap(aliases = ["d"], long_about = "\n
+    Arguments:
+        This subcommand accepts a number of paths to directories of documentation
+        to build. For example:
+            ./x.py doc src/doc/book
+            ./x.py doc src/doc/nomicon
+            ./x.py doc src/doc/book library/std
+            ./x.py doc library/std --json
+            ./x.py doc library/std --open
+        If no arguments are passed then everything is documented:
+            ./x.py doc
+            ./x.py doc --stage 1")]
+    /// Build documentation
     Doc {
-        paths: Vec<PathBuf>,
+        #[arg(long)]
+        /// open the docs in a browser
         open: bool,
+        #[arg(long)]
+        /// render the documentation in JSON format in addition to the usual HTML format
         json: bool,
     },
+    #[clap(aliases = ["t"], long_about = "\n
+    Arguments:
+        This subcommand accepts a number of paths to test directories that
+        should be compiled and run. For example:
+            ./x.py test tests/ui
+            ./x.py test library/std --test-args hash_map
+            ./x.py test library/std --stage 0 --no-doc
+            ./x.py test tests/ui --bless
+            ./x.py test tests/ui --compare-mode chalk
+        Note that `test tests/* --stage N` does NOT depend on `build compiler/rustc --stage N`;
+        just like `build library/std --stage N` it tests the compiler produced by the previous
+        stage.
+        Execute tool tests with a tool name argument:
+            ./x.py test tidy
+        If no arguments are passed then the complete artifacts for that stage are
+        compiled and tested.
+            ./x.py test
+            ./x.py test --stage 1")]
+    /// Build and run some test suites
     Test {
-        paths: Vec<PathBuf>,
-        /// Whether to automatically update stderr/stdout files
+        #[arg(long)]
+        /// run all tests regardless of failure
+        no_fail_fast: bool,
+        #[arg(long, value_name = "SUBSTRING")]
+        /// skips tests matching SUBSTRING, if supported by test tool. May be passed multiple times
+        skip: Vec<String>,
+        #[arg(long, value_name = "ARGS", allow_hyphen_values(true))]
+        /// extra arguments to be passed for the test tool being used
+        /// (e.g. libtest, compiletest or rustdoc)
+        test_args: Vec<String>,
+        /// extra options to pass the compiler when running tests
+        #[arg(long, value_name = "ARGS", allow_hyphen_values(true))]
+        rustc_args: Vec<String>,
+        #[arg(long)]
+        /// do not run doc tests
+        no_doc: bool,
+        #[arg(long)]
+        /// only run doc tests
+        doc: bool,
+        #[arg(long)]
+        /// whether to automatically update stderr/stdout files
         bless: bool,
+        #[arg(long)]
+        /// rerun tests even if the inputs are unchanged
         force_rerun: bool,
+        #[arg(long)]
+        /// only run tests that result has been changed
+        only_modified: bool,
+        #[arg(long, value_name = "COMPARE MODE")]
+        /// mode describing what file the actual ui output will be compared to
         compare_mode: Option<String>,
+        #[arg(long, value_name = "check | build | run")]
+        /// force {check,build,run}-pass tests to this mode.
         pass: Option<String>,
+        #[arg(long, value_name = "auto | always | never")]
+        /// whether to execute run-* tests
         run: Option<String>,
-        test_args: Vec<String>,
-        rustc_args: Vec<String>,
-        fail_fast: bool,
-        doc_tests: DocTests,
+        #[arg(long)]
+        /// enable this to generate a Rustfix coverage file, which is saved in
+        /// `/<build_base>/rustfix_missing_coverage.txt`
         rustfix_coverage: bool,
-        only_modified: bool,
     },
+    /// Build and run some benchmarks
     Bench {
-        paths: Vec<PathBuf>,
+        #[arg(long, allow_hyphen_values(true))]
         test_args: Vec<String>,
     },
+    /// Clean out build directories
     Clean {
-        paths: Vec<PathBuf>,
+        #[arg(long)]
         all: bool,
     },
-    Dist {
-        paths: Vec<PathBuf>,
-    },
-    Install {
-        paths: Vec<PathBuf>,
-    },
+    /// Duild distribution artifacts
+    Dist,
+    /// Install distribution artifacts
+    Install,
+    #[clap(aliases = ["r"], long_about = "\n
+    Arguments:
+        This subcommand accepts a number of paths to tools to build and run. For
+        example:
+            ./x.py run src/tools/expand-yaml-anchors
+        At least a tool needs to be called.")]
+    /// Run tools contained in this repository
     Run {
-        paths: Vec<PathBuf>,
+        /// arguments for the tool
+        #[arg(long, allow_hyphen_values(true))]
         args: Vec<String>,
     },
-    Setup {
-        profile: Option<PathBuf>,
-    },
-    Suggest {
-        run: bool,
-    },
-}
-
-impl Default for Subcommand {
-    fn default() -> Subcommand {
-        Subcommand::Build { paths: vec![PathBuf::from("nowhere")] }
-    }
-}
-
-impl Flags {
-    pub fn parse(args: &[String]) -> Flags {
-        let (args, free_args) = if let Some(pos) = args.iter().position(|s| s == "--") {
-            let (args, free) = args.split_at(pos);
-            (args, Some(free[1..].to_vec()))
-        } else {
-            (args, None)
-        };
-        let mut subcommand_help = String::from(
-            "\
-Usage: x.py <subcommand> [options] [<paths>...]
-
-Subcommands:
-    build, b    Compile either the compiler or libraries
-    check, c    Compile either the compiler or libraries, using cargo check
-    clippy      Run clippy (uses rustup/cargo-installed clippy binary)
-    fix         Run cargo fix
-    fmt         Run rustfmt
-    test, t     Build and run some test suites
-    bench       Build and run some benchmarks
-    doc, d      Build documentation
-    clean       Clean out build directories
-    dist        Build distribution artifacts
-    install     Install distribution artifacts
-    run, r      Run tools contained in this repository
-    setup       Create a config.toml (making it easier to use `x.py` itself)
-    suggest     Suggest a subset of tests to run, based on modified files
-
-To learn more about a subcommand, run `./x.py <subcommand> -h`",
-        );
-
-        let mut opts = Options::new();
-        // Options common to all subcommands
-        opts.optflagmulti("v", "verbose", "use verbose output (-vv for very verbose)");
-        opts.optflag("i", "incremental", "use incremental compilation");
-        opts.optopt("", "config", "TOML configuration file for build", "FILE");
-        opts.optopt(
-            "",
-            "build-dir",
-            "Build directory, overrides `build.build-dir` in `config.toml`",
-            "DIR",
-        );
-        opts.optopt("", "build", "build target of the stage0 compiler", "BUILD");
-        opts.optmulti("", "host", "host targets to build", "HOST");
-        opts.optmulti("", "target", "target targets to build", "TARGET");
-        opts.optmulti("", "exclude", "build paths to exclude", "PATH");
-        opts.optflag(
-            "",
-            "include-default-paths",
-            "include default paths in addition to the provided ones",
-        );
-        opts.optopt("", "on-fail", "command to run on failure", "CMD");
-        opts.optflag("", "dry-run", "dry run; don't build anything");
-        opts.optopt(
-            "",
-            "stage",
-            "stage to build (indicates compiler to use/test, e.g., stage 0 uses the \
-             bootstrap compiler, stage 1 the stage 0 rustc artifacts, etc.)",
-            "N",
-        );
-        opts.optmulti(
-            "",
-            "keep-stage",
-            "stage(s) to keep without recompiling \
-            (pass multiple times to keep e.g., both stages 0 and 1)",
-            "N",
-        );
-        opts.optmulti(
-            "",
-            "keep-stage-std",
-            "stage(s) of the standard library to keep without recompiling \
-            (pass multiple times to keep e.g., both stages 0 and 1)",
-            "N",
-        );
-        opts.optopt("", "src", "path to the root of the rust checkout", "DIR");
-        let j_msg = format!(
-            "number of jobs to run in parallel; \
-             defaults to {} (this host's logical CPU count)",
-            std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get)
-        );
-        opts.optopt("j", "jobs", &j_msg, "JOBS");
-        opts.optflag("h", "help", "print this help message");
-        opts.optopt(
-            "",
-            "warnings",
-            "if value is deny, will deny warnings, otherwise use default",
-            "VALUE",
-        );
-        opts.optopt("", "error-format", "rustc error format", "FORMAT");
-        opts.optflag("", "json-output", "use message-format=json");
-        opts.optopt("", "color", "whether to use color in cargo and rustc output", "STYLE");
-        opts.optopt(
-            "",
-            "rust-profile-generate",
-            "generate PGO profile with rustc build",
-            "PROFILE",
-        );
-        opts.optopt("", "rust-profile-use", "use PGO profile for rustc build", "PROFILE");
-        opts.optflag("", "llvm-profile-generate", "generate PGO profile with llvm built for rustc");
-        opts.optopt("", "llvm-profile-use", "use PGO profile for llvm build", "PROFILE");
-        opts.optmulti("A", "", "allow certain clippy lints", "OPT");
-        opts.optmulti("D", "", "deny certain clippy lints", "OPT");
-        opts.optmulti("W", "", "warn about certain clippy lints", "OPT");
-        opts.optmulti("F", "", "forbid certain clippy lints", "OPT");
-        opts.optflag("", "llvm-bolt-profile-generate", "generate BOLT profile for LLVM build");
-        opts.optopt("", "llvm-bolt-profile-use", "use BOLT profile for LLVM build", "PROFILE");
-
-        // We can't use getopt to parse the options until we have completed specifying which
-        // options are valid, but under the current implementation, some options are conditional on
-        // the subcommand. Therefore we must manually identify the subcommand first, so that we can
-        // complete the definition of the options.  Then we can use the getopt::Matches object from
-        // there on out.
-        let subcommand = match args.iter().find_map(|s| Kind::parse(&s)) {
-            Some(s) => s,
-            None => {
-                // No or an invalid subcommand -- show the general usage and subcommand help
-                // An exit code will be 0 when no subcommand is given, and 1 in case of an invalid
-                // subcommand.
-                println!("{}\n", subcommand_help);
-                let exit_code = if args.is_empty() { 0 } else { 1 };
-                crate::detail_exit(exit_code);
-            }
-        };
-
-        // Some subcommands get extra options
-        match subcommand {
-            Kind::Test => {
-                opts.optflag("", "no-fail-fast", "Run all tests regardless of failure");
-                opts.optmulti("", "skip", "skips tests matching SUBSTRING, if supported by test tool. May be passed multiple times", "SUBSTRING");
-                opts.optmulti(
-                    "",
-                    "test-args",
-                    "extra arguments to be passed for the test tool being used \
-                        (e.g. libtest, compiletest or rustdoc)",
-                    "ARGS",
-                );
-                opts.optmulti(
-                    "",
-                    "rustc-args",
-                    "extra options to pass the compiler when running tests",
-                    "ARGS",
-                );
-                opts.optflag("", "no-doc", "do not run doc tests");
-                opts.optflag("", "doc", "only run doc tests");
-                opts.optflag("", "bless", "update all stderr/stdout files of failing ui tests");
-                opts.optflag("", "force-rerun", "rerun tests even if the inputs are unchanged");
-                opts.optflag("", "only-modified", "only run tests that result has been changed");
-                opts.optopt(
-                    "",
-                    "compare-mode",
-                    "mode describing what file the actual ui output will be compared to",
-                    "COMPARE MODE",
-                );
-                opts.optopt(
-                    "",
-                    "pass",
-                    "force {check,build,run}-pass tests to this mode.",
-                    "check | build | run",
-                );
-                opts.optopt("", "run", "whether to execute run-* tests", "auto | always | never");
-                opts.optflag(
-                    "",
-                    "rustfix-coverage",
-                    "enable this to generate a Rustfix coverage file, which is saved in \
-                        `/<build_base>/rustfix_missing_coverage.txt`",
-                );
-            }
-            Kind::Check => {
-                opts.optflag("", "all-targets", "Check all targets");
-            }
-            Kind::Bench => {
-                opts.optmulti("", "test-args", "extra arguments", "ARGS");
-            }
-            Kind::Clippy => {
-                opts.optflag("", "fix", "automatically apply lint suggestions");
-            }
-            Kind::Doc => {
-                opts.optflag("", "open", "open the docs in a browser");
-                opts.optflag(
-                    "",
-                    "json",
-                    "render the documentation in JSON format in addition to the usual HTML format",
-                );
-            }
-            Kind::Clean => {
-                opts.optflag("", "all", "clean all build artifacts");
-            }
-            Kind::Format => {
-                opts.optflag("", "check", "check formatting instead of applying.");
-            }
-            Kind::Run => {
-                opts.optmulti("", "args", "arguments for the tool", "ARGS");
-            }
-            Kind::Suggest => {
-                opts.optflag("", "run", "run suggested tests");
-            }
-            _ => {}
-        };
-
-        // fn usage()
-        let usage = |exit_code: i32, opts: &Options, verbose: bool, subcommand_help: &str| -> ! {
-            println!("{}", opts.usage(subcommand_help));
-            if verbose {
-                // We have an unfortunate situation here: some Steps use `builder.in_tree_crates` to determine their paths.
-                // To determine those crates, we need to run `cargo metadata`, which means we need all submodules to be checked out.
-                // That takes a while to run, so only do it when paths were explicitly requested, not on all CLI errors.
-                // `Build::new` won't load submodules for the `setup` command.
-                let cmd = if verbose {
-                    println!("note: updating submodules before printing available paths");
-                    "build"
-                } else {
-                    "setup"
-                };
-                let config = Config::parse(&[cmd.to_string()]);
-                let build = Build::new(config);
-                let paths = Builder::get_help(&build, subcommand);
-
-                if let Some(s) = paths {
-                    println!("{}", s);
-                } else {
-                    panic!("No paths available for subcommand `{}`", subcommand.as_str());
-                }
-            } else {
-                println!(
-                    "Run `./x.py {} -h -v` to see a list of available paths.",
-                    subcommand.as_str()
-                );
-            }
-            crate::detail_exit(exit_code);
-        };
-
-        // Done specifying what options are possible, so do the getopts parsing
-        let matches = opts.parse(args).unwrap_or_else(|e| {
-            // Invalid argument/option format
-            println!("\n{}\n", e);
-            usage(1, &opts, false, &subcommand_help);
-        });
-
-        // Extra sanity check to make sure we didn't hit this crazy corner case:
-        //
-        //     ./x.py --frobulate clean build
-        //            ^-- option  ^     ^- actual subcommand
-        //                        \_ arg to option could be mistaken as subcommand
-        let mut pass_sanity_check = true;
-        match matches.free.get(0).and_then(|s| Kind::parse(&s)) {
-            Some(check_subcommand) => {
-                if check_subcommand != subcommand {
-                    pass_sanity_check = false;
-                }
-            }
-            None => {
-                pass_sanity_check = false;
-            }
-        }
-        if !pass_sanity_check {
-            eprintln!("{}\n", subcommand_help);
-            eprintln!(
-                "Sorry, I couldn't figure out which subcommand you were trying to specify.\n\
-                 You may need to move some options to after the subcommand.\n"
-            );
-            crate::detail_exit(1);
-        }
-        // Extra help text for some commands
-        match subcommand {
-            Kind::Build => {
-                subcommand_help.push_str(
-                    "\n
-Arguments:
-    This subcommand accepts a number of paths to directories to the crates
-    and/or artifacts to compile. For example, for a quick build of a usable
-    compiler:
-
-        ./x.py build --stage 1 library/std
-
-    This will build a compiler and standard library from the local source code.
-    Once this is done, build/$ARCH/stage1 contains a usable compiler.
-
-    If no arguments are passed then the default artifacts for that stage are
-    compiled. For example:
-
-        ./x.py build --stage 0
-        ./x.py build ",
-                );
-            }
-            Kind::Check => {
-                subcommand_help.push_str(
-                    "\n
-Arguments:
-    This subcommand accepts a number of paths to directories to the crates
-    and/or artifacts to compile. For example:
-
-        ./x.py check library/std
-
-    If no arguments are passed then many artifacts are checked.",
-                );
-            }
-            Kind::Clippy => {
-                subcommand_help.push_str(
-                    "\n
-Arguments:
-    This subcommand accepts a number of paths to directories to the crates
-    and/or artifacts to run clippy against. For example:
-
-        ./x.py clippy library/core
-        ./x.py clippy library/core library/proc_macro",
-                );
-            }
-            Kind::Fix => {
-                subcommand_help.push_str(
-                    "\n
-Arguments:
-    This subcommand accepts a number of paths to directories to the crates
-    and/or artifacts to run `cargo fix` against. For example:
-
-        ./x.py fix library/core
-        ./x.py fix library/core library/proc_macro",
-                );
-            }
-            Kind::Format => {
-                subcommand_help.push_str(
-                    "\n
-Arguments:
-    This subcommand optionally accepts a `--check` flag which succeeds if formatting is correct and
-    fails if it is not. For example:
-
-        ./x.py fmt
-        ./x.py fmt --check",
-                );
-            }
-            Kind::Test => {
-                subcommand_help.push_str(
-                    "\n
-Arguments:
-    This subcommand accepts a number of paths to test directories that
-    should be compiled and run. For example:
-
-        ./x.py test tests/ui
-        ./x.py test library/std --test-args hash_map
-        ./x.py test library/std --stage 0 --no-doc
-        ./x.py test tests/ui --bless
-        ./x.py test tests/ui --compare-mode chalk
-
-    Note that `test tests/* --stage N` does NOT depend on `build compiler/rustc --stage N`;
-    just like `build library/std --stage N` it tests the compiler produced by the previous
-    stage.
-
-    Execute tool tests with a tool name argument:
-
-        ./x.py test tidy
-
-    If no arguments are passed then the complete artifacts for that stage are
-    compiled and tested.
-
-        ./x.py test
-        ./x.py test --stage 1",
-                );
-            }
-            Kind::Doc => {
-                subcommand_help.push_str(
-                    "\n
-Arguments:
-    This subcommand accepts a number of paths to directories of documentation
-    to build. For example:
-
-        ./x.py doc src/doc/book
-        ./x.py doc src/doc/nomicon
-        ./x.py doc src/doc/book library/std
-        ./x.py doc library/std --json
-        ./x.py doc library/std --open
-
-    If no arguments are passed then everything is documented:
-
-        ./x.py doc
-        ./x.py doc --stage 1",
-                );
-            }
-            Kind::Run => {
-                subcommand_help.push_str(
-                    "\n
-Arguments:
-    This subcommand accepts a number of paths to tools to build and run. For
-    example:
-
-        ./x.py run src/tools/expand-yaml-anchors
-
-    At least a tool needs to be called.",
-                );
-            }
-            Kind::Setup => {
-                subcommand_help.push_str(&format!(
-                    "\n
+    /// Set up the environment for development
+    #[clap(long_about = format!(
+        "\n
 x.py setup creates a `config.toml` which changes the defaults for x.py itself,
-as well as setting up a git pre-push hook, VS code config and toolchain link.
-
+as well as setting up a git pre-push hook, VS Code config and toolchain link.
 Arguments:
     This subcommand accepts a 'profile' to use for builds. For example:
-
         ./x.py setup library
-
     The profile is optional and you will be prompted interactively if it is not given.
     The following profiles are available:
-
 {}
-
-    To only set up the git hook, VS code or toolchain link, you may use
+    To only set up the git hook, VS Code config or toolchain link, you may use
         ./x.py setup hook
         ./x.py setup vscode
-        ./x.py setup link
-",
-                    Profile::all_for_help("        ").trim_end()
-                ));
-            }
-            Kind::Bench | Kind::Clean | Kind::Dist | Kind::Install | Kind::Suggest => {}
-        };
-        // Get any optional paths which occur after the subcommand
-        let mut paths = matches.free[1..].iter().map(|p| p.into()).collect::<Vec<PathBuf>>();
-
-        let verbose = matches.opt_present("verbose");
-
-        // User passed in -h/--help?
-        if matches.opt_present("help") {
-            usage(0, &opts, verbose, &subcommand_help);
-        }
-
-        let cmd = match subcommand {
-            Kind::Build => Subcommand::Build { paths },
-            Kind::Check => {
-                if matches.opt_present("all-targets") {
-                    println!(
-                        "Warning: --all-targets is now on by default and does not need to be passed explicitly."
-                    );
-                }
-                Subcommand::Check { paths }
-            }
-            Kind::Clippy => Subcommand::Clippy {
-                paths,
-                fix: matches.opt_present("fix"),
-                clippy_lint_allow: matches.opt_strs("A"),
-                clippy_lint_warn: matches.opt_strs("W"),
-                clippy_lint_deny: matches.opt_strs("D"),
-                clippy_lint_forbid: matches.opt_strs("F"),
-            },
-            Kind::Fix => Subcommand::Fix { paths },
-            Kind::Test => Subcommand::Test {
-                paths,
-                bless: matches.opt_present("bless"),
-                force_rerun: matches.opt_present("force-rerun"),
-                compare_mode: matches.opt_str("compare-mode"),
-                pass: matches.opt_str("pass"),
-                run: matches.opt_str("run"),
-                test_args: matches.opt_strs("test-args"),
-                rustc_args: matches.opt_strs("rustc-args"),
-                fail_fast: !matches.opt_present("no-fail-fast"),
-                rustfix_coverage: matches.opt_present("rustfix-coverage"),
-                only_modified: matches.opt_present("only-modified"),
-                doc_tests: if matches.opt_present("doc") {
-                    DocTests::Only
-                } else if matches.opt_present("no-doc") {
-                    DocTests::No
-                } else {
-                    DocTests::Yes
-                },
-            },
-            Kind::Bench => Subcommand::Bench { paths, test_args: matches.opt_strs("test-args") },
-            Kind::Doc => Subcommand::Doc {
-                paths,
-                open: matches.opt_present("open"),
-                json: matches.opt_present("json"),
-            },
-            Kind::Clean => Subcommand::Clean { all: matches.opt_present("all"), paths },
-            Kind::Format => Subcommand::Format { check: matches.opt_present("check"), paths },
-            Kind::Dist => Subcommand::Dist { paths },
-            Kind::Install => Subcommand::Install { paths },
-            Kind::Suggest => Subcommand::Suggest { run: matches.opt_present("run") },
-            Kind::Run => {
-                if paths.is_empty() {
-                    println!("\nrun requires at least a path!\n");
-                    usage(1, &opts, verbose, &subcommand_help);
-                }
-                Subcommand::Run { paths, args: matches.opt_strs("args") }
-            }
-            Kind::Setup => {
-                let profile = if paths.len() > 1 {
-                    eprintln!("\nerror: At most one option can be passed to setup\n");
-                    usage(1, &opts, verbose, &subcommand_help)
-                } else if let Some(path) = paths.pop() {
-                    let profile_string = t!(path.into_os_string().into_string().map_err(
-                        |path| format!("{} is not a valid UTF8 string", path.to_string_lossy())
-                    ));
-
-                    let profile = profile_string.parse().unwrap_or_else(|err| {
-                        eprintln!("error: {}", err);
-                        eprintln!("help: the available profiles are:");
-                        eprint!("{}", Profile::all_for_help("- "));
-                        crate::detail_exit(1);
-                    });
-                    Some(profile)
-                } else {
-                    None
-                };
-                Subcommand::Setup { profile }
-            }
-        };
-
-        Flags {
-            verbose: matches.opt_count("verbose"),
-            stage: matches.opt_str("stage").map(|j| j.parse().expect("`stage` should be a number")),
-            dry_run: matches.opt_present("dry-run"),
-            on_fail: matches.opt_str("on-fail"),
-            rustc_error_format: matches.opt_str("error-format"),
-            json_output: matches.opt_present("json-output"),
-            keep_stage: matches
-                .opt_strs("keep-stage")
-                .into_iter()
-                .map(|j| j.parse().expect("`keep-stage` should be a number"))
-                .collect(),
-            keep_stage_std: matches
-                .opt_strs("keep-stage-std")
-                .into_iter()
-                .map(|j| j.parse().expect("`keep-stage-std` should be a number"))
-                .collect(),
-            host: if matches.opt_present("host") {
-                Some(
-                    split(&matches.opt_strs("host"))
-                        .into_iter()
-                        .map(|x| TargetSelection::from_user(&x))
-                        .collect::<Vec<_>>(),
-                )
-            } else {
-                None
-            },
-            target: if matches.opt_present("target") {
-                Some(
-                    split(&matches.opt_strs("target"))
-                        .into_iter()
-                        .map(|x| TargetSelection::from_user(&x))
-                        .collect::<Vec<_>>(),
-                )
-            } else {
-                None
-            },
-            config: matches.opt_str("config").map(PathBuf::from),
-            build_dir: matches.opt_str("build-dir").map(PathBuf::from),
-            jobs: matches.opt_str("jobs").map(|j| j.parse().expect("`jobs` should be a number")),
-            cmd,
-            incremental: matches.opt_present("incremental"),
-            exclude: split(&matches.opt_strs("exclude"))
-                .into_iter()
-                .map(|p| p.into())
-                .collect::<Vec<_>>(),
-            include_default_paths: matches.opt_present("include-default-paths"),
-            deny_warnings: parse_deny_warnings(&matches),
-            color: matches
-                .opt_get_default("color", Color::Auto)
-                .expect("`color` should be `always`, `never`, or `auto`"),
-            rust_profile_use: matches.opt_str("rust-profile-use"),
-            rust_profile_generate: matches.opt_str("rust-profile-generate"),
-            llvm_profile_use: matches.opt_str("llvm-profile-use"),
-            llvm_profile_generate: matches.opt_present("llvm-profile-generate"),
-            llvm_bolt_profile_generate: matches.opt_present("llvm-bolt-profile-generate"),
-            llvm_bolt_profile_use: matches.opt_str("llvm-bolt-profile-use"),
-            free_args,
-        }
-    }
+        ./x.py setup link", Profile::all_for_help("        ").trim_end()))]
+    Setup {
+        /// Either the profile for `config.toml` or another setup action.
+        /// May be omitted to set up interactively
+        #[arg(value_name = "<PROFILE>|hook|vscode|link")]
+        profile: Option<PathBuf>,
+    },
+    /// Suggest a subset of tests to run, based on modified files
+    #[clap(long_about = "\n")]
+    Suggest {
+        /// run suggested tests
+        #[arg(long)]
+        run: bool,
+    },
 }
 
 impl Subcommand {
@@ -756,14 +441,22 @@ impl Subcommand {
 
     pub fn fail_fast(&self) -> bool {
         match *self {
-            Subcommand::Test { fail_fast, .. } => fail_fast,
+            Subcommand::Test { no_fail_fast, .. } => !no_fail_fast,
             _ => false,
         }
     }
 
     pub fn doc_tests(&self) -> DocTests {
         match *self {
-            Subcommand::Test { doc_tests, .. } => doc_tests,
+            Subcommand::Test { doc, no_doc, .. } => {
+                if doc {
+                    DocTests::Only
+                } else if no_doc {
+                    DocTests::No
+                } else {
+                    DocTests::Yes
+                }
+            }
             _ => DocTests::Yes,
         }
     }
@@ -831,19 +524,3 @@ impl Subcommand {
         }
     }
 }
-
-fn split(s: &[String]) -> Vec<String> {
-    s.iter().flat_map(|s| s.split(',')).filter(|s| !s.is_empty()).map(|s| s.to_string()).collect()
-}
-
-fn parse_deny_warnings(matches: &getopts::Matches) -> Option<bool> {
-    match matches.opt_str("warnings").as_deref() {
-        Some("deny") => Some(true),
-        Some("warn") => Some(false),
-        Some(value) => {
-            eprintln!(r#"invalid value for --warnings: {:?}, expected "warn" or "deny""#, value,);
-            crate::detail_exit(1);
-        }
-        None => None,
-    }
-}
diff --git a/src/bootstrap/lib.rs b/src/bootstrap/lib.rs
index 59d2e9cc69e..994336977dc 100644
--- a/src/bootstrap/lib.rs
+++ b/src/bootstrap/lib.rs
@@ -660,8 +660,8 @@ impl Build {
 
         // hardcoded subcommands
         match &self.config.cmd {
-            Subcommand::Format { check, paths } => {
-                return format::format(&builder::Builder::new(&self), *check, &paths);
+            Subcommand::Format { check } => {
+                return format::format(&builder::Builder::new(&self), *check, &self.config.paths);
             }
             Subcommand::Suggest { run } => {
                 return suggest::suggest(&builder::Builder::new(&self), *run);
diff --git a/src/bootstrap/test.rs b/src/bootstrap/test.rs
index 28813266a4d..854b7f5bd3a 100644
--- a/src/bootstrap/test.rs
+++ b/src/bootstrap/test.rs
@@ -1545,7 +1545,7 @@ note: if you're sure you want to do this, please open an issue as to why. In the
 
         // Get paths from cmd args
         let paths = match &builder.config.cmd {
-            Subcommand::Test { ref paths, .. } => &paths[..],
+            Subcommand::Test { .. } => &builder.config.paths[..],
             _ => &[],
         };