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