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.toml";
27const BUILTINS_TOML: &str = include_str!("builtins.toml");
28
29#[derive(Debug, Clone)]
49pub struct CommandRegistry {
50 metadata: SpecMetadata,
51 commands: IndexMap<String, CommandSpec>,
52 fallback: CommandSpec,
53}
54
55impl CommandRegistry {
56 pub fn load() -> Result<Self> {
62 Self::load_builtins_impl()
63 }
64
65 pub fn builtins() -> &'static Self {
71 static BUILTINS: OnceLock<CommandRegistry> = OnceLock::new();
72 BUILTINS.get_or_init(|| {
73 Self::load_builtins_impl()
74 .expect("embedded built-in command registry should deserialize")
75 })
76 }
77
78 #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
79 fn load_builtins_impl() -> Result<Self> {
80 Self::from_builtins_and_overrides(None::<&Path>)
81 }
82
83 #[cfg(any(target_arch = "wasm32", not(feature = "cli")))]
84 fn load_builtins_impl() -> Result<Self> {
85 Ok(Self::from_spec_file(parse_builtins()?))
86 }
87
88 #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
90 pub fn from_builtins_and_overrides(path: Option<impl AsRef<Path>>) -> Result<Self> {
91 let mut registry = Self::from_spec_file(parse_builtins()?);
92
93 if let Some(path) = path {
94 registry.merge_override_file(path.as_ref())?;
95 }
96
97 Ok(registry)
98 }
99
100 pub(crate) fn from_spec_file(mut spec_file: SpecFile) -> Self {
102 normalize_spec_file(&mut spec_file);
103 Self {
104 metadata: spec_file.metadata,
105 commands: spec_file.commands,
106 fallback: CommandSpec::Single(CommandForm::default()),
107 }
108 }
109
110 pub fn merge_toml_overrides(&mut self, toml_source: &str) -> Result<()> {
112 let mut overrides: SpecOverrideFile = toml::from_str(toml_source)
113 .map_err(|e| Error::Formatter(format!("spec TOML error: {e}")))?;
114 self.apply_overrides(&mut overrides);
115 Ok(())
116 }
117
118 pub fn merge_yaml_overrides(&mut self, yaml_source: &str) -> Result<()> {
120 let mut overrides: SpecOverrideFile = serde_yaml::from_str(yaml_source)
121 .map_err(|e| Error::Formatter(format!("spec YAML error: {e}")))?;
122 self.apply_overrides(&mut overrides);
123 Ok(())
124 }
125
126 fn apply_overrides(&mut self, overrides: &mut SpecOverrideFile) {
127 normalize_override_file(overrides);
128 let commands = std::mem::take(&mut overrides.commands);
129 for (name, override_spec) in commands {
130 match self.commands.get_mut(&name) {
131 Some(existing) => merge_command_spec(existing, override_spec),
132 None => {
133 self.commands.insert(name, override_spec.into_full_spec());
134 }
135 }
136 }
137 }
138
139 #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
141 pub fn merge_override_file(&mut self, path: &Path) -> Result<()> {
142 let source = fs::read_to_string(path)?;
143 self.merge_override_source(&source, path.to_path_buf(), detect_config_format(path)?)
144 }
145
146 #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
148 pub fn merge_override_str(&mut self, source: &str, path: impl Into<PathBuf>) -> Result<()> {
149 self.merge_override_source(source, path.into(), ConfigFileFormat::Toml)
150 }
151
152 #[cfg(all(not(target_arch = "wasm32"), feature = "cli"))]
153 fn merge_override_source(
154 &mut self,
155 source: &str,
156 path: PathBuf,
157 format: ConfigFileFormat,
158 ) -> Result<()> {
159 let mut overrides: SpecOverrideFile = match format {
160 ConfigFileFormat::Toml => toml::from_str(source).map_err(|toml_err| {
161 let (line, column) = crate::config::file::toml_line_col(
162 source,
163 toml_err.span().map(|span| span.start),
164 );
165 Error::Spec {
166 path: path.clone(),
167 details: crate::error::FileParseError {
168 format: format.as_str(),
169 message: toml_err.to_string().into_boxed_str(),
170 line,
171 column,
172 },
173 source_message: toml_err.to_string().into_boxed_str(),
174 }
175 })?,
176 ConfigFileFormat::Yaml => serde_yaml::from_str(source).map_err(|yaml_err| {
177 let location = yaml_err.location();
178 Error::Spec {
179 path: path.clone(),
180 details: crate::error::FileParseError {
181 format: format.as_str(),
182 message: yaml_err.to_string().into_boxed_str(),
183 line: location.as_ref().map(|loc| loc.line()),
184 column: location.as_ref().map(|loc| loc.column()),
185 },
186 source_message: yaml_err.to_string().into_boxed_str(),
187 }
188 })?,
189 };
190 normalize_override_file(&mut overrides);
191
192 for (name, override_spec) in overrides.commands {
193 match self.commands.get_mut(&name) {
194 Some(existing) => merge_command_spec(existing, override_spec),
195 None => {
196 self.commands.insert(name, override_spec.into_full_spec());
197 }
198 }
199 }
200
201 Ok(())
202 }
203
204 pub fn get(&self, command_name: &str) -> &CommandSpec {
207 if let Some(spec) = self.commands.get(command_name) {
208 return spec;
209 }
210
211 if !has_ascii_uppercase(command_name) {
212 return &self.fallback;
213 }
214
215 self.commands
216 .get(&command_name.to_ascii_lowercase())
217 .unwrap_or(&self.fallback)
218 }
219
220 pub fn contains_builtin(&self, command_name: &str) -> bool {
222 self.commands.contains_key(command_name)
223 || (has_ascii_uppercase(command_name)
224 && self
225 .commands
226 .contains_key(&command_name.to_ascii_lowercase()))
227 }
228
229 pub fn audited_cmake_version(&self) -> &str {
231 &self.metadata.cmake_version
232 }
233}
234
235fn has_ascii_uppercase(s: &str) -> bool {
236 s.bytes().any(|byte| byte.is_ascii_uppercase())
237}
238
239fn parse_builtins() -> Result<SpecFile> {
240 let mut spec: SpecFile = toml::from_str(BUILTINS_TOML).map_err(|source| Error::Spec {
241 path: PathBuf::from(BUILTINS_PATH),
242 details: crate::error::FileParseError {
243 format: "TOML",
244 message: source.to_string().into_boxed_str(),
245 line: None,
246 column: None,
247 },
248 source_message: source.to_string().into_boxed_str(),
249 })?;
250 normalize_spec_file(&mut spec);
251 Ok(spec)
252}
253
254fn normalize_spec_file(spec: &mut SpecFile) {
255 spec.commands = std::mem::take(&mut spec.commands)
256 .into_iter()
257 .map(|(name, mut command)| {
258 normalize_command_spec(&mut command);
259 (name.to_ascii_lowercase(), command)
260 })
261 .collect();
262}
263
264fn normalize_override_file(spec: &mut SpecOverrideFile) {
265 spec.commands = std::mem::take(&mut spec.commands)
266 .into_iter()
267 .map(|(name, mut command)| {
268 normalize_command_override(&mut command);
269 (name.to_ascii_lowercase(), command)
270 })
271 .collect();
272}
273
274fn normalize_command_spec(spec: &mut CommandSpec) {
275 match spec {
276 CommandSpec::Single(form) => normalize_form(form),
277 CommandSpec::Discriminated { forms, fallback } => {
278 *forms = std::mem::take(forms)
279 .into_iter()
280 .map(|(name, mut form)| {
281 normalize_form(&mut form);
282 (name.to_ascii_uppercase(), form)
283 })
284 .collect();
285
286 if let Some(fallback) = fallback {
287 normalize_form(fallback);
288 }
289 }
290 }
291}
292
293fn normalize_command_override(spec: &mut CommandSpecOverride) {
294 match spec {
295 CommandSpecOverride::Single(form) => normalize_form_override(form),
296 CommandSpecOverride::Discriminated { forms, fallback } => {
297 *forms = std::mem::take(forms)
298 .into_iter()
299 .map(|(name, mut form)| {
300 normalize_form_override(&mut form);
301 (name.to_ascii_uppercase(), form)
302 })
303 .collect();
304
305 if let Some(fallback) = fallback {
306 normalize_form_override(fallback);
307 }
308 }
309 }
310}
311
312fn normalize_form(form: &mut CommandForm) {
313 form.kwargs = std::mem::take(&mut form.kwargs)
314 .into_iter()
315 .map(|(name, mut kwarg)| {
316 normalize_kwarg(&mut kwarg);
317 (name.to_ascii_uppercase(), kwarg)
318 })
319 .collect();
320
321 form.flags = std::mem::take(&mut form.flags)
322 .into_iter()
323 .map(|flag| flag.to_ascii_uppercase())
324 .collect();
325}
326
327fn normalize_form_override(form: &mut CommandFormOverride) {
328 form.kwargs = std::mem::take(&mut form.kwargs)
329 .into_iter()
330 .map(|(name, mut kwarg)| {
331 normalize_kwarg_override(&mut kwarg);
332 (name.to_ascii_uppercase(), kwarg)
333 })
334 .collect();
335
336 form.flags = std::mem::take(&mut form.flags)
337 .into_iter()
338 .map(|flag| flag.to_ascii_uppercase())
339 .collect();
340}
341
342fn normalize_kwarg(spec: &mut KwargSpec) {
343 spec.kwargs = std::mem::take(&mut spec.kwargs)
344 .into_iter()
345 .map(|(name, mut kwarg)| {
346 normalize_kwarg(&mut kwarg);
347 (name.to_ascii_uppercase(), kwarg)
348 })
349 .collect();
350
351 spec.flags = std::mem::take(&mut spec.flags)
352 .into_iter()
353 .map(|flag| flag.to_ascii_uppercase())
354 .collect();
355}
356
357fn normalize_kwarg_override(spec: &mut KwargSpecOverride) {
358 spec.kwargs = std::mem::take(&mut spec.kwargs)
359 .into_iter()
360 .map(|(name, mut kwarg)| {
361 normalize_kwarg_override(&mut kwarg);
362 (name.to_ascii_uppercase(), kwarg)
363 })
364 .collect();
365
366 spec.flags = std::mem::take(&mut spec.flags)
367 .into_iter()
368 .map(|flag| flag.to_ascii_uppercase())
369 .collect();
370}
371
372fn merge_command_spec(base: &mut CommandSpec, override_spec: CommandSpecOverride) {
373 match (base, override_spec) {
374 (CommandSpec::Single(base_form), CommandSpecOverride::Single(override_form)) => {
375 merge_form(base_form, override_form);
376 }
377 (
378 CommandSpec::Discriminated {
379 forms: base_forms,
380 fallback: base_fallback,
381 },
382 CommandSpecOverride::Discriminated {
383 forms: override_forms,
384 fallback: override_fallback,
385 },
386 ) => {
387 for (name, override_form) in override_forms {
388 match base_forms.get_mut(&name) {
389 Some(base_form) => merge_form(base_form, override_form),
390 None => {
391 base_forms.insert(name, override_form.into_full_form());
392 }
393 }
394 }
395
396 if let Some(override_fallback) = override_fallback {
397 match base_fallback {
398 Some(base_fallback) => merge_form(base_fallback, override_fallback),
399 None => {
400 *base_fallback = Some(override_fallback.into_full_form());
401 }
402 }
403 }
404 }
405 (base_spec, override_spec) => {
406 *base_spec = override_spec.into_full_spec();
407 }
408 }
409}
410
411fn merge_form(base: &mut CommandForm, override_form: CommandFormOverride) {
412 if let Some(pargs) = override_form.pargs {
413 base.pargs = pargs;
414 }
415
416 merge_flags(&mut base.flags, override_form.flags);
417
418 for (name, override_kwarg) in override_form.kwargs {
419 match base.kwargs.get_mut(&name) {
420 Some(base_kwarg) => merge_kwarg(base_kwarg, override_kwarg),
421 None => {
422 base.kwargs.insert(name, override_kwarg.into_full_spec());
423 }
424 }
425 }
426
427 if let Some(layout) = override_form.layout {
428 merge_layout(
429 base.layout.get_or_insert_with(LayoutOverrides::default),
430 layout,
431 );
432 }
433}
434
435fn merge_kwarg(base: &mut KwargSpec, override_kwarg: KwargSpecOverride) {
436 if let Some(nargs) = override_kwarg.nargs {
437 base.nargs = nargs;
438 }
439
440 merge_flags(&mut base.flags, override_kwarg.flags);
441
442 for (name, nested_override) in override_kwarg.kwargs {
443 match base.kwargs.get_mut(&name) {
444 Some(base_nested) => merge_kwarg(base_nested, nested_override),
445 None => {
446 base.kwargs.insert(name, nested_override.into_full_spec());
447 }
448 }
449 }
450}
451
452fn merge_layout(base: &mut LayoutOverrides, override_layout: LayoutOverridesOverride) {
453 if let Some(value) = override_layout.line_width {
454 base.line_width = Some(value);
455 }
456 if let Some(value) = override_layout.tab_size {
457 base.tab_size = Some(value);
458 }
459 if let Some(value) = override_layout.dangle_parens {
460 base.dangle_parens = Some(value);
461 }
462 if let Some(value) = override_layout.always_wrap {
463 base.always_wrap = Some(value);
464 }
465 if let Some(value) = override_layout.max_pargs_hwrap {
466 base.max_pargs_hwrap = Some(value);
467 }
468}
469
470fn merge_flags(base: &mut IndexSet<String>, override_flags: IndexSet<String>) {
471 for flag in override_flags {
472 base.insert(flag);
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::spec::NArgs;
480 use std::fs;
481
482 #[test]
483 fn registry_has_target_link_libraries_keywords() {
484 let registry = CommandRegistry::load().unwrap();
485 let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
486 panic!()
487 };
488 assert!(form.kwargs.contains_key("PUBLIC"));
489 assert!(form.kwargs.contains_key("PRIVATE"));
490 assert!(form.kwargs.contains_key("INTERFACE"));
491 }
492
493 #[test]
494 fn registry_has_install_forms() {
495 let registry = CommandRegistry::load().unwrap();
496 assert!(matches!(
497 registry.get("install"),
498 CommandSpec::Discriminated { .. }
499 ));
500 }
501
502 #[test]
503 fn registry_unknown_command_uses_fallback() {
504 let registry = CommandRegistry::load().unwrap();
505 let spec = registry.get("my_unknown_command");
506 let CommandSpec::Single(form) = spec else {
507 panic!()
508 };
509 assert_eq!(form.pargs, NArgs::ZeroOrMore);
510 assert!(form.kwargs.is_empty());
511 assert!(form.flags.is_empty());
512 }
513
514 #[test]
515 fn registry_knows_builtin_surface() {
516 let registry = CommandRegistry::load().unwrap();
517 assert!(registry.contains_builtin("cmake_minimum_required"));
518 assert!(registry.contains_builtin("target_sources"));
519 assert!(registry.contains_builtin("while"));
520 assert!(registry.contains_builtin("external_project_add"));
521 }
522
523 #[test]
524 fn registry_reports_audited_cmake_version() {
525 let registry = CommandRegistry::load().unwrap();
526 assert_eq!(registry.audited_cmake_version(), "4.3.1");
527 }
528
529 #[test]
530 fn registry_knows_project_43_keywords() {
531 let registry = CommandRegistry::load().unwrap();
532 let CommandSpec::Single(form) = registry.get("project") else {
533 panic!()
534 };
535 assert!(form.flags.contains("COMPAT_VERSION"));
536 assert!(form.flags.contains("SPDX_LICENSE"));
537 }
538
539 #[test]
540 fn registry_knows_export_package_info_form() {
541 let registry = CommandRegistry::load().unwrap();
542 let CommandSpec::Discriminated { .. } = registry.get("export") else {
543 panic!()
544 };
545 let form = registry.get("export").form_for(Some("PACKAGE_INFO"));
546 assert_eq!(form.pargs, NArgs::Fixed(1));
547 assert!(form.kwargs.contains_key("EXPORT"));
548 assert!(form.kwargs.contains_key("CXX_MODULES_DIRECTORY"));
549 }
550
551 #[test]
552 fn registry_knows_install_package_info_form() {
553 let registry = CommandRegistry::load().unwrap();
554 let form = registry.get("install").form_for(Some("PACKAGE_INFO"));
555 assert_eq!(form.pargs, NArgs::Fixed(1));
556 assert!(form.kwargs.contains_key("DESTINATION"));
557 assert!(form.kwargs.contains_key("COMPAT_VERSION"));
558 }
559
560 #[test]
561 fn registry_knows_install_export_namespace_keyword() {
562 let registry = CommandRegistry::load().unwrap();
563 let form = registry.get("install").form_for(Some("EXPORT"));
564 assert!(form.kwargs.contains_key("DESTINATION"));
565 assert!(form.kwargs.contains_key("NAMESPACE"));
566 assert!(form.kwargs.contains_key("FILE"));
567 assert!(form.flags.contains("EXCLUDE_FROM_ALL"));
568 }
569
570 #[test]
571 fn registry_knows_install_targets_export_and_includes_sections() {
572 let registry = CommandRegistry::load().unwrap();
573 let form = registry.get("install").form_for(Some("TARGETS"));
574 assert!(form.kwargs.contains_key("EXPORT"));
575 assert!(form.kwargs.contains_key("INCLUDES"));
576 assert!(form
577 .kwargs
578 .get("INCLUDES")
579 .is_some_and(|spec| spec.kwargs.contains_key("DESTINATION")));
580 assert!(form.kwargs.contains_key("RUNTIME_DEPENDENCY_SET"));
581 assert!(form.flags.contains("RUNTIME"));
582 assert!(form.flags.contains("LIBRARY"));
583 assert!(form.flags.contains("ARCHIVE"));
584 }
585
586 #[test]
587 fn registry_knows_cmake_language_trace_form() {
588 let registry = CommandRegistry::load().unwrap();
589 let form = registry.get("cmake_language").form_for(Some("TRACE"));
590 assert!(form.flags.contains("ON"));
591 assert!(form.flags.contains("OFF"));
592 assert!(form.flags.contains("EXPAND"));
593 }
594
595 #[test]
596 fn registry_knows_cmake_pkg_config_import_keywords() {
597 let registry = CommandRegistry::load().unwrap();
598 let form = registry.get("cmake_pkg_config").form_for(Some("IMPORT"));
599 assert!(form.kwargs.contains_key("NAME"));
600 assert!(form.kwargs.contains_key("BIND_PC_REQUIRES"));
601 }
602
603 #[test]
604 fn registry_knows_file_archive_create_threads() {
605 let registry = CommandRegistry::load().unwrap();
606 let form = registry.get("file").form_for(Some("ARCHIVE_CREATE"));
607 assert!(form.kwargs.contains_key("THREADS"));
608 assert!(form.kwargs.contains_key("COMPRESSION_LEVEL"));
609 }
610
611 #[test]
612 fn registry_knows_file_strings_keywords() {
613 let registry = CommandRegistry::load().unwrap();
614 let form = registry.get("file").form_for(Some("STRINGS"));
615 assert_eq!(form.pargs, NArgs::Fixed(2));
616 assert!(form.kwargs.contains_key("REGEX"));
617 assert!(form.kwargs.contains_key("LIMIT_COUNT"));
618 }
619
620 #[test]
621 fn registry_knows_cmake_package_config_helpers_commands() {
622 let registry = CommandRegistry::load().unwrap();
623 let configure = registry.get("configure_package_config_file").form_for(None);
624 assert!(configure.kwargs.contains_key("INSTALL_DESTINATION"));
625 assert!(configure.kwargs.contains_key("PATH_VARS"));
626
627 let version = registry
628 .get("write_basic_package_version_file")
629 .form_for(None);
630 assert!(version.kwargs.contains_key("COMPATIBILITY"));
631 assert!(version.kwargs.contains_key("VERSION"));
632 }
633
634 #[test]
635 fn registry_knows_utility_module_commands() {
636 let registry = CommandRegistry::load().unwrap();
637 assert_eq!(
638 registry.get("cmake_dependent_option").form_for(None).pargs,
639 NArgs::Fixed(5)
640 );
641 assert_eq!(
642 registry.get("check_language").form_for(None).pargs,
643 NArgs::Fixed(1)
644 );
645 assert_eq!(
646 registry.get("check_include_file").form_for(None).pargs,
647 NArgs::AtLeast(2)
648 );
649 assert_eq!(
650 registry.get("check_compiler_flag").form_for(None).pargs,
651 NArgs::Fixed(3)
652 );
653 assert_eq!(
654 registry
655 .get("check_objc_compiler_flag")
656 .form_for(None)
657 .pargs,
658 NArgs::Fixed(2)
659 );
660 assert_eq!(
661 registry.get("check_cxx_symbol_exists").form_for(None).pargs,
662 NArgs::Fixed(3)
663 );
664 assert!(registry
665 .get("cmake_push_check_state")
666 .form_for(None)
667 .flags
668 .contains("RESET"));
669 let print_props = registry.get("cmake_print_properties").form_for(None);
670 assert!(print_props.kwargs.contains_key("TARGETS"));
671 assert!(print_props.kwargs.contains_key("PROPERTIES"));
672 let pie = registry.get("check_pie_supported").form_for(None);
673 assert!(pie.kwargs.contains_key("OUTPUT_VARIABLE"));
674 assert!(pie.kwargs.contains_key("LANGUAGES"));
675 let source_compiles = registry.get("check_source_compiles").form_for(None);
676 assert!(source_compiles.kwargs.contains_key("SRC_EXT"));
677 assert!(source_compiles.kwargs.contains_key("FAIL_REGEX"));
678 let find_dependency = registry.get("find_dependency").form_for(None);
679 assert!(find_dependency.flags.contains("REQUIRED"));
680 assert!(find_dependency.kwargs.contains_key("COMPONENTS"));
681 }
682
683 #[test]
684 fn registry_knows_supported_deprecated_module_commands() {
685 let registry = CommandRegistry::load().unwrap();
686 let version = registry
687 .get("write_basic_config_version_file")
688 .form_for(None);
689 assert_eq!(version.pargs, NArgs::Fixed(1));
690 assert!(version.kwargs.contains_key("COMPATIBILITY"));
691 assert!(version.flags.contains("ARCH_INDEPENDENT"));
692 assert_eq!(
693 registry.get("check_cxx_accepts_flag").form_for(None).pargs,
694 NArgs::Fixed(2)
695 );
696 }
697
698 #[test]
699 fn registry_knows_fetchcontent_commands() {
700 let registry = CommandRegistry::load().unwrap();
701 let declare = registry.get("fetchcontent_declare").form_for(None);
702 assert_eq!(declare.pargs, NArgs::Fixed(1));
703 assert!(declare.flags.contains("EXCLUDE_FROM_ALL"));
704 assert!(declare.kwargs.contains_key("FIND_PACKAGE_ARGS"));
705
706 let get_properties = registry.get("fetchcontent_getproperties").form_for(None);
707 assert!(get_properties.kwargs.contains_key("SOURCE_DIR"));
708 assert!(get_properties.kwargs.contains_key("BINARY_DIR"));
709 assert!(get_properties.kwargs.contains_key("POPULATED"));
710
711 let populate = registry.get("fetchcontent_populate").form_for(None);
712 assert!(populate.flags.contains("QUIET"));
713 assert!(populate.kwargs.contains_key("SUBBUILD_DIR"));
714 }
715
716 #[test]
717 fn registry_knows_common_test_and_package_helper_modules() {
718 let registry = CommandRegistry::load().unwrap();
719
720 let google_add = registry.get("gtest_add_tests").form_for(None);
721 assert!(google_add.kwargs.contains_key("TARGET"));
722 assert!(google_add.kwargs.contains_key("SOURCES"));
723 assert!(google_add.flags.contains("SKIP_DEPENDENCY"));
724
725 let google_discover = registry.get("gtest_discover_tests").form_for(None);
726 assert!(google_discover.kwargs.contains_key("DISCOVERY_MODE"));
727 assert!(google_discover.kwargs.contains_key("XML_OUTPUT_DIR"));
728 assert!(google_discover.flags.contains("NO_PRETTY_TYPES"));
729
730 assert_eq!(
731 registry.get("processorcount").form_for(None).pargs,
732 NArgs::Fixed(1)
733 );
734
735 let fp_hsa = registry
736 .get("find_package_handle_standard_args")
737 .form_for(None);
738 assert!(fp_hsa.flags.contains("DEFAULT_MSG"));
739 assert!(fp_hsa.kwargs.contains_key("REQUIRED_VARS"));
740 assert!(fp_hsa.kwargs.contains_key("VERSION_VAR"));
741
742 let fp_check = registry.get("find_package_check_version").form_for(None);
743 assert_eq!(fp_check.pargs, NArgs::Fixed(2));
744 assert!(fp_check.flags.contains("HANDLE_VERSION_RANGE"));
745 }
746
747 #[test]
748 fn registry_knows_externalproject_helper_commands() {
749 let registry = CommandRegistry::load().unwrap();
750 let step = registry.get("externalproject_add_step").form_for(None);
751 assert_eq!(step.pargs, NArgs::Fixed(2));
752 assert!(step.kwargs.contains_key("COMMAND"));
753 assert!(step.kwargs.contains_key("DEPENDEES"));
754 assert!(step.kwargs.contains_key("ENVIRONMENT_MODIFICATION"));
755
756 let targets = registry
757 .get("externalproject_add_steptargets")
758 .form_for(None);
759 assert_eq!(targets.pargs, NArgs::AtLeast(2));
760 assert!(targets.flags.contains("NO_DEPENDS"));
761
762 let deps = registry
763 .get("externalproject_add_stepdependencies")
764 .form_for(None);
765 assert_eq!(deps.pargs, NArgs::AtLeast(3));
766
767 let props = registry.get("externalproject_get_property").form_for(None);
768 assert_eq!(props.pargs, NArgs::AtLeast(2));
769 }
770
771 #[test]
772 fn registry_knows_packaging_and_find_helper_module_commands() {
773 let registry = CommandRegistry::load().unwrap();
774
775 assert_eq!(
776 registry.get("find_package_message").form_for(None).pargs,
777 NArgs::Fixed(3)
778 );
779 assert_eq!(
780 registry
781 .get("select_library_configurations")
782 .form_for(None)
783 .pargs,
784 NArgs::Fixed(1)
785 );
786
787 let component = registry.get("cpack_add_component").form_for(None);
788 assert!(component.flags.contains("HIDDEN"));
789 assert!(component.kwargs.contains_key("DISPLAY_NAME"));
790 assert!(component.kwargs.contains_key("DEPENDS"));
791
792 let group = registry.get("cpack_add_component_group").form_for(None);
793 assert!(group.flags.contains("EXPANDED"));
794 assert!(group.kwargs.contains_key("PARENT_GROUP"));
795
796 let downloads = registry.get("cpack_configure_downloads").form_for(None);
797 assert_eq!(downloads.pargs, NArgs::Fixed(1));
798 assert!(downloads.kwargs.contains_key("UPLOAD_DIRECTORY"));
799 }
800
801 #[test]
802 fn registry_knows_export_header_module_commands() {
803 let registry = CommandRegistry::load().unwrap();
804 let export_header = registry.get("generate_export_header").form_for(None);
805 assert_eq!(export_header.pargs, NArgs::Fixed(1));
806 assert!(export_header.flags.contains("DEFINE_NO_DEPRECATED"));
807 assert!(export_header.kwargs.contains_key("EXPORT_FILE_NAME"));
808 assert!(export_header.kwargs.contains_key("PREFIX_NAME"));
809
810 assert_eq!(
811 registry
812 .get("add_compiler_export_flags")
813 .form_for(None)
814 .pargs,
815 NArgs::Optional
816 );
817 }
818
819 #[test]
820 fn registry_knows_remaining_utility_module_commands() {
821 let registry = CommandRegistry::load().unwrap();
822
823 for command in [
824 "android_add_test_data",
825 "add_file_dependencies",
826 "cmake_add_fortran_subdirectory",
827 "cmake_expand_imported_targets",
828 "cmake_force_c_compiler",
829 "cmake_force_cxx_compiler",
830 "cmake_force_fortran_compiler",
831 "ctest_coverage_collect_gcov",
832 "copy_and_fixup_bundle",
833 "fixup_bundle",
834 "fixup_bundle_item",
835 "verify_app",
836 "verify_bundle_prerequisites",
837 "verify_bundle_symlinks",
838 "get_bundle_main_executable",
839 "get_dotapp_dir",
840 "get_bundle_and_executable",
841 "get_bundle_all_executables",
842 "get_bundle_keys",
843 "get_item_key",
844 "get_item_rpaths",
845 "clear_bundle_keys",
846 "set_bundle_key_values",
847 "copy_resolved_framework_into_bundle",
848 "copy_resolved_item_into_bundle",
849 "cpack_ifw_add_package_resources",
850 "cpack_ifw_add_repository",
851 "cpack_ifw_configure_component",
852 "cpack_ifw_configure_component_group",
853 "cpack_ifw_update_repository",
854 "cpack_ifw_configure_file",
855 "csharp_set_windows_forms_properties",
856 "csharp_set_designer_cs_properties",
857 "csharp_set_xaml_cs_properties",
858 "csharp_get_filename_keys",
859 "csharp_get_filename_key_base",
860 "csharp_get_dependentupon_name",
861 "externaldata_expand_arguments",
862 "externaldata_add_test",
863 "externaldata_add_target",
864 "fortrancinterface_header",
865 "fortrancinterface_verify",
866 "fetchcontent_setpopulated",
867 "gnuinstalldirs_get_absolute_install_dir",
868 "find_jar",
869 "add_jar",
870 "install_jar",
871 "install_jar_exports",
872 "export_jars",
873 "create_javadoc",
874 "create_javah",
875 "install_jni_symlink",
876 "swig_add_library",
877 "swig_link_libraries",
878 "print_enabled_features",
879 "print_disabled_features",
880 "set_feature_info",
881 "set_package_info",
882 ] {
883 assert!(
884 registry.contains_builtin(command),
885 "missing built-in {command}"
886 );
887 }
888
889 assert_eq!(
890 registry
891 .get("ctest_coverage_collect_gcov")
892 .form_for(None)
893 .pargs,
894 NArgs::ZeroOrMore
895 );
896 assert_eq!(
897 registry
898 .get("fortrancinterface_verify")
899 .form_for(None)
900 .pargs,
901 NArgs::ZeroOrMore
902 );
903 assert_eq!(
904 registry.get("add_jar").form_for(None).pargs,
905 NArgs::AtLeast(2)
906 );
907 assert_eq!(
908 registry
909 .get("cpack_ifw_configure_file")
910 .form_for(None)
911 .pargs,
912 NArgs::Fixed(2)
913 );
914 assert_eq!(
915 registry
916 .get("gnuinstalldirs_get_absolute_install_dir")
917 .form_for(None)
918 .pargs,
919 NArgs::AtLeast(3)
920 );
921 }
922
923 #[test]
924 fn registry_knows_string_json_43_modes() {
925 let registry = CommandRegistry::load().unwrap();
926 let form = registry.get("string").form_for(Some("JSON"));
927 assert!(form.flags.contains("GET_RAW"));
928 assert!(form.flags.contains("STRING_ENCODE"));
929 assert!(form.kwargs.contains_key("ERROR_VARIABLE"));
930 }
931
932 #[test]
933 fn user_override_entries_merge_with_builtins() {
934 let mut registry = CommandRegistry::load().unwrap();
935 let overrides = r#"
936[commands.target_link_libraries.layout]
937always_wrap = true
938
939[commands.target_link_libraries.kwargs.LINKER_LANGUAGE]
940nargs = 1
941"#;
942
943 registry
944 .merge_override_str(overrides, PathBuf::from("test-overrides.toml"))
945 .unwrap();
946
947 let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
948 panic!()
949 };
950 assert_eq!(
951 form.layout.as_ref().and_then(|layout| layout.always_wrap),
952 Some(true)
953 );
954 assert!(form.kwargs.contains_key("PUBLIC"));
955 assert_eq!(form.kwargs["LINKER_LANGUAGE"].nargs, NArgs::Fixed(1));
956 }
957
958 #[test]
959 fn uppercase_lookup_uses_builtin_normalization() {
960 let registry = CommandRegistry::load().unwrap();
961 assert!(registry.contains_builtin("TARGET_LINK_LIBRARIES"));
962 let CommandSpec::Single(form) = registry.get("TARGET_LINK_LIBRARIES") else {
963 panic!()
964 };
965 assert!(form.kwargs.contains_key("PUBLIC"));
966 assert!(form.kwargs.contains_key("PRIVATE"));
967 }
968
969 #[test]
970 fn from_builtins_and_yaml_override_file_merges_entries() {
971 let dir = tempfile::tempdir().unwrap();
972 let overrides = dir.path().join("override.yaml");
973 fs::write(
974 &overrides,
975 r#"
976commands:
977 target_link_libraries:
978 kwargs:
979 linker_language:
980 nargs: 1
981"#,
982 )
983 .unwrap();
984
985 let registry = CommandRegistry::from_builtins_and_overrides(Some(&overrides)).unwrap();
986 let CommandSpec::Single(form) = registry.get("target_link_libraries") else {
987 panic!()
988 };
989 assert_eq!(form.kwargs["LINKER_LANGUAGE"].nargs, NArgs::Fixed(1));
990 }
991
992 #[test]
993 fn merge_override_file_reports_structured_toml_parse_errors() {
994 let mut registry = CommandRegistry::load().unwrap();
995 let dir = tempfile::tempdir().unwrap();
996 let path = dir.path().join("override.toml");
997 fs::write(&path, "[commands.bad]\npargs = [\n").unwrap();
998
999 let err = registry.merge_override_file(&path).unwrap_err();
1000 match err {
1001 Error::Spec { details, .. } => {
1002 assert_eq!(details.format, "TOML");
1003 assert!(details.line.is_some());
1004 assert!(details.column.is_some());
1005 }
1006 other => panic!("expected spec parse error, got {other:?}"),
1007 }
1008 }
1009
1010 #[test]
1011 fn merge_override_file_reports_structured_yaml_parse_errors() {
1012 let mut registry = CommandRegistry::load().unwrap();
1013 let dir = tempfile::tempdir().unwrap();
1014 let path = dir.path().join("override.yaml");
1015 fs::write(&path, "commands:\n target_link_libraries: [\n").unwrap();
1016
1017 let err = registry.merge_override_file(&path).unwrap_err();
1018 match err {
1019 Error::Spec { details, .. } => {
1020 assert_eq!(details.format, "YAML");
1021 assert!(details.line.is_some());
1022 assert!(details.column.is_some());
1023 }
1024 other => panic!("expected spec parse error, got {other:?}"),
1025 }
1026 }
1027
1028 #[test]
1029 fn override_with_mismatched_shape_replaces_base_command_spec() {
1030 let mut registry = CommandRegistry::load().unwrap();
1031 registry
1032 .merge_override_str(
1033 r#"
1034[commands.cmake_minimum_required.forms.VERSION]
1035pargs = 1
1036"#,
1037 PathBuf::from("override.toml"),
1038 )
1039 .unwrap();
1040
1041 let CommandSpec::Discriminated { .. } = registry.get("cmake_minimum_required") else {
1042 panic!("expected discriminated command after mismatched override")
1043 };
1044 assert_eq!(
1045 registry
1046 .get("cmake_minimum_required")
1047 .form_for(Some("VERSION"))
1048 .pargs,
1049 NArgs::Fixed(1)
1050 );
1051 }
1052}