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