espforge_lib/
compile.rs

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    // Resolve the raw template name to a full path
71    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    // Check for Ruchy script
87    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        // Use full resolved path to find the embedded ruchy file
96        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    // Combine variables from YAML (legacy) and Ruchy (modern)
110    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        // Inject top-level Ruchy variables
122        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    // 1. Apply _dynamic template first
132    process_template_directory("_dynamic", project_name, &context)?;
133
134    // 2. Apply specific template
135    if template_path != "_dynamic" {
136        process_template_directory(&template_path, project_name, &context)?;
137    }
138
139    // 3. Local overrides
140
141    // 3a. Handle local wokwi.toml.tera
142    // This allows the downloaded example to have a template file that gets rendered
143    // correctly based on the current platform/project name during compilation.
144    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        // Save as wokwi.toml in the target project directory
151        let target_path = Path::new(project_name).join("wokwi.toml");
152        fs::write(&target_path, rendered)?;
153    }
154
155    // 3b. Handle verbatim local file overrides (highest precedence)
156    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}