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