use crate::generate;
use anyhow::{Error, Result, Context};
use std::fs;
use std::path::Path;
use crate::config::{EspforgeConfig, EspforgeConfiguration};
use crate::nibblers::{NibblerDispatcher, NibblerStatus};
use crate::resolver::{ContextResolver, ruchy_bridge};
use crate::template_utils::{find_template_path, get_templates, process_template_directory};
use serde::Serialize;
use toml;
#[derive(Serialize)]
struct EspforgeContext<'a> {
#[serde(flatten)]
espforge: &'a EspforgeConfig,
target: &'a str,
}
pub fn compile<P: AsRef<Path>>(path: P) -> Result<(), Error> {
let configuration_contents = fs::read_to_string(&path)?;
let config: EspforgeConfiguration = serde_yaml_ng::from_str(&configuration_contents)?;
println!("Running configuration checks...");
let dispatcher = NibblerDispatcher::new();
let results = dispatcher.process_config(&config);
let mut validation_failed = false;
for result in results {
if !result.findings.is_empty() {
println!("== {} ==", result.nibbler_name);
for finding in result.findings {
let prefix = match result.status {
NibblerStatus::Error => "❌",
NibblerStatus::Warning => "⚠️",
NibblerStatus::Ok => "✅",
};
println!(" {} {}", prefix, finding);
}
}
if result.status == NibblerStatus::Error {
validation_failed = true;
}
}
if validation_failed {
anyhow::bail!("Configuration validation failed due to errors above.");
}
let project_name = config.get_name();
let esp32_platform = config.get_platform();
println!("Generating project '{}'...", project_name);
generate::generate(project_name, &esp32_platform, config.espforge.enable_async)?;
println!("Project generation complete.");
let cargo_path = Path::new(project_name).join("Cargo.toml");
let base_toml_content = fs::read_to_string(&cargo_path)
.with_context(|| "Failed to read base Cargo.toml")?;
let mut base_manifest: toml::Table = toml::from_str(&base_toml_content)
.with_context(|| "Failed to parse generated Cargo.toml")?;
let espforge_context = EspforgeContext {
espforge: &config.espforge,
target: config.espforge.platform.target(),
};
let mut context = if let Some(example_config) = &config.example {
crate::templates::create_context(&example_config.name, &example_config.example_properties)?
} else {
tera::Context::new()
};
context.insert("espforge", &espforge_context);
let template_name_raw = config
.get_template()
.unwrap_or_else(|| "_dynamic".to_string());
let template_path = if template_name_raw == "_dynamic" {
"_dynamic".to_string()
} else {
find_template_path(&template_name_raw)
.ok_or_else(|| anyhow::anyhow!("Template '{}' not found", template_name_raw))?
};
let manifests = generate::load_manifests()?;
let mut resolver = ContextResolver::new();
let mut render_ctx = resolver.resolve(&config, &manifests)?;
let config_dir = path.as_ref().parent().unwrap_or_else(|| Path::new("."));
let local_ruchy_path = config_dir.join("app.ruchy");
let ruchy_source_opt = if local_ruchy_path.exists() {
println!("Found local Ruchy script: {}", local_ruchy_path.display());
Some(fs::read_to_string(&local_ruchy_path)?)
} else {
let templates = get_templates();
let embedded_path = format!("{}/app.ruchy", template_path);
if let Some(file) = templates.get_file(&embedded_path) {
println!("Found embedded Ruchy script: {}", embedded_path);
Some(
file.contents_utf8()
.ok_or_else(|| anyhow::anyhow!("Embedded Ruchy file is not valid UTF-8"))?
.to_string(),
)
} else {
None
}
};
let mut combined_variables = render_ctx.variables.clone();
if let Some(raw_source) = ruchy_source_opt {
let ruchy_out = ruchy_bridge::compile_ruchy_script(&raw_source, config.espforge.enable_async)?;
if !ruchy_out.setup.is_empty() {
render_ctx.setup_code.push(ruchy_out.setup);
}
if !ruchy_out.loop_body.is_empty() {
render_ctx.loop_code.push(ruchy_out.loop_body);
}
render_ctx.task_definitions.extend(ruchy_out.task_definitions);
render_ctx.task_spawns.extend(ruchy_out.task_spawns);
combined_variables.extend(ruchy_out.variables);
}
context.insert("includes", &render_ctx.includes);
context.insert("initializations", &render_ctx.initializations);
context.insert("variables", &combined_variables);
context.insert("setup_code", &render_ctx.setup_code);
context.insert("loop_code", &render_ctx.loop_code);
context.insert("task_definitions", &render_ctx.task_definitions);
context.insert("task_spawns", &render_ctx.task_spawns);
process_template_directory("_dynamic", project_name, &context)?;
if template_path != "_dynamic" {
process_template_directory(&template_path, project_name, &context)?;
}
let current_toml_content = fs::read_to_string(&cargo_path)?;
if current_toml_content != base_toml_content {
println!("Merging template dependencies into Cargo.toml...");
let template_manifest: toml::Table = toml::from_str(¤t_toml_content)
.with_context(|| "Failed to parse template Cargo.toml for merging")?;
if let Some(deps) = template_manifest.get("dependencies").and_then(|v| v.as_table()) {
let base_deps = base_manifest
.entry("dependencies".to_string())
.or_insert(toml::Value::Table(toml::Table::new()))
.as_table_mut()
.unwrap();
for (k, v) in deps {
base_deps.insert(k.clone(), v.clone());
}
}
if let Some(deps) = template_manifest.get("build-dependencies").and_then(|v| v.as_table()) {
let base_deps = base_manifest
.entry("build-dependencies".to_string())
.or_insert(toml::Value::Table(toml::Table::new()))
.as_table_mut()
.unwrap();
for (k, v) in deps {
base_deps.insert(k.clone(), v.clone());
}
}
let merged_content = toml::to_string_pretty(&base_manifest)?;
fs::write(&cargo_path, merged_content)?;
}
let local_wokwi_tera = config_dir.join("wokwi.toml.tera");
if local_wokwi_tera.exists() {
println!("Applying local override: wokwi.toml.tera");
let content = fs::read_to_string(&local_wokwi_tera)?;
let rendered = tera::Tera::one_off(&content, &context, true)?;
let target_path = Path::new(project_name).join("wokwi.toml");
fs::write(&target_path, rendered)?;
}
let local_cargo_tera = config_dir.join("Cargo.toml.tera");
if local_cargo_tera.exists() {
println!("Applying local override: Cargo.toml.tera");
let content = fs::read_to_string(&local_cargo_tera)?;
let rendered = tera::Tera::one_off(&content, &context, true)?;
let current_base_content = fs::read_to_string(&cargo_path)?;
let mut base_manifest: toml::Table = toml::from_str(¤t_base_content)?;
let template_manifest: toml::Table = toml::from_str(&rendered)
.with_context(|| "Failed to parse local Cargo.toml.tera")?;
if let Some(deps) = template_manifest.get("dependencies").and_then(|v| v.as_table()) {
let base_deps = base_manifest
.entry("dependencies".to_string())
.or_insert(toml::Value::Table(toml::Table::new()))
.as_table_mut()
.unwrap();
for (k, v) in deps {
base_deps.insert(k.clone(), v.clone());
}
}
fs::write(&cargo_path, toml::to_string_pretty(&base_manifest)?)?;
}
let overrides = ["diagram.json", "wokwi.toml", "chip.wasm", "chip.json"];
for filename in overrides {
let local_path = config_dir.join(filename);
if local_path.exists() {
println!("Applying local override: {}", filename);
let target_path = Path::new(project_name).join(filename);
fs::copy(&local_path, &target_path)?;
}
}
Ok(())
}