1use crate::generate;
2use anyhow::{Error, Result};
3use std::fs;
4use std::path::Path;
5
6use crate::config::{EspforgeConfig, EspforgeConfiguration};
7use crate::nibblers::{NibblerDispatcher, NibblerStatus};
8use crate::resolver::{ContextResolver, ruchy_bridge};
9use crate::template_utils::{find_template_path, get_templates, process_template_directory};
10
11use serde::Serialize;
12
13#[derive(Serialize)]
14struct EspforgeContext<'a> {
15 #[serde(flatten)]
16 espforge: &'a EspforgeConfig,
17 target: &'a str,
18}
19
20pub fn compile<P: AsRef<Path>>(path: P) -> Result<(), Error> {
21 let configuration_contents = fs::read_to_string(&path)?;
22 let config: EspforgeConfiguration = serde_yaml_ng::from_str(&configuration_contents)?;
23
24 println!("Running configuration checks...");
25 let dispatcher = NibblerDispatcher::new();
26 let results = dispatcher.process_config(&config);
27 let mut validation_failed = false;
28
29 for result in results {
30 if !result.findings.is_empty() {
31 println!("== {} ==", result.nibbler_name);
32 for finding in result.findings {
33 let prefix = match result.status {
34 NibblerStatus::Error => "❌",
35 NibblerStatus::Warning => "⚠️",
36 NibblerStatus::Ok => "✅",
37 };
38 println!(" {} {}", prefix, finding);
39 }
40 }
41
42 if result.status == NibblerStatus::Error {
43 validation_failed = true;
44 }
45 }
46
47 if validation_failed {
48 anyhow::bail!("Configuration validation failed due to errors above.");
49 }
50
51 let project_name = config.get_name();
52 let esp32_platform = config.get_platform();
53
54 println!("Generating project '{}'...", project_name);
55 generate::generate(project_name, &esp32_platform)?;
56 println!("Project generation complete.");
57
58 let espforge_context = EspforgeContext {
59 espforge: &config.espforge,
60 target: config.espforge.platform.target(),
61 };
62
63 let mut context = if let Some(example_config) = &config.example {
64 crate::templates::create_context(&example_config.name, &example_config.example_properties)?
65 } else {
66 tera::Context::new()
67 };
68 context.insert("espforge", &espforge_context);
69
70 let template_name_raw = config
72 .get_template()
73 .unwrap_or_else(|| "_dynamic".to_string());
74
75 let template_path = if template_name_raw == "_dynamic" {
76 "_dynamic".to_string()
77 } else {
78 find_template_path(&template_name_raw)
79 .ok_or_else(|| anyhow::anyhow!("Template '{}' not found", template_name_raw))?
80 };
81
82 let manifests = generate::load_manifests()?;
83 let mut resolver = ContextResolver::new();
84 let mut render_ctx = resolver.resolve(&config, &manifests)?;
85
86 let config_dir = path.as_ref().parent().unwrap_or_else(|| Path::new("."));
88 let local_ruchy_path = config_dir.join("app.ruchy");
89
90 let ruchy_source_opt = if local_ruchy_path.exists() {
91 println!("Found local Ruchy script: {}", local_ruchy_path.display());
92 Some(fs::read_to_string(&local_ruchy_path)?)
93 } else {
94 let templates = get_templates();
95 let embedded_path = format!("{}/app.ruchy", template_path);
97 if let Some(file) = templates.get_file(&embedded_path) {
98 println!("Found embedded Ruchy script: {}", embedded_path);
99 Some(
100 file.contents_utf8()
101 .ok_or_else(|| anyhow::anyhow!("Embedded Ruchy file is not valid UTF-8"))?
102 .to_string(),
103 )
104 } else {
105 None
106 }
107 };
108
109 let mut combined_variables = render_ctx.variables.clone();
111
112 if let Some(raw_source) = ruchy_source_opt {
113 let ruchy_out = ruchy_bridge::compile_ruchy_script(&raw_source)?;
114
115 if !ruchy_out.setup.is_empty() {
116 render_ctx.setup_code.push(ruchy_out.setup);
117 }
118 if !ruchy_out.loop_body.is_empty() {
119 render_ctx.loop_code.push(ruchy_out.loop_body);
120 }
121 combined_variables.extend(ruchy_out.variables);
123 }
124
125 context.insert("includes", &render_ctx.includes);
126 context.insert("initializations", &render_ctx.initializations);
127 context.insert("variables", &combined_variables);
128 context.insert("setup_code", &render_ctx.setup_code);
129 context.insert("loop_code", &render_ctx.loop_code);
130
131 process_template_directory("_dynamic", project_name, &context)?;
133
134 if template_path != "_dynamic" {
136 process_template_directory(&template_path, project_name, &context)?;
137 }
138
139 let local_wokwi_tera = config_dir.join("wokwi.toml.tera");
145 if local_wokwi_tera.exists() {
146 println!("Applying local override: wokwi.toml.tera");
147 let content = fs::read_to_string(&local_wokwi_tera)?;
148 let rendered = tera::Tera::one_off(&content, &context, true)?;
149
150 let target_path = Path::new(project_name).join("wokwi.toml");
152 fs::write(&target_path, rendered)?;
153 }
154
155 let overrides = ["diagram.json", "wokwi.toml", "chip.wasm", "chip.json"];
157 for filename in overrides {
158 let local_path = config_dir.join(filename);
159 if local_path.exists() {
160 println!("Applying local override: {}", filename);
161 let target_path = Path::new(project_name).join(filename);
162 fs::copy(&local_path, &target_path)?;
163 }
164 }
165
166 Ok(())
167}