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