Skip to main content

cargo_msrv/cli/
mod.rs

1use crate::cli::custom_check_opts::CustomCheckOpts;
2use crate::cli::rust_releases_opts::RustReleasesOpts;
3use crate::cli::shared_opts::SharedOpts;
4use crate::cli::toolchain_opts::ToolchainOpts;
5use crate::context::list::ListMsrvVariant;
6use crate::manifest::bare_version::BareVersion;
7use clap::{Args, Parser, Subcommand};
8use clap_cargo::style::CLAP_STYLING;
9use std::ffi::{OsStr, OsString};
10
11pub(crate) mod custom_check_opts;
12pub(crate) mod rust_releases_opts;
13pub(crate) mod shared_opts;
14pub(crate) mod toolchain_opts;
15
16#[derive(Debug, Parser)]
17#[command(version, name = "cargo", bin_name = "cargo", max_term_width = 120, styles = CLAP_STYLING)]
18pub struct CargoCli {
19    #[command(subcommand)]
20    subcommand: CargoMsrvCli,
21}
22
23impl CargoCli {
24    pub fn parse_args<I: IntoIterator<Item = T>, T: Into<OsString> + Clone>(args: I) -> Self {
25        let modified_args = modify_args(args);
26        CargoCli::parse_from(modified_args)
27    }
28
29    pub fn to_cargo_msrv_cli(self) -> CargoMsrvCli {
30        self.subcommand
31    }
32}
33
34// When we call cargo-msrv with cargo, cargo will supply the msrv subcommand, in addition
35// to the binary name itself. As a result, when you call cargo-msrv without cargo, for example
36// `cargo-msrv` (without cargo) instead of `cargo msrv` (with cargo), the process will receive
37// too many arguments, and you will have to specify the subcommand again like so: `cargo-msrv msrv`.
38// This function removes the subcommand when it's present in addition to the program name.
39fn modify_args<I: IntoIterator<Item = T>, T: Into<OsString> + Clone>(
40    args: I,
41) -> impl IntoIterator<Item = OsString> {
42    let mut args = args.into_iter().map(Into::into).collect::<Vec<_>>();
43
44    if args.len() >= 2 {
45        let program: &OsStr = args[0].as_os_str();
46        let program = program.to_string_lossy();
47
48        // when `cargo-msrv(.exe)` or `msrv` are present
49        if program.ends_with("cargo-msrv") || program.ends_with("cargo-msrv.exe") {
50            // remove `msrv`, or `cargo-msrv(.exe)`
51            args[0] = OsStr::new("cargo").to_os_string();
52
53            let cargo_msrv_subcmd: &OsStr = args[1].as_os_str();
54            let cargo_msrv_subcmd = cargo_msrv_subcmd.to_string_lossy();
55
56            if cargo_msrv_subcmd != "msrv" {
57                args.insert(1, OsStr::new("msrv").to_os_string());
58            }
59        }
60    }
61
62    args
63}
64
65#[derive(Debug, Subcommand)]
66pub enum CargoMsrvCli {
67    /// Find your Minimum Supported Rust Version!
68    #[command(
69        author = "Martijn Gribnau <garm@ilumeo.com>",
70        after_help = indoc::indoc!{"
71            You can provide a custom compatibility check command as the last positional argument via
72            the -- syntax, e.g. `$ cargo msrv find -- my custom command`.
73
74            This custom check command will then be used to validate whether a Rust version is
75            compatible.
76
77            A custom `check` command should be runnable by rustup, as it will be passed to
78            rustup like so: `rustup run <toolchain> <COMMAND...>`.
79            NB: You only need to provide the <COMMAND...> part.
80
81            By default, the compatibility check command is `cargo check`.
82        "}
83    )]
84    Msrv(CargoMsrvOpts),
85}
86
87impl CargoMsrvCli {
88    pub fn to_opts(self) -> CargoMsrvOpts {
89        match self {
90            Self::Msrv(opts) => opts,
91        }
92    }
93}
94
95#[derive(Debug, Args)]
96#[command(version)]
97pub struct CargoMsrvOpts {
98    #[command(flatten)]
99    pub shared_opts: SharedOpts,
100
101    #[command(subcommand)]
102    pub subcommand: SubCommand,
103}
104
105#[derive(Debug, Subcommand)]
106#[command(propagate_version = true)]
107pub enum SubCommand {
108    /// Find the MSRV
109    Find(FindOpts),
110    /// Display the MSRV's of dependencies
111    List(ListOpts),
112    /// Set the MSRV of the current crate to a given Rust version
113    Set(SetOpts),
114    /// Show the MSRV of your crate, as specified in the Cargo manifest
115    Show,
116    /// Verify whether the MSRV is satisfiable.
117    ///
118    ///  The MSRV must be specified via the `--rust-version` option, or via the 'package.rust-version' or 'package.metadata.msrv' keys in the Cargo.toml manifest.
119    Verify(VerifyOpts),
120}
121
122// Cli Options for top-level cargo-msrv (find) command
123#[derive(Debug, Args)]
124#[command(next_help_heading = "Find MSRV options")]
125pub struct FindOpts {
126    /// Use a binary search to find the MSRV (default)
127    ///
128    /// When the search space is sufficiently large, which is common, this is much
129    /// faster than a linear search. A binary search will approximately halve the search
130    /// space for each Rust version checked for compatibility.
131    #[arg(long, conflicts_with = "linear")]
132    pub bisect: bool,
133
134    /// Use a linear search to find the MSRV
135    ///
136    /// This method checks toolchain from the most recent release to the earliest.
137    #[arg(long, conflicts_with = "bisect")]
138    pub linear: bool,
139
140    /// Pin the MSRV by writing the version to a rust-toolchain file
141    ///
142    /// The [toolchain](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file will pin the Rust version for this crate.
143    #[arg(long, alias = "toolchain-file")]
144    pub write_toolchain_file: bool,
145
146    /// Temporarily remove the lockfile, so it will not interfere with the building process
147    ///
148    /// This is important when testing against older Rust versions such as Cargo versions prior to
149    /// Rust 1.38.0, for which Cargo does not recognize the newer lockfile formats.
150    #[arg(long)]
151    pub ignore_lockfile: bool,
152
153    /// Treats a Rust version as incompatible when a toolchain failed to install or was otherwise unavailable
154    ///
155    /// Can be useful for reducing the search space on platforms with limited toolchain availability.
156    /// Be warned that network errors on either end can also mark versions incorrectly as incompatible.
157    #[arg(long)]
158    pub skip_unavailable_toolchains: bool,
159
160    /// Don't print the result of compatibility checks
161    ///
162    /// The feedback of a compatibility check can be useful to determine why a certain Rust
163    /// version is not compatible. Rust usually prints very detailed error messages.
164    /// While most often very useful, in some cases they may be too noisy or lengthy.
165    /// If this flag is given, the result messages will not be printed.
166    #[arg(long)]
167    pub no_check_feedback: bool,
168
169    /// Write the MSRV to the Cargo manifest
170    ///
171    /// For toolchains which include a Cargo version which supports the rust-version field,
172    /// the `package.rust-version` field will be written. For older Rust toolchains,
173    /// the `package.metadata.msrv` field will be written instead.
174    #[arg(long, visible_alias = "set")]
175    pub write_msrv: bool,
176
177    #[command(flatten)]
178    pub rust_releases_opts: RustReleasesOpts,
179
180    #[command(flatten)]
181    pub toolchain_opts: ToolchainOpts,
182
183    #[command(flatten)]
184    pub custom_check_opts: CustomCheckOpts,
185}
186
187#[derive(Debug, Args)]
188#[command(next_help_heading = "List options")]
189pub struct ListOpts {
190    /// Display the MSRV's of crates that your crate depends on
191    #[arg(long, value_enum, default_value_t)]
192    pub variant: ListMsrvVariant,
193}
194
195#[derive(Debug, Args)]
196#[command(next_help_heading = "Set options")]
197pub struct SetOpts {
198    /// The version to be set as MSRV
199    ///
200    /// The given version must be a two- or three component Rust version number.
201    /// MSRV values prior to Rust 1.56 will be written to the `package.metadata.msrv` field
202    /// in the Cargo manifest. MSRV's greater or equal to 1.56 will be written to
203    /// `package.rust-version` in the Cargo manifest.
204    #[arg(value_name = "MSRV")]
205    pub msrv: BareVersion,
206
207    #[command(flatten)]
208    pub rust_releases_opts: RustReleasesOpts,
209}
210
211#[derive(Debug, Args)]
212#[command(next_help_heading = "Verify options")]
213pub struct VerifyOpts {
214    /// Ignore the lockfile for the MSRV search
215    #[arg(long)]
216    pub ignore_lockfile: bool,
217
218    /// Don't print the result of compatibility checks
219    ///
220    /// The feedback of a compatibility check can be useful to determine why a certain Rust
221    /// version is not compatible. Rust usually prints very detailed error messages.
222    /// While most often very useful, in some cases they may be too noisy or lengthy.
223    /// If this flag is given, the result messages will not be printed.
224    #[arg(long)]
225    pub no_check_feedback: bool,
226
227    #[command(flatten)]
228    pub rust_releases_opts: RustReleasesOpts,
229
230    /// The Rust version, to check against for toolchain compatibility
231    ///
232    /// If not set, the MSRV will be parsed from the Cargo manifest instead.
233    #[arg(long, value_name = "rust-version")]
234    pub rust_version: Option<BareVersion>,
235
236    #[command(flatten)]
237    pub toolchain_opts: ToolchainOpts,
238
239    #[command(flatten)]
240    pub custom_check_opts: CustomCheckOpts,
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn verify_cli() {
249        use clap::CommandFactory;
250        CargoCli::command().debug_assert();
251    }
252
253    mod top_level {
254        use super::*;
255
256        fn assert_find_opts(opts: CargoMsrvOpts, assertions: impl Fn(FindOpts)) {
257            if let SubCommand::Find(find_opts) = opts.subcommand {
258                assertions(find_opts);
259                return;
260            }
261
262            panic!("Assertion failed: expected subcommand 'cargo msrv find'");
263        }
264
265        mod find_opts {
266            use super::*;
267
268            #[test]
269            fn has_bisect() {
270                let cargo = CargoCli::parse_args(["cargo", "msrv", "find", "--bisect"]);
271                let cargo_msrv = cargo.to_cargo_msrv_cli();
272                let opts = cargo_msrv.to_opts();
273
274                assert_find_opts(opts, |find_opts| {
275                    assert!(find_opts.bisect);
276                    assert!(!find_opts.linear);
277                });
278            }
279
280            #[test]
281            fn has_not_bisect() {
282                let cargo = CargoCli::parse_args(["cargo", "msrv", "find"]);
283                let cargo_msrv = cargo.to_cargo_msrv_cli();
284                let opts = cargo_msrv.to_opts();
285
286                assert_find_opts(opts, |find_opts| {
287                    assert!(!find_opts.bisect);
288                });
289            }
290
291            #[test]
292            fn has_linear() {
293                let cargo = CargoCli::parse_args(["cargo", "msrv", "find", "--linear"]);
294                let cargo_msrv = cargo.to_cargo_msrv_cli();
295                let opts = cargo_msrv.to_opts();
296
297                assert_find_opts(opts, |find_opts| {
298                    assert!(find_opts.linear);
299                    assert!(!find_opts.bisect);
300                });
301            }
302
303            #[test]
304            fn has_not_linear() {
305                let cargo = CargoCli::parse_args(["cargo", "msrv", "find"]);
306                let cargo_msrv = cargo.to_cargo_msrv_cli();
307                let opts = cargo_msrv.to_opts();
308
309                assert_find_opts(opts, |find_opts| {
310                    assert!(!find_opts.linear);
311                });
312            }
313
314            #[test]
315            fn has_write_toolchain_file() {
316                let cargo =
317                    CargoCli::parse_args(["cargo", "msrv", "find", "--write-toolchain-file"]);
318                let cargo_msrv = cargo.to_cargo_msrv_cli();
319                let opts = cargo_msrv.to_opts();
320
321                assert_find_opts(opts, |find_opts| {
322                    assert!(find_opts.write_toolchain_file);
323                });
324            }
325
326            #[test]
327            fn has_not_write_toolchain_file() {
328                let cargo = CargoCli::parse_args(["cargo", "msrv", "find"]);
329                let cargo_msrv = cargo.to_cargo_msrv_cli();
330                let opts = cargo_msrv.to_opts();
331
332                assert_find_opts(opts, |find_opts| {
333                    assert!(!find_opts.write_toolchain_file);
334                });
335            }
336
337            #[test]
338            fn has_ignore_lockfile() {
339                let cargo = CargoCli::parse_args(["cargo", "msrv", "find", "--ignore-lockfile"]);
340                let cargo_msrv = cargo.to_cargo_msrv_cli();
341                let opts = cargo_msrv.to_opts();
342
343                assert_find_opts(opts, |find_opts| {
344                    assert!(find_opts.ignore_lockfile);
345                });
346            }
347
348            #[test]
349            fn has_not_ignore_lockfile() {
350                let cargo = CargoCli::parse_args(["cargo", "msrv", "find"]);
351                let cargo_msrv = cargo.to_cargo_msrv_cli();
352                let opts = cargo_msrv.to_opts();
353
354                assert_find_opts(opts, |find_opts| {
355                    assert!(!find_opts.ignore_lockfile);
356                });
357            }
358
359            #[test]
360            fn has_no_check_feedback() {
361                let cargo = CargoCli::parse_args(["cargo", "msrv", "find", "--no-check-feedback"]);
362                let cargo_msrv = cargo.to_cargo_msrv_cli();
363                let opts = cargo_msrv.to_opts();
364
365                assert_find_opts(opts, |find_opts| {
366                    assert!(find_opts.no_check_feedback);
367                });
368            }
369
370            #[test]
371            fn has_not_no_check_feedback() {
372                let cargo = CargoCli::parse_args(["cargo", "msrv", "find"]);
373                let cargo_msrv = cargo.to_cargo_msrv_cli();
374                let opts = cargo_msrv.to_opts();
375
376                assert_find_opts(opts, |find_opts| {
377                    assert!(!find_opts.no_check_feedback);
378                });
379            }
380
381            #[test]
382            fn has_write_msrv() {
383                let cargo = CargoCli::parse_args(["cargo", "msrv", "find", "--write-msrv"]);
384                let cargo_msrv = cargo.to_cargo_msrv_cli();
385                let opts = cargo_msrv.to_opts();
386
387                assert_find_opts(opts, |find_opts| {
388                    assert!(find_opts.write_msrv);
389                });
390            }
391
392            #[test]
393            fn has_not_write_msrv() {
394                let cargo = CargoCli::parse_args(["cargo", "msrv", "find"]);
395                let cargo_msrv = cargo.to_cargo_msrv_cli();
396                let opts = cargo_msrv.to_opts();
397
398                assert_find_opts(opts, |find_opts| {
399                    assert!(!find_opts.write_msrv);
400                });
401            }
402
403            // todo: rust-releases opts
404
405            // todo: toolchain opts
406
407            // todo: custom check opts
408        }
409    }
410}