1#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
8use std::fs;
9#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
10use std::path::Path;
11use std::path::PathBuf;
12use std::sync::OnceLock;
13
14use indexmap::{IndexMap, IndexSet};
15
16#[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
17use crate::config::file::{detect_config_format, ConfigFileFormat};
18use crate::error::{Error, Result};
19
20use super::{
21 CommandForm, CommandFormOverride, CommandSpec, CommandSpecOverride, KwargSpec,
22 KwargSpecOverride, LayoutOverrides, LayoutOverridesOverride, SpecFile, SpecMetadata,
23 SpecOverrideFile,
24};
25
26const BUILTINS_PATH: &str = "src/spec/builtins.yaml";
27const BUILTINS_MSGPACK: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/builtins.msgpack"));
31
32#[derive(Debug, Clone)]
53pub struct CommandRegistry {
54 metadata: SpecMetadata,
55 builtin_commands: IndexSet<String>,
56 commands: IndexMap<String, CommandSpec>,
57 fallback: CommandSpec,
58}
59
60impl CommandRegistry {
61 pub fn load() -> Result<Self> {
67 Self::load_builtins_impl()
68 }
69
70 pub fn builtins() -> &'static Self {
76 static BUILTINS: OnceLock<CommandRegistry> = OnceLock::new();
77 BUILTINS.get_or_init(|| {
78 Self::load_builtins_impl()
79 .expect("embedded built-in command registry should deserialize")
80 })
81 }
82
83 #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
84 fn load_builtins_impl() -> Result<Self> {
85 Self::from_builtins_and_overrides(None::<&Path>)
86 }
87
88 #[cfg(any(target_arch = "wasm32", not(feature = "cli")))]
89 fn load_builtins_impl() -> Result<Self> {
90 Ok(Self::from_spec_file(parse_builtins()?))
91 }
92
93 #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
95 pub fn from_builtins_and_overrides(path: Option<impl AsRef<Path>>) -> Result<Self> {
96 let mut registry = Self::from_spec_file(parse_builtins()?);
97
98 if let Some(path) = path {
99 registry.merge_override_file(path.as_ref())?;
100 }
101
102 Ok(registry)
103 }
104
105 pub(crate) fn from_spec_file(mut spec_file: SpecFile) -> Self {
107 normalize_spec_file(&mut spec_file);
108 let builtin_commands = spec_file.commands.keys().cloned().collect();
109 Self {
110 metadata: spec_file.metadata,
111 builtin_commands,
112 commands: spec_file.commands,
113 fallback: CommandSpec::Single(CommandForm::default()),
114 }
115 }
116
117 pub fn merge_toml_overrides(&mut self, toml_source: &str) -> Result<()> {
146 let mut overrides: SpecOverrideFile = toml::from_str(toml_source)
147 .map_err(|e| Error::Formatter(format!("spec TOML error: {e}")))?;
148 self.apply_overrides(&mut overrides);
149 Ok(())
150 }
151
152 pub fn merge_yaml_overrides(&mut self, yaml_source: &str) -> Result<()> {
180 let mut overrides: SpecOverrideFile = serde_yaml::from_str(yaml_source)
181 .map_err(|e| Error::Formatter(format!("spec YAML error: {e}")))?;
182 self.apply_overrides(&mut overrides);
183 Ok(())
184 }
185
186 fn apply_overrides(&mut self, overrides: &mut SpecOverrideFile) {
187 normalize_override_file(overrides);
188 let commands = std::mem::take(&mut overrides.commands);
189 for (name, override_spec) in commands {
190 match self.commands.get_mut(&name) {
191 Some(existing) => merge_command_spec(existing, override_spec),
192 None => {
193 self.commands.insert(name, override_spec.into_full_spec());
194 }
195 }
196 }
197 }
198
199 #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
209 #[cfg_attr(docsrs, doc(cfg(feature = "cli")))]
210 pub fn merge_override_file(&mut self, path: &Path) -> Result<()> {
211 let source = fs::read_to_string(path)?;
212 self.merge_override_source(&source, path.to_path_buf(), detect_config_format(path)?)
213 }
214
215 #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
225 #[cfg_attr(docsrs, doc(cfg(feature = "cli")))]
226 pub fn merge_override_str(&mut self, source: &str, path: impl Into<PathBuf>) -> Result<()> {
227 self.merge_override_source(source, path.into(), ConfigFileFormat::Toml)
228 }
229
230 #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
231 fn merge_override_source(
232 &mut self,
233 source: &str,
234 path: PathBuf,
235 format: ConfigFileFormat,
236 ) -> Result<()> {
237 let mut overrides: SpecOverrideFile = match format {
238 ConfigFileFormat::Toml => toml::from_str(source).map_err(|toml_err| {
239 let (line, column) = crate::config::file::toml_line_col(
240 source,
241 toml_err.span().map(|span| span.start),
242 );
243 Error::Spec(crate::error::SpecError {
244 path: path.clone(),
245 details: crate::error::FileParseError {
246 format: format.as_str(),
247 message: toml_err.to_string().into_boxed_str(),
248 line,
249 column,
250 },
251 })
252 })?,
253 ConfigFileFormat::Yaml => serde_yaml::from_str(source).map_err(|yaml_err| {
254 let location = yaml_err.location();
255 Error::Spec(crate::error::SpecError {
256 path: path.clone(),
257 details: crate::error::FileParseError {
258 format: format.as_str(),
259 message: yaml_err.to_string().into_boxed_str(),
260 line: location.as_ref().map(|loc| loc.line()),
261 column: location.as_ref().map(|loc| loc.column()),
262 },
263 })
264 })?,
265 };
266 normalize_override_file(&mut overrides);
267
268 for (name, override_spec) in overrides.commands {
269 match self.commands.get_mut(&name) {
270 Some(existing) => merge_command_spec(existing, override_spec),
271 None => {
272 self.commands.insert(name, override_spec.into_full_spec());
273 }
274 }
275 }
276
277 Ok(())
278 }
279
280 pub fn get(&self, command_name: &str) -> &CommandSpec {
289 if let Some(spec) = self.commands.get(command_name) {
290 return spec;
291 }
292
293 if !has_ascii_uppercase(command_name) {
294 return &self.fallback;
295 }
296
297 self.commands
298 .get(&command_name.to_ascii_lowercase())
299 .unwrap_or(&self.fallback)
300 }
301
302 pub fn contains(&self, command_name: &str) -> bool {
305 self.commands.contains_key(command_name)
306 || (has_ascii_uppercase(command_name)
307 && self
308 .commands
309 .contains_key(&command_name.to_ascii_lowercase()))
310 }
311
312 pub fn contains_builtin(&self, command_name: &str) -> bool {
314 self.builtin_commands.contains(command_name)
315 || (has_ascii_uppercase(command_name)
316 && self
317 .builtin_commands
318 .contains(&command_name.to_ascii_lowercase()))
319 }
320
321 pub fn audited_cmake_version(&self) -> &str {
327 &self.metadata.cmake_version
328 }
329}
330
331fn has_ascii_uppercase(s: &str) -> bool {
332 s.bytes().any(|byte| byte.is_ascii_uppercase())
333}
334
335fn parse_builtins() -> Result<SpecFile> {
336 let mut spec: SpecFile = rmp_serde::from_slice(BUILTINS_MSGPACK).map_err(|source| {
337 Error::Spec(crate::error::SpecError {
338 path: PathBuf::from(BUILTINS_PATH),
339 details: crate::error::FileParseError {
340 format: "MessagePack",
341 message: source.to_string().into_boxed_str(),
342 line: None,
343 column: None,
344 },
345 })
346 })?;
347 normalize_spec_file(&mut spec);
348 Ok(spec)
349}
350
351fn normalize_spec_file(spec: &mut SpecFile) {
352 spec.commands = std::mem::take(&mut spec.commands)
353 .into_iter()
354 .map(|(name, mut command)| {
355 normalize_command_spec(&mut command);
356 (name.to_ascii_lowercase(), command)
357 })
358 .collect();
359}
360
361fn normalize_override_file(spec: &mut SpecOverrideFile) {
362 spec.commands = std::mem::take(&mut spec.commands)
363 .into_iter()
364 .map(|(name, mut command)| {
365 normalize_command_override(&mut command);
366 (name.to_ascii_lowercase(), command)
367 })
368 .collect();
369}
370
371fn normalize_command_spec(spec: &mut CommandSpec) {
372 match spec {
373 CommandSpec::Single(form) => normalize_form(form),
374 CommandSpec::Discriminated { forms, fallback } => {
375 *forms = std::mem::take(forms)
376 .into_iter()
377 .map(|(name, mut form)| {
378 normalize_form(&mut form);
379 (name.to_ascii_uppercase(), form)
380 })
381 .collect();
382
383 if let Some(fallback) = fallback {
384 normalize_form(fallback);
385 }
386 }
387 }
388}
389
390fn normalize_command_override(spec: &mut CommandSpecOverride) {
391 match spec {
392 CommandSpecOverride::Single(form) => normalize_form_override(form),
393 CommandSpecOverride::Discriminated { forms, fallback } => {
394 *forms = std::mem::take(forms)
395 .into_iter()
396 .map(|(name, mut form)| {
397 normalize_form_override(&mut form);
398 (name.to_ascii_uppercase(), form)
399 })
400 .collect();
401
402 if let Some(fallback) = fallback {
403 normalize_form_override(fallback);
404 }
405 }
406 }
407}
408
409fn normalize_form(form: &mut CommandForm) {
410 form.kwargs = std::mem::take(&mut form.kwargs)
411 .into_iter()
412 .map(|(name, mut kwarg)| {
413 normalize_kwarg(&mut kwarg);
414 (name.to_ascii_uppercase(), kwarg)
415 })
416 .collect();
417
418 form.flags = std::mem::take(&mut form.flags)
419 .into_iter()
420 .map(|flag| flag.to_ascii_uppercase())
421 .collect();
422}
423
424fn normalize_form_override(form: &mut CommandFormOverride) {
425 form.kwargs = std::mem::take(&mut form.kwargs)
426 .into_iter()
427 .map(|(name, mut kwarg)| {
428 normalize_kwarg_override(&mut kwarg);
429 (name.to_ascii_uppercase(), kwarg)
430 })
431 .collect();
432
433 form.flags = std::mem::take(&mut form.flags)
434 .into_iter()
435 .map(|flag| flag.to_ascii_uppercase())
436 .collect();
437}
438
439fn normalize_kwarg(spec: &mut KwargSpec) {
440 spec.kwargs = std::mem::take(&mut spec.kwargs)
441 .into_iter()
442 .map(|(name, mut kwarg)| {
443 normalize_kwarg(&mut kwarg);
444 (name.to_ascii_uppercase(), kwarg)
445 })
446 .collect();
447
448 spec.flags = std::mem::take(&mut spec.flags)
449 .into_iter()
450 .map(|flag| flag.to_ascii_uppercase())
451 .collect();
452}
453
454fn normalize_kwarg_override(spec: &mut KwargSpecOverride) {
455 spec.kwargs = std::mem::take(&mut spec.kwargs)
456 .into_iter()
457 .map(|(name, mut kwarg)| {
458 normalize_kwarg_override(&mut kwarg);
459 (name.to_ascii_uppercase(), kwarg)
460 })
461 .collect();
462
463 spec.flags = std::mem::take(&mut spec.flags)
464 .into_iter()
465 .map(|flag| flag.to_ascii_uppercase())
466 .collect();
467}
468
469fn merge_command_spec(base: &mut CommandSpec, override_spec: CommandSpecOverride) {
470 match (base, override_spec) {
471 (CommandSpec::Single(base_form), CommandSpecOverride::Single(override_form)) => {
472 merge_form(base_form, override_form);
473 }
474 (
475 CommandSpec::Discriminated {
476 forms: base_forms,
477 fallback: base_fallback,
478 },
479 CommandSpecOverride::Discriminated {
480 forms: override_forms,
481 fallback: override_fallback,
482 },
483 ) => {
484 for (name, override_form) in override_forms {
485 match base_forms.get_mut(&name) {
486 Some(base_form) => merge_form(base_form, override_form),
487 None => {
488 base_forms.insert(name, override_form.into_full_form());
489 }
490 }
491 }
492
493 if let Some(override_fallback) = override_fallback {
494 match base_fallback {
495 Some(base_fallback) => merge_form(base_fallback, override_fallback),
496 None => {
497 *base_fallback = Some(override_fallback.into_full_form());
498 }
499 }
500 }
501 }
502 (base_spec, override_spec) => {
503 *base_spec = override_spec.into_full_spec();
504 }
505 }
506}
507
508fn merge_form(base: &mut CommandForm, override_form: CommandFormOverride) {
509 if let Some(pargs) = override_form.pargs {
510 base.pargs = pargs;
511 }
512
513 merge_flags(&mut base.flags, override_form.flags);
514
515 for (name, override_kwarg) in override_form.kwargs {
516 match base.kwargs.get_mut(&name) {
517 Some(base_kwarg) => merge_kwarg(base_kwarg, override_kwarg),
518 None => {
519 base.kwargs.insert(name, override_kwarg.into_full_spec());
520 }
521 }
522 }
523
524 if let Some(layout) = override_form.layout {
525 merge_layout(
526 base.layout.get_or_insert_with(LayoutOverrides::default),
527 layout,
528 );
529 }
530}
531
532fn merge_kwarg(base: &mut KwargSpec, override_kwarg: KwargSpecOverride) {
533 if let Some(nargs) = override_kwarg.nargs {
534 base.nargs = nargs;
535 }
536
537 merge_flags(&mut base.flags, override_kwarg.flags);
538
539 for (name, nested_override) in override_kwarg.kwargs {
540 match base.kwargs.get_mut(&name) {
541 Some(base_nested) => merge_kwarg(base_nested, nested_override),
542 None => {
543 base.kwargs.insert(name, nested_override.into_full_spec());
544 }
545 }
546 }
547}
548
549fn merge_layout(base: &mut LayoutOverrides, override_layout: LayoutOverridesOverride) {
550 if let Some(value) = override_layout.line_width {
551 base.line_width = Some(value);
552 }
553 if let Some(value) = override_layout.tab_size {
554 base.tab_size = Some(value);
555 }
556 if let Some(value) = override_layout.dangle_parens {
557 base.dangle_parens = Some(value);
558 }
559 if let Some(value) = override_layout.always_wrap {
560 base.always_wrap = Some(value);
561 }
562 if let Some(value) = override_layout.max_pargs_hwrap {
563 base.max_pargs_hwrap = Some(value);
564 }
565 if let Some(value) = override_layout.continuation_align {
566 base.continuation_align = Some(value);
567 }
568}
569
570fn merge_flags(base: &mut IndexSet<String>, override_flags: IndexSet<String>) {
571 for flag in override_flags {
572 base.insert(flag);
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579 use crate::spec::NArgs;
580 use std::fs;
581
582 #[test]
583 fn registry_has_target_link_libraries_keywords() {
584 let registry = CommandRegistry::load().unwrap();
585 let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
586 panic!()
587 };
588 assert!(form.kwargs.contains_key("PUBLIC"));
589 assert!(form.kwargs.contains_key("PRIVATE"));
590 assert!(form.kwargs.contains_key("INTERFACE"));
591 }
592
593 #[test]
594 fn registry_has_install_forms() {
595 let registry = CommandRegistry::load().unwrap();
596 assert!(matches!(
597 registry.get("install"),
598 CommandSpec::Discriminated { .. }
599 ));
600 }
601
602 #[test]
603 fn registry_unknown_command_uses_fallback() {
604 let registry = CommandRegistry::load().unwrap();
605 let spec = registry.get("my_unknown_command");
606 let CommandSpec::Single(form) = spec else {
607 panic!()
608 };
609 assert_eq!(form.pargs, NArgs::ZeroOrMore);
610 assert!(form.kwargs.is_empty());
611 assert!(form.flags.is_empty());
612 }
613
614 #[test]
615 fn registry_knows_builtin_surface() {
616 let registry = CommandRegistry::load().unwrap();
617 assert!(registry.contains_builtin("cmake_minimum_required"));
618 assert!(registry.contains_builtin("target_sources"));
619 assert!(registry.contains_builtin("while"));
620 assert!(registry.contains_builtin("external_project_add"));
621 }
622
623 #[test]
624 fn registry_reports_audited_cmake_version() {
625 let registry = CommandRegistry::load().unwrap();
626 assert_eq!(registry.audited_cmake_version(), "4.3.1");
627 }
628
629 #[test]
630 fn registry_knows_project_43_keywords() {
631 let registry = CommandRegistry::load().unwrap();
632 let CommandSpec::Single(form) = registry.get("project") else {
633 panic!()
634 };
635 assert!(form.flags.contains("COMPAT_VERSION"));
636 assert!(form.flags.contains("SPDX_LICENSE"));
637 }
638
639 #[test]
640 fn registry_knows_export_package_info_form() {
641 let registry = CommandRegistry::load().unwrap();
642 let CommandSpec::Discriminated { .. } = registry.get("export") else {
643 panic!()
644 };
645 let form = registry.get("export").form_for(Some("PACKAGE_INFO"));
646 assert_eq!(form.pargs, NArgs::Fixed(1));
647 assert!(form.kwargs.contains_key("EXPORT"));
648 assert!(form.kwargs.contains_key("CXX_MODULES_DIRECTORY"));
649 }
650
651 #[test]
652 fn registry_knows_install_package_info_form() {
653 let registry = CommandRegistry::load().unwrap();
654 let form = registry.get("install").form_for(Some("PACKAGE_INFO"));
655 assert_eq!(form.pargs, NArgs::Fixed(1));
656 assert!(form.kwargs.contains_key("DESTINATION"));
657 assert!(form.kwargs.contains_key("COMPAT_VERSION"));
658 }
659
660 #[test]
661 fn registry_knows_install_export_namespace_keyword() {
662 let registry = CommandRegistry::load().unwrap();
663 let form = registry.get("install").form_for(Some("EXPORT"));
664 assert!(form.kwargs.contains_key("DESTINATION"));
665 assert!(form.kwargs.contains_key("NAMESPACE"));
666 assert!(form.kwargs.contains_key("FILE"));
667 assert!(form.flags.contains("EXCLUDE_FROM_ALL"));
668 }
669
670 #[test]
671 fn registry_knows_install_targets_export_and_includes_sections() {
672 let registry = CommandRegistry::load().unwrap();
673 let form = registry.get("install").form_for(Some("TARGETS"));
674 assert!(form.kwargs.contains_key("EXPORT"));
675 assert!(form.kwargs.contains_key("INCLUDES"));
676 assert!(form
677 .kwargs
678 .get("INCLUDES")
679 .is_some_and(|spec| spec.kwargs.contains_key("DESTINATION")));
680 assert!(form.kwargs.contains_key("RUNTIME_DEPENDENCY_SET"));
681 }
682
683 #[test]
684 fn install_targets_artifact_kinds_are_kwargs_with_subgroups() {
685 let registry = CommandRegistry::load().unwrap();
686 let form = registry.get("install").form_for(Some("TARGETS"));
687
688 for kind in [
689 "ARCHIVE",
690 "LIBRARY",
691 "RUNTIME",
692 "OBJECTS",
693 "FRAMEWORK",
694 "BUNDLE",
695 "PRIVATE_HEADER",
696 "PUBLIC_HEADER",
697 "RESOURCE",
698 "FILE_SET",
699 "CXX_MODULES_BMI",
700 ] {
701 let spec = form
702 .kwargs
703 .get(kind)
704 .unwrap_or_else(|| panic!("install(TARGETS) missing artifact kind {kind}"));
705 for sub in [
706 "DESTINATION",
707 "PERMISSIONS",
708 "CONFIGURATIONS",
709 "COMPONENT",
710 "NAMELINK_COMPONENT",
711 ] {
712 assert!(
713 spec.kwargs.contains_key(sub),
714 "{kind} missing subkwarg {sub}"
715 );
716 }
717 for flag in [
718 "OPTIONAL",
719 "EXCLUDE_FROM_ALL",
720 "NAMELINK_ONLY",
721 "NAMELINK_SKIP",
722 ] {
723 assert!(spec.flags.contains(flag), "{kind} missing subflag {flag}");
724 }
725 assert!(
726 !form.flags.contains(kind),
727 "{kind} should not appear as an outer flag"
728 );
729 }
730 }
731
732 #[test]
733 fn install_targets_file_set_takes_positional_set_name() {
734 let registry = CommandRegistry::load().unwrap();
735 let form = registry.get("install").form_for(Some("TARGETS"));
736 let file_set = form.kwargs.get("FILE_SET").unwrap();
737 assert_eq!(file_set.nargs, crate::spec::NArgs::Fixed(1));
738 }
739
740 #[test]
741 fn install_targets_artifact_option_flags_are_not_outer_flags() {
742 let registry = CommandRegistry::load().unwrap();
743 let form = registry.get("install").form_for(Some("TARGETS"));
744 for flag in [
745 "OPTIONAL",
746 "EXCLUDE_FROM_ALL",
747 "NAMELINK_ONLY",
748 "NAMELINK_SKIP",
749 ] {
750 assert!(
751 !form.flags.contains(flag),
752 "{flag} should not appear at the outer TARGETS level"
753 );
754 }
755 }
756
757 #[test]
758 fn install_targets_runtime_dependencies_is_kwarg_group() {
759 let registry = CommandRegistry::load().unwrap();
760 let form = registry.get("install").form_for(Some("TARGETS"));
761 let rd = form.kwargs.get("RUNTIME_DEPENDENCIES").unwrap();
762 for sub in [
763 "DIRECTORIES",
764 "PRE_INCLUDE_REGEXES",
765 "PRE_EXCLUDE_REGEXES",
766 "POST_INCLUDE_REGEXES",
767 "POST_EXCLUDE_REGEXES",
768 "POST_INCLUDE_FILES",
769 "POST_EXCLUDE_FILES",
770 ] {
771 assert!(
772 rd.kwargs.contains_key(sub),
773 "RUNTIME_DEPENDENCIES missing subkwarg {sub}"
774 );
775 }
776 }
777
778 #[test]
779 fn install_imported_runtime_artifacts_artifact_kinds_are_kwargs() {
780 let registry = CommandRegistry::load().unwrap();
781 let form = registry
782 .get("install")
783 .form_for(Some("IMPORTED_RUNTIME_ARTIFACTS"));
784
785 for kind in ["LIBRARY", "RUNTIME", "FRAMEWORK", "BUNDLE"] {
786 let spec = form
787 .kwargs
788 .get(kind)
789 .unwrap_or_else(|| panic!("IMPORTED_RUNTIME_ARTIFACTS missing {kind}"));
790 for sub in ["DESTINATION", "PERMISSIONS", "CONFIGURATIONS", "COMPONENT"] {
791 assert!(
792 spec.kwargs.contains_key(sub),
793 "{kind} missing subkwarg {sub}"
794 );
795 }
796 for flag in ["OPTIONAL", "EXCLUDE_FROM_ALL"] {
797 assert!(spec.flags.contains(flag), "{kind} missing subflag {flag}");
798 }
799 assert!(!form.flags.contains(kind));
800 }
801 }
802
803 #[test]
804 fn install_files_has_type_rename_and_exclude_from_all() {
805 let registry = CommandRegistry::load().unwrap();
806 let form = registry.get("install").form_for(Some("FILES"));
807 assert!(form.kwargs.contains_key("TYPE"));
808 assert!(form.kwargs.contains_key("RENAME"));
809 assert!(form.flags.contains("EXCLUDE_FROM_ALL"));
810 }
811
812 #[test]
813 fn install_directory_has_full_option_coverage() {
814 let registry = CommandRegistry::load().unwrap();
815 let form = registry.get("install").form_for(Some("DIRECTORY"));
816 for kw in [
817 "TYPE",
818 "DESTINATION",
819 "FILE_PERMISSIONS",
820 "DIRECTORY_PERMISSIONS",
821 "CONFIGURATIONS",
822 "COMPONENT",
823 "PATTERN",
824 "REGEX",
825 ] {
826 assert!(form.kwargs.contains_key(kw), "DIRECTORY missing kwarg {kw}");
827 }
828 assert!(
831 !form.kwargs.contains_key("PERMISSIONS"),
832 "PERMISSIONS must not be a top-level DIRECTORY kwarg"
833 );
834 for flag in [
835 "OPTIONAL",
836 "USE_SOURCE_PERMISSIONS",
837 "MESSAGE_NEVER",
838 "EXCLUDE_FROM_ALL",
839 "FILES_MATCHING",
840 ] {
841 assert!(form.flags.contains(flag), "DIRECTORY missing flag {flag}");
842 }
843 }
844
845 #[test]
846 fn install_directory_pattern_and_regex_open_subgroup() {
847 let registry = CommandRegistry::load().unwrap();
848 let form = registry.get("install").form_for(Some("DIRECTORY"));
849 for name in ["PATTERN", "REGEX"] {
850 let spec = form.kwargs.get(name).unwrap();
851 assert_eq!(spec.nargs, crate::spec::NArgs::Fixed(1));
852 assert!(spec.flags.contains("EXCLUDE"), "{name} missing EXCLUDE");
853 assert!(
854 spec.kwargs.contains_key("PERMISSIONS"),
855 "{name} missing PERMISSIONS subkwarg"
856 );
857 }
858 }
859
860 #[test]
861 fn install_programs_mirrors_files_form() {
862 let registry = CommandRegistry::load().unwrap();
863 let form = registry.get("install").form_for(Some("PROGRAMS"));
864 for kw in [
865 "TYPE",
866 "DESTINATION",
867 "PERMISSIONS",
868 "CONFIGURATIONS",
869 "COMPONENT",
870 "RENAME",
871 ] {
872 assert!(form.kwargs.contains_key(kw), "PROGRAMS missing kwarg {kw}");
873 }
874 assert!(form.flags.contains("OPTIONAL"));
875 assert!(form.flags.contains("EXCLUDE_FROM_ALL"));
876 }
877
878 #[test]
879 fn install_script_and_code_accept_component_and_flags() {
880 let registry = CommandRegistry::load().unwrap();
881 for disc in ["SCRIPT", "CODE"] {
882 let form = registry.get("install").form_for(Some(disc));
883 assert!(
884 form.kwargs.contains_key("COMPONENT"),
885 "{disc} missing COMPONENT"
886 );
887 assert!(
888 form.flags.contains("ALL_COMPONENTS"),
889 "{disc} missing ALL_COMPONENTS"
890 );
891 assert!(
892 form.flags.contains("EXCLUDE_FROM_ALL"),
893 "{disc} missing EXCLUDE_FROM_ALL"
894 );
895 }
896 }
897
898 #[test]
899 fn install_runtime_dependency_set_has_filter_kwargs_and_artifact_kinds() {
900 let registry = CommandRegistry::load().unwrap();
901 let form = registry
902 .get("install")
903 .form_for(Some("RUNTIME_DEPENDENCY_SET"));
904
905 for sub in [
906 "DIRECTORIES",
907 "PRE_INCLUDE_REGEXES",
908 "PRE_EXCLUDE_REGEXES",
909 "POST_INCLUDE_REGEXES",
910 "POST_EXCLUDE_REGEXES",
911 "POST_INCLUDE_FILES",
912 "POST_EXCLUDE_FILES",
913 ] {
914 assert!(
915 form.kwargs.contains_key(sub),
916 "RUNTIME_DEPENDENCY_SET missing {sub}"
917 );
918 }
919
920 for kind in ["LIBRARY", "RUNTIME", "FRAMEWORK"] {
921 let spec = form
922 .kwargs
923 .get(kind)
924 .unwrap_or_else(|| panic!("RUNTIME_DEPENDENCY_SET missing {kind}"));
925 for k in [
926 "DESTINATION",
927 "PERMISSIONS",
928 "CONFIGURATIONS",
929 "COMPONENT",
930 "NAMELINK_COMPONENT",
931 ] {
932 assert!(spec.kwargs.contains_key(k), "{kind} missing subkwarg {k}");
933 }
934 for f in [
935 "OPTIONAL",
936 "EXCLUDE_FROM_ALL",
937 "NAMELINK_ONLY",
938 "NAMELINK_SKIP",
939 ] {
940 assert!(spec.flags.contains(f), "{kind} missing subflag {f}");
941 }
942 }
943 }
944
945 #[test]
946 fn registry_knows_cmake_language_trace_form() {
947 let registry = CommandRegistry::load().unwrap();
948 let form = registry.get("cmake_language").form_for(Some("TRACE"));
949 assert!(form.flags.contains("ON"));
950 assert!(form.flags.contains("OFF"));
951 assert!(form.flags.contains("EXPAND"));
952 }
953
954 #[test]
955 fn registry_knows_cmake_pkg_config_import_keywords() {
956 let registry = CommandRegistry::load().unwrap();
957 let form = registry.get("cmake_pkg_config").form_for(Some("IMPORT"));
958 assert!(form.kwargs.contains_key("NAME"));
959 assert!(form.kwargs.contains_key("BIND_PC_REQUIRES"));
960 }
961
962 #[test]
963 fn registry_knows_file_archive_create_threads() {
964 let registry = CommandRegistry::load().unwrap();
965 let form = registry.get("file").form_for(Some("ARCHIVE_CREATE"));
966 assert!(form.kwargs.contains_key("THREADS"));
967 assert!(form.kwargs.contains_key("COMPRESSION_LEVEL"));
968 }
969
970 #[test]
971 fn registry_knows_file_strings_keywords() {
972 let registry = CommandRegistry::load().unwrap();
973 let form = registry.get("file").form_for(Some("STRINGS"));
974 assert_eq!(form.pargs, NArgs::Fixed(2));
975 assert!(form.kwargs.contains_key("REGEX"));
976 assert!(form.kwargs.contains_key("LIMIT_COUNT"));
977 }
978
979 #[test]
980 fn registry_knows_cmake_package_config_helpers_commands() {
981 let registry = CommandRegistry::load().unwrap();
982 let configure = registry.get("configure_package_config_file").form_for(None);
983 assert!(configure.kwargs.contains_key("INSTALL_DESTINATION"));
984 assert!(configure.kwargs.contains_key("PATH_VARS"));
985
986 let version = registry
987 .get("write_basic_package_version_file")
988 .form_for(None);
989 assert!(version.kwargs.contains_key("COMPATIBILITY"));
990 assert!(version.kwargs.contains_key("VERSION"));
991 }
992
993 #[test]
994 fn registry_knows_utility_module_commands() {
995 let registry = CommandRegistry::load().unwrap();
996 assert_eq!(
997 registry.get("cmake_dependent_option").form_for(None).pargs,
998 NArgs::Fixed(5)
999 );
1000 assert_eq!(
1001 registry.get("check_language").form_for(None).pargs,
1002 NArgs::Fixed(1)
1003 );
1004 assert_eq!(
1005 registry.get("check_include_file").form_for(None).pargs,
1006 NArgs::AtLeast(2)
1007 );
1008 assert_eq!(
1009 registry.get("check_compiler_flag").form_for(None).pargs,
1010 NArgs::Fixed(3)
1011 );
1012 assert_eq!(
1013 registry
1014 .get("check_objc_compiler_flag")
1015 .form_for(None)
1016 .pargs,
1017 NArgs::Fixed(2)
1018 );
1019 assert_eq!(
1020 registry.get("check_cxx_symbol_exists").form_for(None).pargs,
1021 NArgs::Fixed(3)
1022 );
1023 assert!(registry
1024 .get("cmake_push_check_state")
1025 .form_for(None)
1026 .flags
1027 .contains("RESET"));
1028 let print_props = registry.get("cmake_print_properties").form_for(None);
1029 assert!(print_props.kwargs.contains_key("TARGETS"));
1030 assert!(print_props.kwargs.contains_key("PROPERTIES"));
1031 let pie = registry.get("check_pie_supported").form_for(None);
1032 assert!(pie.kwargs.contains_key("OUTPUT_VARIABLE"));
1033 assert!(pie.kwargs.contains_key("LANGUAGES"));
1034 let source_compiles = registry.get("check_source_compiles").form_for(None);
1035 assert!(source_compiles.kwargs.contains_key("SRC_EXT"));
1036 assert!(source_compiles.kwargs.contains_key("FAIL_REGEX"));
1037 let find_dependency = registry.get("find_dependency").form_for(None);
1038 assert!(find_dependency.flags.contains("REQUIRED"));
1039 assert!(find_dependency.kwargs.contains_key("COMPONENTS"));
1040 }
1041
1042 #[test]
1043 fn registry_knows_supported_deprecated_module_commands() {
1044 let registry = CommandRegistry::load().unwrap();
1045 let version = registry
1046 .get("write_basic_config_version_file")
1047 .form_for(None);
1048 assert_eq!(version.pargs, NArgs::Fixed(1));
1049 assert!(version.kwargs.contains_key("COMPATIBILITY"));
1050 assert!(version.flags.contains("ARCH_INDEPENDENT"));
1051 assert_eq!(
1052 registry.get("check_cxx_accepts_flag").form_for(None).pargs,
1053 NArgs::Fixed(2)
1054 );
1055 }
1056
1057 #[test]
1058 fn registry_knows_fetchcontent_commands() {
1059 let registry = CommandRegistry::load().unwrap();
1060 let declare = registry.get("fetchcontent_declare").form_for(None);
1061 assert_eq!(declare.pargs, NArgs::Fixed(1));
1062 assert!(declare.flags.contains("EXCLUDE_FROM_ALL"));
1063 assert!(declare.kwargs.contains_key("FIND_PACKAGE_ARGS"));
1064
1065 let get_properties = registry.get("fetchcontent_getproperties").form_for(None);
1066 assert!(get_properties.kwargs.contains_key("SOURCE_DIR"));
1067 assert!(get_properties.kwargs.contains_key("BINARY_DIR"));
1068 assert!(get_properties.kwargs.contains_key("POPULATED"));
1069
1070 let populate = registry.get("fetchcontent_populate").form_for(None);
1071 assert!(populate.flags.contains("QUIET"));
1072 assert!(populate.kwargs.contains_key("SUBBUILD_DIR"));
1073 }
1074
1075 #[test]
1076 fn registry_knows_common_test_and_package_helper_modules() {
1077 let registry = CommandRegistry::load().unwrap();
1078
1079 let google_add = registry.get("gtest_add_tests").form_for(None);
1080 assert!(google_add.kwargs.contains_key("TARGET"));
1081 assert!(google_add.kwargs.contains_key("SOURCES"));
1082 assert!(google_add.flags.contains("SKIP_DEPENDENCY"));
1083
1084 let google_discover = registry.get("gtest_discover_tests").form_for(None);
1085 assert!(google_discover.kwargs.contains_key("DISCOVERY_MODE"));
1086 assert!(google_discover.kwargs.contains_key("XML_OUTPUT_DIR"));
1087 assert!(google_discover.flags.contains("NO_PRETTY_TYPES"));
1088
1089 assert_eq!(
1090 registry.get("processorcount").form_for(None).pargs,
1091 NArgs::Fixed(1)
1092 );
1093
1094 let fp_hsa = registry
1095 .get("find_package_handle_standard_args")
1096 .form_for(None);
1097 assert!(fp_hsa.flags.contains("DEFAULT_MSG"));
1098 assert!(fp_hsa.kwargs.contains_key("REQUIRED_VARS"));
1099 assert!(fp_hsa.kwargs.contains_key("VERSION_VAR"));
1100
1101 let fp_check = registry.get("find_package_check_version").form_for(None);
1102 assert_eq!(fp_check.pargs, NArgs::Fixed(2));
1103 assert!(fp_check.flags.contains("HANDLE_VERSION_RANGE"));
1104 }
1105
1106 #[test]
1107 fn registry_knows_externalproject_helper_commands() {
1108 let registry = CommandRegistry::load().unwrap();
1109 let step = registry.get("externalproject_add_step").form_for(None);
1110 assert_eq!(step.pargs, NArgs::Fixed(2));
1111 assert!(step.kwargs.contains_key("COMMAND"));
1112 assert!(step.kwargs.contains_key("DEPENDEES"));
1113 assert!(step.kwargs.contains_key("ENVIRONMENT_MODIFICATION"));
1114
1115 let targets = registry
1116 .get("externalproject_add_steptargets")
1117 .form_for(None);
1118 assert_eq!(targets.pargs, NArgs::AtLeast(2));
1119 assert!(targets.flags.contains("NO_DEPENDS"));
1120
1121 let deps = registry
1122 .get("externalproject_add_stepdependencies")
1123 .form_for(None);
1124 assert_eq!(deps.pargs, NArgs::AtLeast(3));
1125
1126 let props = registry.get("externalproject_get_property").form_for(None);
1127 assert_eq!(props.pargs, NArgs::AtLeast(2));
1128 }
1129
1130 #[test]
1131 fn registry_knows_packaging_and_find_helper_module_commands() {
1132 let registry = CommandRegistry::load().unwrap();
1133
1134 assert_eq!(
1135 registry.get("find_package_message").form_for(None).pargs,
1136 NArgs::Fixed(3)
1137 );
1138 assert_eq!(
1139 registry
1140 .get("select_library_configurations")
1141 .form_for(None)
1142 .pargs,
1143 NArgs::Fixed(1)
1144 );
1145
1146 let component = registry.get("cpack_add_component").form_for(None);
1147 assert!(component.flags.contains("HIDDEN"));
1148 assert!(component.kwargs.contains_key("DISPLAY_NAME"));
1149 assert!(component.kwargs.contains_key("DEPENDS"));
1150
1151 let group = registry.get("cpack_add_component_group").form_for(None);
1152 assert!(group.flags.contains("EXPANDED"));
1153 assert!(group.kwargs.contains_key("PARENT_GROUP"));
1154
1155 let downloads = registry.get("cpack_configure_downloads").form_for(None);
1156 assert_eq!(downloads.pargs, NArgs::Fixed(1));
1157 assert!(downloads.kwargs.contains_key("UPLOAD_DIRECTORY"));
1158 }
1159
1160 #[test]
1161 fn registry_knows_export_header_module_commands() {
1162 let registry = CommandRegistry::load().unwrap();
1163 let export_header = registry.get("generate_export_header").form_for(None);
1164 assert_eq!(export_header.pargs, NArgs::Fixed(1));
1165 assert!(export_header.flags.contains("DEFINE_NO_DEPRECATED"));
1166 assert!(export_header.kwargs.contains_key("EXPORT_FILE_NAME"));
1167 assert!(export_header.kwargs.contains_key("PREFIX_NAME"));
1168
1169 assert_eq!(
1170 registry
1171 .get("add_compiler_export_flags")
1172 .form_for(None)
1173 .pargs,
1174 NArgs::Optional
1175 );
1176 }
1177
1178 #[test]
1179 fn registry_knows_remaining_utility_module_commands() {
1180 let registry = CommandRegistry::load().unwrap();
1181
1182 for command in [
1183 "android_add_test_data",
1184 "add_file_dependencies",
1185 "cmake_add_fortran_subdirectory",
1186 "cmake_expand_imported_targets",
1187 "cmake_force_c_compiler",
1188 "cmake_force_cxx_compiler",
1189 "cmake_force_fortran_compiler",
1190 "ctest_coverage_collect_gcov",
1191 "copy_and_fixup_bundle",
1192 "fixup_bundle",
1193 "fixup_bundle_item",
1194 "verify_app",
1195 "verify_bundle_prerequisites",
1196 "verify_bundle_symlinks",
1197 "get_bundle_main_executable",
1198 "get_dotapp_dir",
1199 "get_bundle_and_executable",
1200 "get_bundle_all_executables",
1201 "get_bundle_keys",
1202 "get_item_key",
1203 "get_item_rpaths",
1204 "clear_bundle_keys",
1205 "set_bundle_key_values",
1206 "copy_resolved_framework_into_bundle",
1207 "copy_resolved_item_into_bundle",
1208 "cpack_ifw_add_package_resources",
1209 "cpack_ifw_add_repository",
1210 "cpack_ifw_configure_component",
1211 "cpack_ifw_configure_component_group",
1212 "cpack_ifw_update_repository",
1213 "cpack_ifw_configure_file",
1214 "csharp_set_windows_forms_properties",
1215 "csharp_set_designer_cs_properties",
1216 "csharp_set_xaml_cs_properties",
1217 "csharp_get_filename_keys",
1218 "csharp_get_filename_key_base",
1219 "csharp_get_dependentupon_name",
1220 "externaldata_expand_arguments",
1221 "externaldata_add_test",
1222 "externaldata_add_target",
1223 "fortrancinterface_header",
1224 "fortrancinterface_verify",
1225 "fetchcontent_setpopulated",
1226 "gnuinstalldirs_get_absolute_install_dir",
1227 "find_jar",
1228 "add_jar",
1229 "install_jar",
1230 "install_jar_exports",
1231 "export_jars",
1232 "create_javadoc",
1233 "create_javah",
1234 "install_jni_symlink",
1235 "swig_add_library",
1236 "swig_link_libraries",
1237 "print_enabled_features",
1238 "print_disabled_features",
1239 "set_feature_info",
1240 "set_package_info",
1241 ] {
1242 assert!(
1243 registry.contains_builtin(command),
1244 "missing built-in {command}"
1245 );
1246 }
1247
1248 assert_eq!(
1249 registry
1250 .get("ctest_coverage_collect_gcov")
1251 .form_for(None)
1252 .pargs,
1253 NArgs::ZeroOrMore
1254 );
1255 assert_eq!(
1256 registry
1257 .get("fortrancinterface_verify")
1258 .form_for(None)
1259 .pargs,
1260 NArgs::ZeroOrMore
1261 );
1262 assert_eq!(
1263 registry.get("add_jar").form_for(None).pargs,
1264 NArgs::AtLeast(2)
1265 );
1266 assert_eq!(
1267 registry
1268 .get("cpack_ifw_configure_file")
1269 .form_for(None)
1270 .pargs,
1271 NArgs::Fixed(2)
1272 );
1273 assert_eq!(
1274 registry
1275 .get("gnuinstalldirs_get_absolute_install_dir")
1276 .form_for(None)
1277 .pargs,
1278 NArgs::AtLeast(3)
1279 );
1280 }
1281
1282 #[test]
1283 fn registry_knows_string_json_43_modes() {
1284 let registry = CommandRegistry::load().unwrap();
1285 let form = registry.get("string").form_for(Some("JSON"));
1286 assert!(form.flags.contains("GET_RAW"));
1287 assert!(form.flags.contains("STRING_ENCODE"));
1288 assert!(form.kwargs.contains_key("ERROR_VARIABLE"));
1289 }
1290
1291 #[test]
1292 fn user_override_entries_merge_with_builtins() {
1293 let mut registry = CommandRegistry::load().unwrap();
1294 let overrides = r#"
1295[commands.target_link_libraries.layout]
1296always_wrap = true
1297
1298[commands.target_link_libraries.kwargs.LINKER_LANGUAGE]
1299nargs = 1
1300"#;
1301
1302 registry
1303 .merge_override_str(overrides, PathBuf::from("test-overrides.toml"))
1304 .unwrap();
1305
1306 let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
1307 panic!()
1308 };
1309 assert_eq!(
1310 form.layout.as_ref().and_then(|layout| layout.always_wrap),
1311 Some(true)
1312 );
1313 assert!(form.kwargs.contains_key("PUBLIC"));
1314 assert_eq!(form.kwargs["LINKER_LANGUAGE"].nargs, NArgs::Fixed(1));
1315 }
1316
1317 #[test]
1318 fn uppercase_lookup_uses_builtin_normalization() {
1319 let registry = CommandRegistry::load().unwrap();
1320 assert!(registry.contains_builtin("TARGET_LINK_LIBRARIES"));
1321 let CommandSpec::Single(form) = registry.get("TARGET_LINK_LIBRARIES") else {
1322 panic!()
1323 };
1324 assert!(form.kwargs.contains_key("PUBLIC"));
1325 assert!(form.kwargs.contains_key("PRIVATE"));
1326 }
1327
1328 #[test]
1329 fn contains_builtin_excludes_user_added_commands_after_merge() {
1330 let mut registry = CommandRegistry::load().unwrap();
1331 registry
1332 .merge_toml_overrides(
1333 r#"
1334[commands.my_custom_command]
1335pargs = 1
1336"#,
1337 )
1338 .unwrap();
1339
1340 assert!(!registry.contains_builtin("my_custom_command"));
1341 assert!(!registry.contains_builtin("MY_CUSTOM_COMMAND"));
1342 assert!(matches!(
1343 registry.get("my_custom_command"),
1344 CommandSpec::Single(_)
1345 ));
1346 }
1347
1348 #[test]
1349 fn from_builtins_and_yaml_override_file_merges_entries() {
1350 let dir = tempfile::tempdir().unwrap();
1351 let overrides = dir.path().join("override.yaml");
1352 fs::write(
1353 &overrides,
1354 r#"
1355commands:
1356 target_link_libraries:
1357 kwargs:
1358 linker_language:
1359 nargs: 1
1360"#,
1361 )
1362 .unwrap();
1363
1364 let registry = CommandRegistry::from_builtins_and_overrides(Some(&overrides)).unwrap();
1365 let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
1366 panic!()
1367 };
1368 assert_eq!(form.kwargs["LINKER_LANGUAGE"].nargs, NArgs::Fixed(1));
1369 }
1370
1371 #[test]
1372 fn merge_override_file_reports_structured_toml_parse_errors() {
1373 let mut registry = CommandRegistry::load().unwrap();
1374 let dir = tempfile::tempdir().unwrap();
1375 let path = dir.path().join("override.toml");
1376 fs::write(&path, "[commands.bad]\npargs = [\n").unwrap();
1377
1378 let err = registry.merge_override_file(&path).unwrap_err();
1379 match err {
1380 Error::Spec(spec_err) => {
1381 let details = &spec_err.details;
1382 assert_eq!(details.format, "TOML");
1383 assert!(details.line.is_some());
1384 assert!(details.column.is_some());
1385 }
1386 other => panic!("expected spec parse error, got {other:?}"),
1387 }
1388 }
1389
1390 #[test]
1391 fn merge_override_file_reports_structured_yaml_parse_errors() {
1392 let mut registry = CommandRegistry::load().unwrap();
1393 let dir = tempfile::tempdir().unwrap();
1394 let path = dir.path().join("override.yaml");
1395 fs::write(&path, "commands:\n target_link_libraries: [\n").unwrap();
1396
1397 let err = registry.merge_override_file(&path).unwrap_err();
1398 match err {
1399 Error::Spec(spec_err) => {
1400 let details = &spec_err.details;
1401 assert_eq!(details.format, "YAML");
1402 assert!(details.line.is_some());
1403 assert!(details.column.is_some());
1404 }
1405 other => panic!("expected spec parse error, got {other:?}"),
1406 }
1407 }
1408
1409 #[test]
1410 fn override_with_mismatched_shape_replaces_base_command_spec() {
1411 let mut registry = CommandRegistry::load().unwrap();
1412 registry
1413 .merge_override_str(
1414 r#"
1415[commands.cmake_minimum_required.forms.VERSION]
1416pargs = 1
1417"#,
1418 PathBuf::from("override.toml"),
1419 )
1420 .unwrap();
1421
1422 let CommandSpec::Discriminated { .. } = registry.get("cmake_minimum_required") else {
1423 panic!("expected discriminated command after mismatched override")
1424 };
1425 assert_eq!(
1426 registry
1427 .get("cmake_minimum_required")
1428 .form_for(Some("VERSION"))
1429 .pargs,
1430 NArgs::Fixed(1)
1431 );
1432 }
1433}