espforge_lib/
compile.rs

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        // Use the spawns generated by ruchy_bridge which include arguments
123        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(&current_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(&current_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}