1use crate::generate;
2use anyhow::{Error, Result, Context};
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;
12use toml;
13
14#[derive(Serialize)]
15struct EspforgeContext<'a> {
16 #[serde(flatten)]
17 espforge: &'a EspforgeConfig,
18 target: &'a str,
19}
20
21pub fn compile<P: AsRef<Path>>(path: P) -> Result<(), Error> {
22 let configuration_contents = fs::read_to_string(&path)?;
23 let config: EspforgeConfiguration = serde_yaml_ng::from_str(&configuration_contents)?;
24
25 println!("Running configuration checks...");
26 let dispatcher = NibblerDispatcher::new();
27 let results = dispatcher.process_config(&config);
28 let mut validation_failed = false;
29
30 for result in results {
31 if !result.findings.is_empty() {
32 println!("== {} ==", result.nibbler_name);
33 for finding in result.findings {
34 let prefix = match result.status {
35 NibblerStatus::Error => "❌",
36 NibblerStatus::Warning => "⚠️",
37 NibblerStatus::Ok => "✅",
38 };
39 println!(" {} {}", prefix, finding);
40 }
41 }
42
43 if result.status == NibblerStatus::Error {
44 validation_failed = true;
45 }
46 }
47
48 if validation_failed {
49 anyhow::bail!("Configuration validation failed due to errors above.");
50 }
51
52 let project_name = config.get_name();
53 let esp32_platform = config.get_platform();
54
55 println!("Generating project '{}'...", project_name);
56 generate::generate(project_name, &esp32_platform, config.espforge.enable_async)?;
57 println!("Project generation complete.");
58 let cargo_path = Path::new(project_name).join("Cargo.toml");
59 let base_toml_content = fs::read_to_string(&cargo_path)
60 .with_context(|| "Failed to read base Cargo.toml")?;
61 let mut base_manifest: toml::Table = toml::from_str(&base_toml_content)
62 .with_context(|| "Failed to parse generated Cargo.toml")?;
63
64 let espforge_context = EspforgeContext {
65 espforge: &config.espforge,
66 target: config.espforge.platform.target(),
67 };
68
69 let mut context = if let Some(example_config) = &config.example {
70 crate::templates::create_context(&example_config.name, &example_config.example_properties)?
71 } else {
72 tera::Context::new()
73 };
74 context.insert("espforge", &espforge_context);
75
76 let template_name_raw = config
77 .get_template()
78 .unwrap_or_else(|| "_dynamic".to_string());
79
80 let template_path = if template_name_raw == "_dynamic" {
81 "_dynamic".to_string()
82 } else {
83 find_template_path(&template_name_raw)
84 .ok_or_else(|| anyhow::anyhow!("Template '{}' not found", template_name_raw))?
85 };
86 let manifests = generate::load_manifests()?;
87 let mut resolver = ContextResolver::new();
88 let mut render_ctx = resolver.resolve(&config, &manifests)?;
89
90 let config_dir = path.as_ref().parent().unwrap_or_else(|| Path::new("."));
91 let local_ruchy_path = config_dir.join("app.ruchy");
92 let ruchy_source_opt = if local_ruchy_path.exists() {
93 println!("Found local Ruchy script: {}", local_ruchy_path.display());
94 Some(fs::read_to_string(&local_ruchy_path)?)
95 } else {
96 let templates = get_templates();
97 let embedded_path = format!("{}/app.ruchy", template_path);
98 if let Some(file) = templates.get_file(&embedded_path) {
99 println!("Found embedded Ruchy script: {}", embedded_path);
100 Some(
101 file.contents_utf8()
102 .ok_or_else(|| anyhow::anyhow!("Embedded Ruchy file is not valid UTF-8"))?
103 .to_string(),
104 )
105 } else {
106 None
107 }
108 };
109
110 let mut combined_variables = render_ctx.variables.clone();
111 if let Some(raw_source) = ruchy_source_opt {
112 let ruchy_out = ruchy_bridge::compile_ruchy_script(&raw_source, config.espforge.enable_async)?;
113 if !ruchy_out.setup.is_empty() {
114 render_ctx.setup_code.push(ruchy_out.setup);
115 }
116 if !ruchy_out.loop_body.is_empty() {
117 render_ctx.loop_code.push(ruchy_out.loop_body);
118 }
119
120 render_ctx.task_definitions.extend(ruchy_out.task_definitions);
121
122 render_ctx.task_spawns.extend(ruchy_out.task_spawns);
124
125 combined_variables.extend(ruchy_out.variables);
126 }
127
128 context.insert("includes", &render_ctx.includes);
129 context.insert("initializations", &render_ctx.initializations);
130 context.insert("variables", &combined_variables);
131 context.insert("setup_code", &render_ctx.setup_code);
132 context.insert("loop_code", &render_ctx.loop_code);
133 context.insert("task_definitions", &render_ctx.task_definitions);
134 context.insert("task_spawns", &render_ctx.task_spawns);
135
136
137 process_template_directory("_dynamic", project_name, &context)?;
138 if template_path != "_dynamic" {
139 process_template_directory(&template_path, project_name, &context)?;
140 }
141 let current_toml_content = fs::read_to_string(&cargo_path)?;
142 if current_toml_content != base_toml_content {
143 println!("Merging template dependencies into Cargo.toml...");
144
145 let template_manifest: toml::Table = toml::from_str(¤t_toml_content)
146 .with_context(|| "Failed to parse template Cargo.toml for merging")?;
147
148 if let Some(deps) = template_manifest.get("dependencies").and_then(|v| v.as_table()) {
149 let base_deps = base_manifest
150 .entry("dependencies".to_string())
151 .or_insert(toml::Value::Table(toml::Table::new()))
152 .as_table_mut()
153 .unwrap();
154
155 for (k, v) in deps {
156 base_deps.insert(k.clone(), v.clone());
157 }
158 }
159
160 if let Some(deps) = template_manifest.get("build-dependencies").and_then(|v| v.as_table()) {
161 let base_deps = base_manifest
162 .entry("build-dependencies".to_string())
163 .or_insert(toml::Value::Table(toml::Table::new()))
164 .as_table_mut()
165 .unwrap();
166
167 for (k, v) in deps {
168 base_deps.insert(k.clone(), v.clone());
169 }
170 }
171
172 let merged_content = toml::to_string_pretty(&base_manifest)?;
173 fs::write(&cargo_path, merged_content)?;
174 }
175
176 let local_wokwi_tera = config_dir.join("wokwi.toml.tera");
177 if local_wokwi_tera.exists() {
178 println!("Applying local override: wokwi.toml.tera");
179 let content = fs::read_to_string(&local_wokwi_tera)?;
180 let rendered = tera::Tera::one_off(&content, &context, true)?;
181 let target_path = Path::new(project_name).join("wokwi.toml");
182 fs::write(&target_path, rendered)?;
183 }
184
185 let local_cargo_tera = config_dir.join("Cargo.toml.tera");
186 if local_cargo_tera.exists() {
187 println!("Applying local override: Cargo.toml.tera");
188 let content = fs::read_to_string(&local_cargo_tera)?;
189 let rendered = tera::Tera::one_off(&content, &context, true)?;
190
191 let current_base_content = fs::read_to_string(&cargo_path)?;
192 let mut base_manifest: toml::Table = toml::from_str(¤t_base_content)?;
193
194 let template_manifest: toml::Table = toml::from_str(&rendered)
195 .with_context(|| "Failed to parse local Cargo.toml.tera")?;
196
197 if let Some(deps) = template_manifest.get("dependencies").and_then(|v| v.as_table()) {
198 let base_deps = base_manifest
199 .entry("dependencies".to_string())
200 .or_insert(toml::Value::Table(toml::Table::new()))
201 .as_table_mut()
202 .unwrap();
203
204 for (k, v) in deps {
205 base_deps.insert(k.clone(), v.clone());
206 }
207 }
208
209 fs::write(&cargo_path, toml::to_string_pretty(&base_manifest)?)?;
210 }
211
212 let overrides = ["diagram.json", "wokwi.toml", "chip.wasm", "chip.json"];
213 for filename in overrides {
214 let local_path = config_dir.join(filename);
215 if local_path.exists() {
216 println!("Applying local override: {}", filename);
217 let target_path = Path::new(project_name).join(filename);
218 fs::copy(&local_path, &target_path)?;
219 }
220 }
221
222 Ok(())
223}