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