1use core::fmt::Display;
2use std::{collections::HashMap, env, fmt, fs, io::Write, path::PathBuf};
3
4use evalexpr::{ContextWithMutableFunctions, ContextWithMutableVariables};
5use serde::{Deserialize, Serialize};
6
7use crate::generate::{
8 evalexpr_extensions::{I128, I128NumericTypes},
9 validator::Validator,
10 value::Value,
11};
12
13pub(crate) mod evalexpr_extensions;
14mod markdown;
15pub(crate) mod validator;
16pub(crate) mod value;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum Error {
21 Parse(String),
23 Validation(String),
25}
26
27impl Error {
28 pub fn parse<S>(message: S) -> Self
30 where
31 S: Into<String>,
32 {
33 Self::Parse(message.into())
34 }
35
36 pub fn validation<S>(message: S) -> Self
38 where
39 S: Into<String>,
40 {
41 Self::Validation(message.into())
42 }
43}
44
45impl fmt::Display for Error {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self {
48 Error::Parse(message) => write!(f, "{message}"),
49 Error::Validation(message) => write!(f, "{message}"),
50 }
51 }
52}
53
54#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
56#[serde(deny_unknown_fields)]
57pub struct Config {
58 #[serde(rename = "crate")]
60 pub krate: String,
61 pub options: Vec<CfgOption>,
63 pub checks: Option<Vec<String>>,
65}
66
67fn true_default() -> String {
68 "true".to_string()
69}
70
71fn unstable_default() -> Stability {
72 Stability::Unstable
73}
74
75#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
77#[serde(deny_unknown_fields)]
78pub struct CfgDefaultValue {
79 #[serde(rename = "if")]
82 #[serde(default = "true_default")]
83 pub if_: String,
84 pub value: Value,
86}
87
88#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
90#[serde(deny_unknown_fields)]
91pub struct CfgOption {
92 pub name: String,
94 pub description: String,
97 #[serde(default = "true_default")]
99 pub active: String,
100 pub default: Vec<CfgDefaultValue>,
103 pub constraints: Option<Vec<CfgConstraint>>,
106 pub display_hint: Option<DisplayHint>,
109 #[serde(default = "unstable_default")]
111 pub stability: Stability,
112}
113
114#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
116#[serde(deny_unknown_fields)]
117pub struct CfgConstraint {
118 #[serde(rename = "if")]
120 #[serde(default = "true_default")]
121 if_: String,
122 #[serde(rename = "type")]
124 type_: Validator,
125}
126
127pub fn generate_config_from_yaml_definition(
132 yaml: &str,
133 enable_unstable: bool,
134 emit_md_tables: bool,
135 chip: Option<esp_metadata_generated::Chip>,
136) -> Result<HashMap<String, Value>, Error> {
137 let features: Vec<String> = env::vars()
138 .filter(|(k, _)| k.starts_with("CARGO_FEATURE_"))
139 .map(|(k, _)| k)
140 .map(|v| {
141 v.strip_prefix("CARGO_FEATURE_")
142 .unwrap_or_default()
143 .to_string()
144 })
145 .collect();
146
147 let (config, options) = evaluate_yaml_config(yaml, chip, features, false)?;
148
149 let cfg = generate_config(&config.krate, &options, enable_unstable, emit_md_tables);
150
151 do_checks(config.checks.as_ref(), &cfg)?;
152
153 Ok(cfg)
154}
155
156pub fn do_checks(checks: Option<&Vec<String>>, cfg: &HashMap<String, Value>) -> Result<(), Error> {
158 if let Some(checks) = checks {
159 let mut eval_ctx = evalexpr::HashMapContext::<I128NumericTypes>::new();
160 for (k, v) in cfg.iter() {
161 eval_ctx
162 .set_value(
163 k.clone(),
164 match v {
165 Value::Bool(v) => evalexpr::Value::Boolean(*v),
166 Value::Integer(v) => evalexpr::Value::Int(I128(*v)),
167 Value::String(v) => evalexpr::Value::String(v.clone()),
168 },
169 )
170 .map_err(|err| Error::Parse(format!("Error setting value for {k} ({err})")))?;
171 }
172 for check in checks {
173 if !evalexpr::eval_with_context(check, &eval_ctx)
174 .and_then(|v| v.as_boolean())
175 .map_err(|err| Error::Validation(format!("Validation error: '{check}' ({err})")))?
176 {
177 return Err(Error::Validation(format!("Validation error: '{check}'")));
178 }
179 }
180 };
181 Ok(())
182}
183
184pub fn evaluate_yaml_config(
186 yaml: &str,
187 chip: Option<esp_metadata_generated::Chip>,
188 features: Vec<String>,
189 ignore_feature_gates: bool,
190) -> Result<(Config, Vec<ConfigOption>), Error> {
191 let config: Config = serde_yaml::from_str(yaml).map_err(|err| Error::Parse(err.to_string()))?;
192 let mut options = Vec::new();
193 let mut eval_ctx = evalexpr::HashMapContext::<evalexpr::DefaultNumericTypes>::new();
194 if let Some(chip) = chip {
195 eval_ctx
196 .set_value(
197 "chip".into(),
198 evalexpr::Value::String(chip.name().to_string()),
199 )
200 .map_err(|err| Error::Parse(err.to_string()))?;
201
202 eval_ctx
203 .set_function(
204 "feature".into(),
205 evalexpr::Function::<evalexpr::DefaultNumericTypes>::new(move |arg| {
206 if let evalexpr::Value::String(which) = arg {
207 let res = chip.contains(which);
208 Ok(evalexpr::Value::Boolean(res))
209 } else {
210 Err(evalexpr::EvalexprError::CustomMessage(format!(
211 "Bad argument: {arg:?}"
212 )))
213 }
214 }),
215 )
216 .map_err(|err| Error::Parse(err.to_string()))?;
217
218 eval_ctx
219 .set_function(
220 "cargo_feature".into(),
221 evalexpr::Function::<evalexpr::DefaultNumericTypes>::new(move |arg| {
222 if let evalexpr::Value::String(which) = arg {
223 let res = features.contains(&which.to_uppercase().replace("-", "_"));
224 Ok(evalexpr::Value::Boolean(res))
225 } else {
226 Err(evalexpr::EvalexprError::CustomMessage(format!(
227 "Bad argument: {arg:?}"
228 )))
229 }
230 }),
231 )
232 .map_err(|err| Error::Parse(err.to_string()))?;
233
234 eval_ctx
235 .set_function(
236 "ignore_feature_gates".into(),
237 evalexpr::Function::<evalexpr::DefaultNumericTypes>::new(move |arg| {
238 if let evalexpr::Value::Empty = arg {
239 Ok(evalexpr::Value::Boolean(ignore_feature_gates))
240 } else {
241 Err(evalexpr::EvalexprError::CustomMessage(format!(
242 "Bad argument: {arg:?}"
243 )))
244 }
245 }),
246 )
247 .map_err(|err| Error::Parse(err.to_string()))?;
248 }
249 for option in config.options.clone() {
250 let active = evalexpr::eval_with_context(&option.active, &eval_ctx)
251 .map_err(|err| {
252 Error::Parse(format!(
253 "Error evaluating '{}', error = {:?}",
254 option.active, err
255 ))
256 })?
257 .as_boolean()
258 .map_err(|err| {
259 Error::Parse(format!(
260 "Error evaluating '{}', error = {:?}",
261 option.active, err
262 ))
263 })?;
264
265 let constraint = {
266 let mut active_constraint = None;
267 if let Some(constraints) = &option.constraints {
268 for constraint in constraints {
269 if evalexpr::eval_with_context(&constraint.if_, &eval_ctx)
270 .map_err(|err| {
271 Error::Parse(format!(
272 "Error evaluating '{}', error = {err:?}",
273 constraint.if_
274 ))
275 })?
276 .as_boolean()
277 .map_err(|err| {
278 Error::Parse(format!(
279 "Error evaluating '{}', error = {:?}",
280 constraint.if_, err
281 ))
282 })?
283 {
284 active_constraint = Some(constraint.type_.clone());
285 break;
286 }
287 }
288 };
289
290 if option.constraints.is_some() && active_constraint.is_none() {
291 panic!(
292 "No constraint active for crate {}, option {}",
293 config.krate, option.name
294 );
295 }
296
297 active_constraint
298 };
299
300 let default_value = {
301 let mut default_value = None;
302 for value in option.default.clone() {
303 if evalexpr::eval_with_context(&value.if_, &eval_ctx)
304 .and_then(|v| v.as_boolean())
305 .map_err(|err| {
306 Error::Parse(format!("Error evaluating '{}', error = {err:?}", value.if_))
307 })?
308 {
309 default_value = Some(value.value);
310 break;
311 }
312 }
313
314 if default_value.is_none() {
315 panic!(
316 "No default value active for crate {}, option {}",
317 config.krate, option.name
318 );
319 }
320
321 default_value
322 };
323
324 let option = ConfigOption {
325 name: option.name.clone(),
326 description: option.description,
327 default_value: default_value.ok_or(Error::Parse(format!(
328 "No default value found for {}",
329 option.name
330 )))?,
331 constraint,
332 stability: option.stability,
333 active,
334 display_hint: option.display_hint.unwrap_or(DisplayHint::None),
335 };
336 options.push(option);
337 }
338 Ok((config, options))
339}
340
341pub fn generate_config(
364 crate_name: &str,
365 config: &[ConfigOption],
366 enable_unstable: bool,
367 emit_md_tables: bool,
368) -> HashMap<String, Value> {
369 let configs = generate_config_internal(std::io::stdout(), crate_name, config, enable_unstable);
370
371 if emit_md_tables {
372 let file_name = snake_case(crate_name);
373
374 let mut doc_table = markdown::DOC_TABLE_HEADER.replace(
375 "{prefix}",
376 format!("{}_CONFIG_*", screaming_snake_case(crate_name)).as_str(),
377 );
378 let mut selected_config = String::from(markdown::SELECTED_TABLE_HEADER);
379
380 for (name, option, value) in configs.iter() {
381 if !option.active {
382 continue;
383 }
384 markdown::write_doc_table_line(&mut doc_table, name, option);
385 markdown::write_summary_table_line(&mut selected_config, name, value);
386 }
387
388 write_out_file(format!("{file_name}_config_table.md"), doc_table);
389 write_out_file(format!("{file_name}_selected_config.md"), selected_config);
390 }
391
392 configs.into_iter().map(|(k, _, v)| (k, v)).collect()
394}
395
396pub fn generate_config_internal<'a>(
397 mut stdout: impl Write,
398 crate_name: &str,
399 config: &'a [ConfigOption],
400 enable_unstable: bool,
401) -> Vec<(String, &'a ConfigOption, Value)> {
402 writeln!(stdout, "cargo:rerun-if-changed=build.rs").ok();
405
406 let prefix = format!("{}_CONFIG_", screaming_snake_case(crate_name));
408
409 let mut configs = create_config(&prefix, config);
410 capture_from_env(crate_name, &prefix, &mut configs, enable_unstable);
411
412 for (_, option, value) in configs.iter() {
413 if let Some(ref validator) = option.constraint {
414 validator.validate(value).unwrap();
415 }
416 }
417
418 emit_configuration(&mut stdout, &configs);
419
420 configs
421}
422
423#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
425pub enum Stability {
426 Unstable,
429 Stable(String),
432}
433
434impl Display for Stability {
435 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436 match self {
437 Stability::Unstable => write!(f, "⚠️ Unstable"),
438 Stability::Stable(version) => write!(f, "Stable since {version}"),
439 }
440 }
441}
442
443#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
445pub enum DisplayHint {
446 None,
448
449 Binary,
451
452 Hex,
454
455 Octal,
457}
458
459impl DisplayHint {
460 pub fn format_value(self, value: &Value) -> String {
462 match value {
463 Value::Bool(b) => b.to_string(),
464 Value::Integer(i) => match self {
465 DisplayHint::None => format!("{i}"),
466 DisplayHint::Binary => format!("0b{i:0b}"),
467 DisplayHint::Hex => format!("0x{i:X}"),
468 DisplayHint::Octal => format!("0o{i:o}"),
469 },
470 Value::String(s) => s.clone(),
471 }
472 }
473}
474
475#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
477pub struct ConfigOption {
478 pub name: String,
483
484 pub description: String,
489
490 pub default_value: Value,
492
493 pub constraint: Option<Validator>,
495
496 pub stability: Stability,
498
499 pub active: bool,
504
505 pub display_hint: DisplayHint,
507}
508
509impl ConfigOption {
510 pub fn full_env_var(&self, crate_name: &str) -> String {
512 self.env_var(&format!("{}_CONFIG_", screaming_snake_case(crate_name)))
513 }
514
515 fn env_var(&self, prefix: &str) -> String {
516 format!("{}{}", prefix, screaming_snake_case(&self.name))
517 }
518
519 fn cfg_name(&self) -> String {
520 snake_case(&self.name)
521 }
522
523 fn is_stable(&self) -> bool {
524 matches!(self.stability, Stability::Stable(_))
525 }
526}
527
528fn create_config<'a>(
529 prefix: &str,
530 config: &'a [ConfigOption],
531) -> Vec<(String, &'a ConfigOption, Value)> {
532 let mut configs = Vec::with_capacity(config.len());
533
534 for option in config {
535 configs.push((option.env_var(prefix), option, option.default_value.clone()));
536 }
537
538 configs
539}
540
541fn capture_from_env(
542 crate_name: &str,
543 prefix: &str,
544 configs: &mut Vec<(String, &ConfigOption, Value)>,
545 enable_unstable: bool,
546) {
547 let mut unknown = Vec::new();
548 let mut failed = Vec::new();
549 let mut unstable = Vec::new();
550
551 for (var, value) in env::vars() {
553 if var.starts_with(prefix) {
554 let Some((_, option, cfg)) = configs.iter_mut().find(|(k, _, _)| k == &var) else {
555 unknown.push(var);
556 continue;
557 };
558
559 if !option.active {
560 unknown.push(var);
561 continue;
562 }
563
564 if !enable_unstable && !option.is_stable() {
565 unstable.push(var);
566 continue;
567 }
568
569 if let Err(e) = cfg.parse_in_place(&value) {
570 failed.push(format!("{var}: {e}"));
571 }
572 }
573 }
574
575 if !failed.is_empty() {
576 panic!("Invalid configuration options detected: {failed:?}");
577 }
578
579 if !unstable.is_empty() {
580 panic!(
581 "The following configuration options are unstable: {unstable:?}. You can enable it by \
582 activating the 'unstable' feature in {crate_name}."
583 );
584 }
585
586 if !unknown.is_empty() {
587 panic!("Unknown configuration options detected: {unknown:?}");
588 }
589}
590
591fn emit_configuration(mut stdout: impl Write, configs: &[(String, &ConfigOption, Value)]) {
592 for (env_var_name, option, value) in configs.iter() {
593 let cfg_name = option.cfg_name();
594
595 writeln!(stdout, "cargo:rustc-env={env_var_name}={value}").ok();
599 writeln!(stdout, "cargo:rerun-if-env-changed={env_var_name}").ok();
600
601 writeln!(stdout, "cargo:rustc-check-cfg=cfg({cfg_name})").ok();
603
604 if let Value::Bool(true) = value {
606 writeln!(stdout, "cargo:rustc-cfg={cfg_name}").ok();
607 }
608
609 if let Some(validator) = option.constraint.as_ref() {
611 validator.emit_cargo_extras(&mut stdout, &cfg_name, value);
612 }
613 }
614}
615
616fn write_out_file(file_name: String, json: String) {
617 let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
618 let out_file = out_dir.join(file_name);
619 fs::write(out_file, json).unwrap();
620}
621
622fn snake_case(name: &str) -> String {
623 let mut name = name.replace("-", "_");
624 name.make_ascii_lowercase();
625
626 name
627}
628
629fn screaming_snake_case(name: &str) -> String {
630 let mut name = name.replace("-", "_");
631 name.make_ascii_uppercase();
632
633 name
634}
635
636#[cfg(test)]
637mod test {
638 use super::*;
639 use crate::generate::{validator::Validator, value::Value};
640
641 #[test]
642 fn value_number_formats() {
643 const INPUTS: &[&str] = &["0xAA", "0o252", "0b0000000010101010", "170"];
644 let mut v = Value::Integer(0);
645
646 for input in INPUTS {
647 v.parse_in_place(input).unwrap();
648 assert_eq!(v.to_string(), "170");
650 }
651 }
652
653 #[test]
654 fn value_bool_inputs() {
655 let mut v = Value::Bool(false);
656
657 v.parse_in_place("true").unwrap();
658 assert_eq!(v.to_string(), "true");
659
660 v.parse_in_place("false").unwrap();
661 assert_eq!(v.to_string(), "false");
662
663 v.parse_in_place("else")
664 .expect_err("Only true or false are valid");
665 }
666
667 #[test]
668 fn env_override() {
669 temp_env::with_vars(
670 [
671 ("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
672 ("ESP_TEST_CONFIG_NUMBER_SIGNED", Some("-999")),
673 ("ESP_TEST_CONFIG_STRING", Some("Hello world!")),
674 ("ESP_TEST_CONFIG_BOOL", Some("true")),
675 ],
676 || {
677 let configs = generate_config(
678 "esp-test",
679 &[
680 ConfigOption {
681 name: String::from("number"),
682 description: String::from("NA"),
683 default_value: Value::Integer(999),
684 constraint: None,
685 stability: Stability::Stable(String::from("testing")),
686 active: true,
687 display_hint: DisplayHint::None,
688 },
689 ConfigOption {
690 name: String::from("number_signed"),
691 description: String::from("NA"),
692 default_value: Value::Integer(-777),
693 constraint: None,
694 stability: Stability::Stable(String::from("testing")),
695 active: true,
696 display_hint: DisplayHint::None,
697 },
698 ConfigOption {
699 name: String::from("string"),
700 description: String::from("NA"),
701 default_value: Value::String("Demo".to_string()),
702 constraint: None,
703 stability: Stability::Stable(String::from("testing")),
704 active: true,
705 display_hint: DisplayHint::None,
706 },
707 ConfigOption {
708 name: String::from("bool"),
709 description: String::from("NA"),
710 default_value: Value::Bool(false),
711 constraint: None,
712 stability: Stability::Stable(String::from("testing")),
713 active: true,
714 display_hint: DisplayHint::None,
715 },
716 ConfigOption {
717 name: String::from("number_default"),
718 description: String::from("NA"),
719 default_value: Value::Integer(999),
720 constraint: None,
721 stability: Stability::Stable(String::from("testing")),
722 active: true,
723 display_hint: DisplayHint::None,
724 },
725 ConfigOption {
726 name: String::from("string_default"),
727 description: String::from("NA"),
728 default_value: Value::String("Demo".to_string()),
729 constraint: None,
730 stability: Stability::Stable(String::from("testing")),
731 active: true,
732 display_hint: DisplayHint::None,
733 },
734 ConfigOption {
735 name: String::from("bool_default"),
736 description: String::from("NA"),
737 default_value: Value::Bool(false),
738 constraint: None,
739 stability: Stability::Stable(String::from("testing")),
740 active: true,
741 display_hint: DisplayHint::None,
742 },
743 ],
744 false,
745 false,
746 );
747
748 assert_eq!(configs["ESP_TEST_CONFIG_NUMBER"], Value::Integer(0xaa));
750 assert_eq!(
751 configs["ESP_TEST_CONFIG_NUMBER_SIGNED"],
752 Value::Integer(-999)
753 );
754 assert_eq!(
755 configs["ESP_TEST_CONFIG_STRING"],
756 Value::String("Hello world!".to_string())
757 );
758 assert_eq!(configs["ESP_TEST_CONFIG_BOOL"], Value::Bool(true));
759
760 assert_eq!(
762 configs["ESP_TEST_CONFIG_NUMBER_DEFAULT"],
763 Value::Integer(999)
764 );
765 assert_eq!(
766 configs["ESP_TEST_CONFIG_STRING_DEFAULT"],
767 Value::String("Demo".to_string())
768 );
769 assert_eq!(configs["ESP_TEST_CONFIG_BOOL_DEFAULT"], Value::Bool(false));
770 },
771 )
772 }
773
774 #[test]
775 fn builtin_validation_passes() {
776 temp_env::with_vars(
777 [
778 ("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("7")),
779 ("ESP_TEST_CONFIG_NEGATIVE_NUMBER", Some("-1")),
780 ("ESP_TEST_CONFIG_NON_NEGATIVE_NUMBER", Some("0")),
781 ("ESP_TEST_CONFIG_RANGE", Some("9")),
782 ],
783 || {
784 generate_config(
785 "esp-test",
786 &[
787 ConfigOption {
788 name: String::from("positive_number"),
789 description: String::from("NA"),
790 default_value: Value::Integer(-1),
791 constraint: Some(Validator::PositiveInteger),
792 stability: Stability::Stable(String::from("testing")),
793 active: true,
794 display_hint: DisplayHint::None,
795 },
796 ConfigOption {
797 name: String::from("negative_number"),
798 description: String::from("NA"),
799 default_value: Value::Integer(1),
800 constraint: Some(Validator::NegativeInteger),
801 stability: Stability::Stable(String::from("testing")),
802 active: true,
803 display_hint: DisplayHint::None,
804 },
805 ConfigOption {
806 name: String::from("non_negative_number"),
807 description: String::from("NA"),
808 default_value: Value::Integer(-1),
809 constraint: Some(Validator::NonNegativeInteger),
810 stability: Stability::Stable(String::from("testing")),
811 active: true,
812 display_hint: DisplayHint::None,
813 },
814 ConfigOption {
815 name: String::from("range"),
816 description: String::from("NA"),
817 default_value: Value::Integer(0),
818 constraint: Some(Validator::IntegerInRange(5..10)),
819 stability: Stability::Stable(String::from("testing")),
820 active: true,
821 display_hint: DisplayHint::None,
822 },
823 ],
824 false,
825 false,
826 )
827 },
828 );
829 }
830
831 #[test]
832 #[should_panic]
833 fn builtin_validation_bails() {
834 temp_env::with_vars([("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("-99"))], || {
835 generate_config(
836 "esp-test",
837 &[ConfigOption {
838 name: String::from("positive_number"),
839 description: String::from("NA"),
840 default_value: Value::Integer(-1),
841 constraint: Some(Validator::PositiveInteger),
842 stability: Stability::Stable(String::from("testing")),
843 active: true,
844 display_hint: DisplayHint::None,
845 }],
846 false,
847 false,
848 )
849 });
850 }
851
852 #[test]
853 #[should_panic]
854 fn env_unknown_bails() {
855 temp_env::with_vars(
856 [
857 ("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
858 ("ESP_TEST_CONFIG_RANDOM_VARIABLE", Some("")),
859 ],
860 || {
861 generate_config(
862 "esp-test",
863 &[ConfigOption {
864 name: String::from("number"),
865 description: String::from("NA"),
866 default_value: Value::Integer(999),
867 constraint: None,
868 stability: Stability::Stable(String::from("testing")),
869 active: true,
870 display_hint: DisplayHint::None,
871 }],
872 false,
873 false,
874 );
875 },
876 );
877 }
878
879 #[test]
880 #[should_panic]
881 fn env_invalid_values_bails() {
882 temp_env::with_vars([("ESP_TEST_CONFIG_NUMBER", Some("Hello world"))], || {
883 generate_config(
884 "esp-test",
885 &[ConfigOption {
886 name: String::from("number"),
887 description: String::from("NA"),
888 default_value: Value::Integer(999),
889 constraint: None,
890 stability: Stability::Stable(String::from("testing")),
891 active: true,
892 display_hint: DisplayHint::None,
893 }],
894 false,
895 false,
896 );
897 });
898 }
899
900 #[test]
901 fn env_unknown_prefix_is_ignored() {
902 temp_env::with_vars(
903 [("ESP_TEST_OTHER_CONFIG_NUMBER", Some("Hello world"))],
904 || {
905 generate_config(
906 "esp-test",
907 &[ConfigOption {
908 name: String::from("number"),
909 description: String::from("NA"),
910 default_value: Value::Integer(999),
911 constraint: None,
912 stability: Stability::Stable(String::from("testing")),
913 active: true,
914 display_hint: DisplayHint::None,
915 }],
916 false,
917 false,
918 );
919 },
920 );
921 }
922
923 #[test]
924 fn enumeration_validator() {
925 let mut stdout = Vec::new();
926 temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
927 generate_config_internal(
928 &mut stdout,
929 "esp-test",
930 &[ConfigOption {
931 name: String::from("some-key"),
932 description: String::from("NA"),
933 default_value: Value::String("variant-0".to_string()),
934 constraint: Some(Validator::Enumeration(vec![
935 "variant-0".to_string(),
936 "variant-1".to_string(),
937 ])),
938 stability: Stability::Stable(String::from("testing")),
939 active: true,
940 display_hint: DisplayHint::None,
941 }],
942 false,
943 );
944 });
945
946 let cargo_lines: Vec<&str> = std::str::from_utf8(&stdout).unwrap().lines().collect();
947 assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key)"));
948 assert!(cargo_lines.contains(&"cargo:rustc-env=ESP_TEST_CONFIG_SOME_KEY=variant-0"));
949 assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_0)"));
950 assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_1)"));
951 assert!(cargo_lines.contains(&"cargo:rustc-cfg=some_key_variant_0"));
952 }
953
954 #[test]
955 #[should_panic]
956 fn unstable_option_panics_unless_enabled() {
957 let mut stdout = Vec::new();
958 temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
959 generate_config_internal(
960 &mut stdout,
961 "esp-test",
962 &[ConfigOption {
963 name: String::from("some-key"),
964 description: String::from("NA"),
965 default_value: Value::String("variant-0".to_string()),
966 constraint: Some(Validator::Enumeration(vec![
967 "variant-0".to_string(),
968 "variant-1".to_string(),
969 ])),
970 stability: Stability::Unstable,
971 active: true,
972 display_hint: DisplayHint::None,
973 }],
974 false,
975 );
976 });
977 }
978
979 #[test]
980 #[should_panic]
981 fn inactive_option_panics() {
982 let mut stdout = Vec::new();
983 temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
984 generate_config_internal(
985 &mut stdout,
986 "esp-test",
987 &[ConfigOption {
988 name: String::from("some-key"),
989 description: String::from("NA"),
990 default_value: Value::String("variant-0".to_string()),
991 constraint: Some(Validator::Enumeration(vec![
992 "variant-0".to_string(),
993 "variant-1".to_string(),
994 ])),
995 stability: Stability::Stable(String::from("testing")),
996 active: false,
997 display_hint: DisplayHint::None,
998 }],
999 false,
1000 );
1001 });
1002 }
1003
1004 #[test]
1005 fn deserialization() {
1006 let yml = r#"
1007crate: esp-bootloader-esp-idf
1008
1009options:
1010- name: mmu_page_size
1011 description: ESP32-C2, ESP32-C6 and ESP32-H2 support configurable page sizes. This is currently only used to populate the app descriptor.
1012 default:
1013 - value: '"64k"'
1014 stability: !Stable xxxx
1015 constraints:
1016 - if: true
1017 type:
1018 validator: enumeration
1019 value:
1020 - 8k
1021 - 16k
1022 - 32k
1023 - 64k
1024
1025- name: esp_idf_version
1026 description: ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.
1027 default:
1028 - if: 'chip == "esp32c6"'
1029 value: '"esp32c6"'
1030 - if: 'chip == "esp32"'
1031 value: '"other"'
1032 active: true
1033
1034- name: partition-table-offset
1035 description: "The address of partition table (by default 0x8000). Allows you to \
1036 move the partition table, it gives more space for the bootloader. Note that the \
1037 bootloader and app will both need to be compiled with the same \
1038 PARTITION_TABLE_OFFSET value."
1039 default:
1040 - if: true
1041 value: 32768
1042 stability: Unstable
1043 active: 'chip == "esp32c6"'
1044"#;
1045
1046 let (cfg, options) = evaluate_yaml_config(
1047 yml,
1048 Some(esp_metadata_generated::Chip::Esp32c6),
1049 vec![],
1050 false,
1051 )
1052 .unwrap();
1053
1054 assert_eq!("esp-bootloader-esp-idf", cfg.krate);
1055
1056 assert_eq!(
1057 vec![
1058 ConfigOption {
1059 name: "mmu_page_size".to_string(),
1060 description: "ESP32-C2, ESP32-C6 and ESP32-H2 support configurable page sizes. This is currently only used to populate the app descriptor.".to_string(),
1061 default_value: Value::String("64k".to_string()),
1062 constraint: Some(
1063 Validator::Enumeration(
1064 vec![
1065 "8k".to_string(),
1066 "16k".to_string(),
1067 "32k".to_string(),
1068 "64k".to_string(),
1069 ],
1070 ),
1071 ),
1072 stability: Stability::Stable("xxxx".to_string()),
1073 active: true,
1074 display_hint: DisplayHint::None,
1075 },
1076 ConfigOption {
1077 name: "esp_idf_version".to_string(),
1078 description: "ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.".to_string(),
1079 default_value: Value::String("esp32c6".to_string()),
1080 constraint: None,
1081 stability: Stability::Unstable,
1082 active: true,
1083 display_hint: DisplayHint::None,
1084 },
1085 ConfigOption {
1086 name: "partition-table-offset".to_string(),
1087 description: "The address of partition table (by default 0x8000). Allows you to move the partition table, it gives more space for the bootloader. Note that the bootloader and app will both need to be compiled with the same PARTITION_TABLE_OFFSET value.".to_string(),
1088 default_value: Value::Integer(32768),
1089 constraint: None,
1090 stability: Stability::Unstable,
1091 active: true,
1092 display_hint: DisplayHint::None,
1093 },
1094 ],
1095 options
1096 );
1097 }
1098
1099 #[test]
1100 fn deserialization_fallback_default() {
1101 let yml = r#"
1102crate: esp-bootloader-esp-idf
1103
1104options:
1105- name: esp_idf_version
1106 description: ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.
1107 default:
1108 - if: 'chip == "esp32c6"'
1109 value: '"esp32c6"'
1110 - if: 'chip == "esp32"'
1111 value: '"other"'
1112 - value: '"default"'
1113 active: true
1114"#;
1115
1116 let (cfg, options) = evaluate_yaml_config(
1117 yml,
1118 Some(esp_metadata_generated::Chip::Esp32c3),
1119 vec![],
1120 false,
1121 )
1122 .unwrap();
1123
1124 assert_eq!("esp-bootloader-esp-idf", cfg.krate);
1125
1126 assert_eq!(
1127 vec![
1128 ConfigOption {
1129 name: "esp_idf_version".to_string(),
1130 description: "ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.".to_string(),
1131 default_value: Value::String("default".to_string()),
1132 constraint: None,
1133 stability: Stability::Unstable,
1134 active: true,
1135 display_hint: DisplayHint::None,
1136 },
1137 ],
1138 options
1139 );
1140 }
1141
1142 #[test]
1143 fn deserialization_fallback_contraint() {
1144 let yml = r#"
1145crate: esp-bootloader-esp-idf
1146
1147options:
1148- name: option
1149 description: Desc
1150 default:
1151 - value: 100
1152 constraints:
1153 - if: 'chip == "esp32c6"'
1154 type:
1155 validator: integer_in_range
1156 value:
1157 start: 0
1158 end: 100
1159 - if: true
1160 type:
1161 validator: integer_in_range
1162 value:
1163 start: 0
1164 end: 50
1165 active: true
1166"#;
1167
1168 let (cfg, options) = evaluate_yaml_config(
1169 yml,
1170 Some(esp_metadata_generated::Chip::Esp32),
1171 vec![],
1172 false,
1173 )
1174 .unwrap();
1175
1176 assert_eq!("esp-bootloader-esp-idf", cfg.krate);
1177
1178 assert_eq!(
1179 vec![ConfigOption {
1180 name: "option".to_string(),
1181 description: "Desc".to_string(),
1182 default_value: Value::Integer(100),
1183 constraint: Some(Validator::IntegerInRange(0..50)),
1184 stability: Stability::Unstable,
1185 active: true,
1186 display_hint: DisplayHint::None,
1187 },],
1188 options
1189 );
1190 }
1191}