espforge_lib/generate/
mod.rs

1use crate::manifest::ComponentManifest;
2use anyhow::{Context, Result};
3use include_dir::{Dir, include_dir};
4use std::collections::HashMap;
5use std::fs;
6use std::process::Command;
7use toml;
8
9static COMPONENTS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/components");
10static GLOBALS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/globals");
11static PLATFORM_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/platform");
12static DEVICES_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/devices");
13
14pub fn load_manifests() -> Result<HashMap<String, ComponentManifest>> {
15    let mut manifests = HashMap::new();
16
17    // Helper to load manifests from a directory recursively
18    let mut load_from_dir = |dir: &Dir<'_>| -> Result<()> {
19        for entry in dir.find("**/*.ron")? {
20            if let Some(file) = entry.as_file() {
21                let path_str = file.path().to_str().unwrap_or("unknown");
22
23                let content = file
24                    .contents_utf8()
25                    .with_context(|| format!("File {} is not valid UTF-8", path_str))?;
26
27                let manifest: ComponentManifest = ron::from_str(content)
28                    .with_context(|| format!("Failed to parse manifest file: {}", path_str))?;
29                manifests.insert(manifest.name.clone(), manifest);
30            }
31        }
32        Ok(())
33    };
34
35    load_from_dir(&COMPONENTS_DIR)?;
36    load_from_dir(&GLOBALS_DIR)?;
37    load_from_dir(&DEVICES_DIR)?;
38    Ok(manifests)
39}
40
41pub fn generate(project_name: &str, chip: &str) -> Result<()> {
42    if fs::metadata(project_name).is_ok() {
43        anyhow::bail!("Directory {} already exists", project_name);
44    }
45
46    let status = Command::new("esp-generate")
47        .arg("--headless")
48        .arg("--chip")
49        .arg(chip)
50        .arg("-o")
51        .arg("log")
52        .arg("-o")
53        .arg("unstable-hal")
54        .arg("-o")
55        .arg("esp-backtrace")
56        .arg("-o")
57        .arg("wokwi")
58        .arg("-o")
59        .arg("vscode")
60        .arg(project_name)
61        .output()?;
62
63    if !status.status.success() {
64        anyhow::bail!(
65            "esp-generate failed: {}",
66            String::from_utf8_lossy(&status.stderr)
67        );
68    }
69
70    // Create src/components directory
71    let components_path = format!("{}/src/components", project_name);
72    fs::create_dir_all(&components_path)?;
73
74    // Copy component sources
75    for file in COMPONENTS_DIR.files() {
76        let path = format!("{}/{}", components_path, file.path().to_str().unwrap());
77        if path.ends_with(".rs") {
78            fs::write(&path, file.contents())?;
79        }
80    }
81
82    // Create src/platform directory and copy sources
83    let platform_path = format!("{}/src/platform", project_name);
84    fs::create_dir_all(&platform_path)?;
85
86    for file in PLATFORM_DIR.files() {
87        let path = format!("{}/{}", platform_path, file.path().to_str().unwrap());
88        fs::write(&path, file.contents())?;
89    }
90
91    // Create src/globals directory and copy sources
92    let globals_path = format!("{}/src/globals", project_name);
93    fs::create_dir_all(&globals_path)?;
94
95    // Devices
96    let devices_path = format!("{}/src/devices", project_name);
97    fs::create_dir_all(&devices_path)?;
98
99    // Flatten nested devices (e.g. devices/ssd1306/device.rs -> src/devices/ssd1306.rs)
100    let mut device_modules = Vec::new();
101    for subdir in DEVICES_DIR.dirs() {
102        if let Some(device_name) = subdir.path().file_name().and_then(|n| n.to_str()) {
103            for file in subdir.files() {
104                if file.path().file_name().and_then(|n| n.to_str()) == Some("device.rs") {
105                    let dest = format!("{}/{}.rs", devices_path, device_name);
106                    fs::write(&dest, file.contents())?;
107                    device_modules.push(device_name.to_string());
108                }
109            }
110        }
111    }
112
113    // Create src/devices/mod.rs
114    let mut devices_mod_content = String::new();
115    for module in device_modules {
116        devices_mod_content.push_str(&format!("pub mod {};\n", module));
117        // Add pub use to re-export the device struct
118        devices_mod_content.push_str(&format!("pub use {}::*;\n", module));
119    }
120    fs::write(format!("{}/mod.rs", devices_path), devices_mod_content)?;
121
122    let mut globals_mod_exists = false;
123    for file in GLOBALS_DIR.files() {
124        let file_path_str = file.path().to_str().unwrap();
125        let path = format!("{}/{}", globals_path, file_path_str);
126        if path.ends_with(".rs") {
127            if file_path_str == "mod.rs" {
128                globals_mod_exists = true;
129            }
130            fs::write(&path, file.contents())?;
131        }
132    }
133
134    // Ensure mod.rs exists for globals so compilation doesn't fail if directory is empty
135    if !globals_mod_exists {
136        fs::write(format!("{}/mod.rs", globals_path), "")?;
137    }
138
139    // Update Cargo.toml: Merge device dependencies and add [workspace]
140    update_cargo_manifest(project_name)?;
141
142    Ok(())
143}
144
145fn update_cargo_manifest(project_name: &str) -> Result<()> {
146    let cargo_path = format!("{}/Cargo.toml", project_name);
147    let cargo_content = fs::read_to_string(&cargo_path)
148        .with_context(|| format!("Failed to read {}", cargo_path))?;
149
150    // Parse the generated Cargo.toml
151    let mut root_manifest: toml::Table =
152        toml::from_str(&cargo_content).with_context(|| "Failed to parse generated Cargo.toml")?;
153
154    // 1. Add [workspace] to the bottom (via Table insertion)
155    // This effectively isolates the project from any parent Cargo.toml files
156    if !root_manifest.contains_key("workspace") {
157        root_manifest.insert(
158            "workspace".to_string(),
159            toml::Value::Table(toml::Table::new()),
160        );
161    }
162
163    // 2. Merge dependencies from all devices found in DEVICES_DIR
164    // We assume the destination 'dependencies' table exists (esp-generate creates it)
165    let root_deps = root_manifest
166        .entry("dependencies".to_string())
167        .or_insert(toml::Value::Table(toml::Table::new()))
168        .as_table_mut()
169        .ok_or_else(|| anyhow::anyhow!("'dependencies' in Cargo.toml is not a table"))?;
170
171    for subdir in DEVICES_DIR.dirs() {
172        let cargo_file_opt = subdir
173            .files()
174            .find(|f| f.path().file_name().and_then(|n| n.to_str()) == Some("Cargo.toml.tera"));
175
176        if let Some(cargo_file) = cargo_file_opt {
177            let device_toml_str = cargo_file
178                .contents_utf8()
179                .ok_or_else(|| anyhow::anyhow!("Device Cargo.toml.tera is not valid UTF-8"))?;
180
181            // Parse the device manifest
182            let device_manifest: toml::Value =
183                toml::from_str(device_toml_str).with_context(|| {
184                    format!("Failed to parse Cargo.toml.tera for device {:?}", subdir.path())
185                })?;
186
187            // If it has dependencies, merge them into the root dependencies
188            if let Some(deps) = device_manifest
189                .get("dependencies")
190                .and_then(|d| d.as_table())
191            {
192                for (k, v) in deps {
193                    root_deps.insert(k.clone(), v.clone());
194                }
195            }
196        }
197    }
198
199    // Write back to file
200    let new_content = toml::to_string_pretty(&root_manifest)?;
201    fs::write(&cargo_path, new_content)?;
202
203    Ok(())
204}