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