Skip to main content

cargo/util/
command_prelude.rs

1use crate::core::compiler::{BuildConfig, MessageFormat};
2use crate::core::InternedString;
3use crate::core::Workspace;
4use crate::ops::{CompileFilter, CompileOptions, NewOptions, Packages, VersionControl};
5use crate::sources::CRATES_IO_REGISTRY;
6use crate::util::important_paths::find_root_manifest_for_wd;
7use crate::util::{paths, toml::TomlProfile, validate_package_name};
8use crate::util::{
9    print_available_benches, print_available_binaries, print_available_examples,
10    print_available_tests,
11};
12use crate::CargoResult;
13use anyhow::bail;
14use clap::{self, SubCommand};
15use std::ffi::{OsStr, OsString};
16use std::fs;
17use std::path::PathBuf;
18
19pub use crate::core::compiler::CompileMode;
20pub use crate::{CliError, CliResult, Config};
21pub use clap::{AppSettings, Arg, ArgMatches};
22
23pub type App = clap::App<'static, 'static>;
24
25pub trait AppExt: Sized {
26    fn _arg(self, arg: Arg<'static, 'static>) -> Self;
27
28    fn arg_package_spec(
29        self,
30        package: &'static str,
31        all: &'static str,
32        exclude: &'static str,
33    ) -> Self {
34        self.arg_package_spec_simple(package)
35            ._arg(opt("all", "Alias for --workspace (deprecated)"))
36            ._arg(opt("workspace", all))
37            ._arg(multi_opt("exclude", "SPEC", exclude))
38    }
39
40    fn arg_package_spec_simple(self, package: &'static str) -> Self {
41        self._arg(multi_opt("package", "SPEC", package).short("p"))
42    }
43
44    fn arg_package(self, package: &'static str) -> Self {
45        self._arg(opt("package", package).short("p").value_name("SPEC"))
46    }
47
48    fn arg_jobs(self) -> Self {
49        self._arg(
50            opt("jobs", "Number of parallel jobs, defaults to # of CPUs")
51                .short("j")
52                .value_name("N"),
53        )
54    }
55
56    fn arg_targets_all(
57        self,
58        lib: &'static str,
59        bin: &'static str,
60        bins: &'static str,
61        example: &'static str,
62        examples: &'static str,
63        test: &'static str,
64        tests: &'static str,
65        bench: &'static str,
66        benches: &'static str,
67        all: &'static str,
68    ) -> Self {
69        self.arg_targets_lib_bin(lib, bin, bins)
70            ._arg(optional_multi_opt("example", "NAME", example))
71            ._arg(opt("examples", examples))
72            ._arg(optional_multi_opt("test", "NAME", test))
73            ._arg(opt("tests", tests))
74            ._arg(optional_multi_opt("bench", "NAME", bench))
75            ._arg(opt("benches", benches))
76            ._arg(opt("all-targets", all))
77    }
78
79    fn arg_targets_lib_bin(self, lib: &'static str, bin: &'static str, bins: &'static str) -> Self {
80        self._arg(opt("lib", lib))
81            ._arg(optional_multi_opt("bin", "NAME", bin))
82            ._arg(opt("bins", bins))
83    }
84
85    fn arg_targets_bins_examples(
86        self,
87        bin: &'static str,
88        bins: &'static str,
89        example: &'static str,
90        examples: &'static str,
91    ) -> Self {
92        self._arg(optional_multi_opt("bin", "NAME", bin))
93            ._arg(opt("bins", bins))
94            ._arg(optional_multi_opt("example", "NAME", example))
95            ._arg(opt("examples", examples))
96    }
97
98    fn arg_targets_bin_example(self, bin: &'static str, example: &'static str) -> Self {
99        self._arg(optional_multi_opt("bin", "NAME", bin))
100            ._arg(optional_multi_opt("example", "NAME", example))
101    }
102
103    fn arg_features(self) -> Self {
104        self._arg(multi_opt(
105            "features",
106            "FEATURES",
107            "Space-separated list of features to activate",
108        ))
109        ._arg(opt("all-features", "Activate all available features"))
110        ._arg(opt(
111            "no-default-features",
112            "Do not activate the `default` feature",
113        ))
114    }
115
116    fn arg_release(self, release: &'static str) -> Self {
117        self._arg(opt("release", release))
118    }
119
120    fn arg_profile(self, profile: &'static str) -> Self {
121        self._arg(opt("profile", profile).value_name("PROFILE-NAME"))
122    }
123
124    fn arg_doc(self, doc: &'static str) -> Self {
125        self._arg(opt("doc", doc))
126    }
127
128    fn arg_target_triple(self, target: &'static str) -> Self {
129        self._arg(opt("target", target).value_name("TRIPLE"))
130    }
131
132    fn arg_target_dir(self) -> Self {
133        self._arg(
134            opt("target-dir", "Directory for all generated artifacts").value_name("DIRECTORY"),
135        )
136    }
137
138    fn arg_manifest_path(self) -> Self {
139        self._arg(opt("manifest-path", "Path to Cargo.toml").value_name("PATH"))
140    }
141
142    fn arg_message_format(self) -> Self {
143        self._arg(multi_opt("message-format", "FMT", "Error format"))
144    }
145
146    fn arg_build_plan(self) -> Self {
147        self._arg(opt(
148            "build-plan",
149            "Output the build plan in JSON (unstable)",
150        ))
151    }
152
153    fn arg_unit_graph(self) -> Self {
154        self._arg(opt("unit-graph", "Output build graph in JSON (unstable)").hidden(true))
155    }
156
157    fn arg_deps(self) -> Self {
158        self._arg(opt(
159            "dependencies",
160            "Build dependencies of the selected packages (unstable)",
161        ).conflicts_with("remote-dependencies"))
162    }
163
164    fn arg_deps_remote(self) -> Self {
165        self._arg(opt(
166            "remote-dependencies",
167            "Build remote dependencies of the selected packages (unstable)",
168        ).conflicts_with("dependencies"))
169    }
170
171    fn arg_new_opts(self) -> Self {
172        self._arg(
173            opt(
174                "vcs",
175                "Initialize a new repository for the given version \
176                 control system (git, hg, pijul, or fossil) or do not \
177                 initialize any version control at all (none), overriding \
178                 a global configuration.",
179            )
180            .value_name("VCS")
181            .possible_values(&["git", "hg", "pijul", "fossil", "none"]),
182        )
183        ._arg(opt("bin", "Use a binary (application) template [default]"))
184        ._arg(opt("lib", "Use a library template"))
185        ._arg(
186            opt("edition", "Edition to set for the crate generated")
187                .possible_values(&["2015", "2018"])
188                .value_name("YEAR"),
189        )
190        ._arg(
191            opt(
192                "name",
193                "Set the resulting package name, defaults to the directory name",
194            )
195            .value_name("NAME"),
196        )
197    }
198
199    fn arg_index(self) -> Self {
200        self._arg(opt("index", "Registry index URL to upload the package to").value_name("INDEX"))
201            ._arg(
202                opt("host", "DEPRECATED, renamed to '--index'")
203                    .value_name("HOST")
204                    .hidden(true),
205            )
206    }
207
208    fn arg_dry_run(self, dry_run: &'static str) -> Self {
209        self._arg(opt("dry-run", dry_run))
210    }
211}
212
213impl AppExt for App {
214    fn _arg(self, arg: Arg<'static, 'static>) -> Self {
215        self.arg(arg)
216    }
217}
218
219pub fn opt(name: &'static str, help: &'static str) -> Arg<'static, 'static> {
220    Arg::with_name(name).long(name).help(help)
221}
222
223pub fn optional_multi_opt(
224    name: &'static str,
225    value_name: &'static str,
226    help: &'static str,
227) -> Arg<'static, 'static> {
228    opt(name, help)
229        .value_name(value_name)
230        .multiple(true)
231        .min_values(0)
232        .number_of_values(1)
233}
234
235pub fn multi_opt(
236    name: &'static str,
237    value_name: &'static str,
238    help: &'static str,
239) -> Arg<'static, 'static> {
240    // Note that all `.multiple(true)` arguments in Cargo should specify
241    // `.number_of_values(1)` as well, so that `--foo val1 val2` is
242    // *not* parsed as `foo` with values ["val1", "val2"].
243    // `number_of_values` should become the default in clap 3.
244    opt(name, help)
245        .value_name(value_name)
246        .multiple(true)
247        .number_of_values(1)
248}
249
250pub fn subcommand(name: &'static str) -> App {
251    SubCommand::with_name(name).settings(&[
252        AppSettings::UnifiedHelpMessage,
253        AppSettings::DeriveDisplayOrder,
254        AppSettings::DontCollapseArgsInUsage,
255    ])
256}
257
258// Determines whether or not to gate `--profile` as unstable when resolving it.
259pub enum ProfileChecking {
260    Checked,
261    Unchecked,
262}
263
264pub trait ArgMatchesExt {
265    fn value_of_u32(&self, name: &str) -> CargoResult<Option<u32>> {
266        let arg = match self._value_of(name) {
267            None => None,
268            Some(arg) => Some(arg.parse::<u32>().map_err(|_| {
269                clap::Error::value_validation_auto(format!("could not parse `{}` as a number", arg))
270            })?),
271        };
272        Ok(arg)
273    }
274
275    /// Returns value of the `name` command-line argument as an absolute path
276    fn value_of_path(&self, name: &str, config: &Config) -> Option<PathBuf> {
277        self._value_of(name).map(|path| config.cwd().join(path))
278    }
279
280    fn root_manifest(&self, config: &Config) -> CargoResult<PathBuf> {
281        if let Some(path) = self.value_of_path("manifest-path", config) {
282            // In general, we try to avoid normalizing paths in Cargo,
283            // but in this particular case we need it to fix #3586.
284            let path = paths::normalize_path(&path);
285            if !path.ends_with("Cargo.toml") {
286                anyhow::bail!("the manifest-path must be a path to a Cargo.toml file")
287            }
288            if fs::metadata(&path).is_err() {
289                anyhow::bail!(
290                    "manifest path `{}` does not exist",
291                    self._value_of("manifest-path").unwrap()
292                )
293            }
294            return Ok(path);
295        }
296        find_root_manifest_for_wd(config.cwd())
297    }
298
299    fn workspace<'a>(&self, config: &'a Config) -> CargoResult<Workspace<'a>> {
300        let root = self.root_manifest(config)?;
301        let mut ws = Workspace::new(&root, config)?;
302        if config.cli_unstable().avoid_dev_deps {
303            ws.set_require_optional_deps(false);
304        }
305        if ws.is_virtual() && !config.cli_unstable().package_features {
306            // --all-features is actually honored. In general, workspaces and
307            // feature flags are a bit of a mess right now.
308            for flag in &["features", "no-default-features"] {
309                if self._is_present(flag) {
310                    bail!(
311                        "--{} is not allowed in the root of a virtual workspace\n\
312                         note: while this was previously accepted, it didn't actually do anything",
313                        flag
314                    );
315                }
316            }
317        }
318        Ok(ws)
319    }
320
321    fn jobs(&self) -> CargoResult<Option<u32>> {
322        self.value_of_u32("jobs")
323    }
324
325    fn target(&self) -> Option<String> {
326        self._value_of("target").map(|s| s.to_string())
327    }
328
329    fn get_profile_name(
330        &self,
331        config: &Config,
332        default: &str,
333        profile_checking: ProfileChecking,
334    ) -> CargoResult<InternedString> {
335        let specified_profile = match self._value_of("profile") {
336            None => None,
337            Some(name) => {
338                TomlProfile::validate_name(name, "profile name")?;
339                Some(InternedString::new(name))
340            }
341        };
342
343        match profile_checking {
344            ProfileChecking::Unchecked => {}
345            ProfileChecking::Checked => {
346                if specified_profile.is_some() && !config.cli_unstable().unstable_options {
347                    anyhow::bail!("Usage of `--profile` requires `-Z unstable-options`")
348                }
349            }
350        }
351
352        if self._is_present("release") {
353            if !config.cli_unstable().unstable_options {
354                Ok(InternedString::new("release"))
355            } else {
356                match specified_profile {
357                    Some(name) if name != "release" => {
358                        anyhow::bail!("Conflicting usage of --profile and --release")
359                    }
360                    _ => Ok(InternedString::new("release")),
361                }
362            }
363        } else if self._is_present("debug") {
364            if !config.cli_unstable().unstable_options {
365                Ok(InternedString::new("dev"))
366            } else {
367                match specified_profile {
368                    Some(name) if name != "dev" => {
369                        anyhow::bail!("Conflicting usage of --profile and --debug")
370                    }
371                    _ => Ok(InternedString::new("dev")),
372                }
373            }
374        } else {
375            Ok(specified_profile.unwrap_or_else(|| InternedString::new(default)))
376        }
377    }
378
379    fn compile_options(
380        &self,
381        config: &Config,
382        mode: CompileMode,
383        workspace: Option<&Workspace<'_>>,
384        profile_checking: ProfileChecking,
385    ) -> CargoResult<CompileOptions> {
386        let spec = Packages::from_flags(
387            // TODO Integrate into 'workspace'
388            self._is_present("workspace") || self._is_present("all"),
389            self._values_of("exclude"),
390            self._values_of("package"),
391        )?;
392
393        let mut message_format = None;
394        let default_json = MessageFormat::Json {
395            short: false,
396            ansi: false,
397            render_diagnostics: false,
398        };
399        for fmt in self._values_of("message-format") {
400            for fmt in fmt.split(',') {
401                let fmt = fmt.to_ascii_lowercase();
402                match fmt.as_str() {
403                    "json" => {
404                        if message_format.is_some() {
405                            bail!("cannot specify two kinds of `message-format` arguments");
406                        }
407                        message_format = Some(default_json);
408                    }
409                    "human" => {
410                        if message_format.is_some() {
411                            bail!("cannot specify two kinds of `message-format` arguments");
412                        }
413                        message_format = Some(MessageFormat::Human);
414                    }
415                    "short" => {
416                        if message_format.is_some() {
417                            bail!("cannot specify two kinds of `message-format` arguments");
418                        }
419                        message_format = Some(MessageFormat::Short);
420                    }
421                    "json-render-diagnostics" => {
422                        if message_format.is_none() {
423                            message_format = Some(default_json);
424                        }
425                        match &mut message_format {
426                            Some(MessageFormat::Json {
427                                render_diagnostics, ..
428                            }) => *render_diagnostics = true,
429                            _ => bail!("cannot specify two kinds of `message-format` arguments"),
430                        }
431                    }
432                    "json-diagnostic-short" => {
433                        if message_format.is_none() {
434                            message_format = Some(default_json);
435                        }
436                        match &mut message_format {
437                            Some(MessageFormat::Json { short, .. }) => *short = true,
438                            _ => bail!("cannot specify two kinds of `message-format` arguments"),
439                        }
440                    }
441                    "json-diagnostic-rendered-ansi" => {
442                        if message_format.is_none() {
443                            message_format = Some(default_json);
444                        }
445                        match &mut message_format {
446                            Some(MessageFormat::Json { ansi, .. }) => *ansi = true,
447                            _ => bail!("cannot specify two kinds of `message-format` arguments"),
448                        }
449                    }
450                    s => bail!("invalid message format specifier: `{}`", s),
451                }
452            }
453        }
454
455        let mut build_config = BuildConfig::new(config, self.jobs()?, &self.target(), mode)?;
456        build_config.message_format = message_format.unwrap_or(MessageFormat::Human);
457        build_config.requested_profile = self.get_profile_name(config, "dev", profile_checking)?;
458        build_config.build_plan = self._is_present("build-plan");
459        build_config.unit_graph = self._is_present("unit-graph");
460        if build_config.build_plan {
461            config
462                .cli_unstable()
463                .fail_if_stable_opt("--build-plan", 5579)?;
464        };
465        if build_config.unit_graph {
466            config
467                .cli_unstable()
468                .fail_if_stable_opt("--unit-graph", 8002)?;
469        }
470
471        let opts = CompileOptions {
472            build_config,
473            features: self._values_of("features"),
474            all_features: self._is_present("all-features"),
475            no_default_features: self._is_present("no-default-features"),
476            spec,
477            filter: CompileFilter::from_raw_arguments(
478                self._is_present("lib"),
479                self._values_of("bin"),
480                self._is_present("bins"),
481                self._values_of("test"),
482                self._is_present("tests"),
483                self._values_of("example"),
484                self._is_present("examples"),
485                self._values_of("bench"),
486                self._is_present("benches"),
487                self._is_present("all-targets"),
488            ),
489            deps_only: false,
490            deps_remote_only: false,
491            target_rustdoc_args: None,
492            target_rustc_args: None,
493            local_rustdoc_args: None,
494            rustdoc_document_private_items: false,
495            export_dir: None,
496        };
497
498        if let Some(ws) = workspace {
499            self.check_optional_opts(ws, &opts)?;
500        }
501
502        Ok(opts)
503    }
504
505    fn compile_options_for_single_package(
506        &self,
507        config: &Config,
508        mode: CompileMode,
509        workspace: Option<&Workspace<'_>>,
510        profile_checking: ProfileChecking,
511    ) -> CargoResult<CompileOptions> {
512        let mut compile_opts = self.compile_options(config, mode, workspace, profile_checking)?;
513        compile_opts.spec = Packages::Packages(self._values_of("package"));
514        Ok(compile_opts)
515    }
516
517    fn new_options(&self, config: &Config) -> CargoResult<NewOptions> {
518        let vcs = self._value_of("vcs").map(|vcs| match vcs {
519            "git" => VersionControl::Git,
520            "hg" => VersionControl::Hg,
521            "pijul" => VersionControl::Pijul,
522            "fossil" => VersionControl::Fossil,
523            "none" => VersionControl::NoVcs,
524            vcs => panic!("Impossible vcs: {:?}", vcs),
525        });
526        NewOptions::new(
527            vcs,
528            self._is_present("bin"),
529            self._is_present("lib"),
530            self.value_of_path("path", config).unwrap(),
531            self._value_of("name").map(|s| s.to_string()),
532            self._value_of("edition").map(|s| s.to_string()),
533            self.registry(config)?,
534        )
535    }
536
537    fn registry(&self, config: &Config) -> CargoResult<Option<String>> {
538        match self._value_of("registry") {
539            Some(registry) => {
540                validate_package_name(registry, "registry name", "")?;
541
542                if registry == CRATES_IO_REGISTRY {
543                    // If "crates.io" is specified, then we just need to return `None`,
544                    // as that will cause cargo to use crates.io. This is required
545                    // for the case where a default alternative registry is used
546                    // but the user wants to switch back to crates.io for a single
547                    // command.
548                    Ok(None)
549                } else {
550                    Ok(Some(registry.to_string()))
551                }
552            }
553            None => config.default_registry(),
554        }
555    }
556
557    fn index(&self, config: &Config) -> CargoResult<Option<String>> {
558        // TODO: deprecated. Remove once it has been decided `--host` can be removed
559        // We may instead want to repurpose the host flag, as mentioned in issue
560        // rust-lang/cargo#4208.
561        let msg = "The flag '--host' is no longer valid.
562
563Previous versions of Cargo accepted this flag, but it is being
564deprecated. The flag is being renamed to 'index', as the flag
565wants the location of the index. Please use '--index' instead.
566
567This will soon become a hard error, so it's either recommended
568to update to a fixed version or contact the upstream maintainer
569about this warning.";
570
571        let index = match self._value_of("host") {
572            Some(host) => {
573                config.shell().warn(&msg)?;
574                Some(host.to_string())
575            }
576            None => self._value_of("index").map(|s| s.to_string()),
577        };
578        Ok(index)
579    }
580
581    fn check_optional_opts(
582        &self,
583        workspace: &Workspace<'_>,
584        compile_opts: &CompileOptions,
585    ) -> CargoResult<()> {
586        if self.is_present_with_zero_values("example") {
587            print_available_examples(workspace, compile_opts)?;
588        }
589
590        if self.is_present_with_zero_values("bin") {
591            print_available_binaries(workspace, compile_opts)?;
592        }
593
594        if self.is_present_with_zero_values("bench") {
595            print_available_benches(workspace, compile_opts)?;
596        }
597
598        if self.is_present_with_zero_values("test") {
599            print_available_tests(workspace, compile_opts)?;
600        }
601
602        Ok(())
603    }
604
605    fn is_present_with_zero_values(&self, name: &str) -> bool {
606        self._is_present(name) && self._value_of(name).is_none()
607    }
608
609    fn _value_of(&self, name: &str) -> Option<&str>;
610
611    fn _values_of(&self, name: &str) -> Vec<String>;
612
613    fn _value_of_os(&self, name: &str) -> Option<&OsStr>;
614
615    fn _values_of_os(&self, name: &str) -> Vec<OsString>;
616
617    fn _is_present(&self, name: &str) -> bool;
618}
619
620impl<'a> ArgMatchesExt for ArgMatches<'a> {
621    fn _value_of(&self, name: &str) -> Option<&str> {
622        self.value_of(name)
623    }
624
625    fn _value_of_os(&self, name: &str) -> Option<&OsStr> {
626        self.value_of_os(name)
627    }
628
629    fn _values_of(&self, name: &str) -> Vec<String> {
630        self.values_of(name)
631            .unwrap_or_default()
632            .map(|s| s.to_string())
633            .collect()
634    }
635
636    fn _values_of_os(&self, name: &str) -> Vec<OsString> {
637        self.values_of_os(name)
638            .unwrap_or_default()
639            .map(|s| s.to_os_string())
640            .collect()
641    }
642
643    fn _is_present(&self, name: &str) -> bool {
644        self.is_present(name)
645    }
646}
647
648pub fn values(args: &ArgMatches<'_>, name: &str) -> Vec<String> {
649    args._values_of(name)
650}
651
652pub fn values_os(args: &ArgMatches<'_>, name: &str) -> Vec<OsString> {
653    args._values_of_os(name)
654}
655
656#[derive(PartialEq, PartialOrd, Eq, Ord)]
657pub enum CommandInfo {
658    BuiltIn { name: String, about: Option<String> },
659    External { name: String, path: PathBuf },
660}
661
662impl CommandInfo {
663    pub fn name(&self) -> &str {
664        match self {
665            CommandInfo::BuiltIn { name, .. } => name,
666            CommandInfo::External { name, .. } => name,
667        }
668    }
669}