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