espforge_lib/generate/
mod.rs1use 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 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 let components_path = format!("{}/src/components", project_name);
72 fs::create_dir_all(&components_path)?;
73
74 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 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 let globals_path = format!("{}/src/globals", project_name);
93 fs::create_dir_all(&globals_path)?;
94
95 let devices_path = format!("{}/src/devices", project_name);
97 fs::create_dir_all(&devices_path)?;
98
99 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 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 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 if !globals_mod_exists {
136 fs::write(format!("{}/mod.rs", globals_path), "")?;
137 }
138
139 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 let mut root_manifest: toml::Table =
152 toml::from_str(&cargo_content).with_context(|| "Failed to parse generated Cargo.toml")?;
153
154 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 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 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 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 let new_content = toml::to_string_pretty(&root_manifest)?;
201 fs::write(&cargo_path, new_content)?;
202
203 Ok(())
204}