espforge 0.1.2

A scaffolding++ project generator for bare-metal ESP32 projects
Documentation
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);
        
        // Use the spawns generated by ruchy_bridge which include arguments
        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(&current_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(&current_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(())
}