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, enable_async : bool) -> Result<()> {
42 if fs::metadata(project_name).is_ok() {
43 anyhow::bail!("Directory {} already exists", project_name);
44 }
45
46 let mut cmd = Command::new("esp-generate");
47 cmd.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 if enable_async {
61 cmd.arg("-o").arg("embassy");
62 }
63
64 let status = cmd.arg(project_name).output()?;
65
66 if !status.status.success() {
67 anyhow::bail!(
68 "esp-generate failed: {}",
69 String::from_utf8_lossy(&status.stderr)
70 );
71 }
72
73 let components_path = format!("{}/src/components", project_name);
75 fs::create_dir_all(&components_path)?;
76
77 for file in COMPONENTS_DIR.files() {
79 let path = format!("{}/{}", components_path, file.path().to_str().unwrap());
80 if path.ends_with(".rs") {
81 fs::write(&path, file.contents())?;
82 }
83 }
84
85 let platform_path = format!("{}/src/platform", project_name);
87 fs::create_dir_all(&platform_path)?;
88
89 for file in PLATFORM_DIR.files() {
90 let path = format!("{}/{}", platform_path, file.path().to_str().unwrap());
91 fs::write(&path, file.contents())?;
92 }
93
94 let globals_path = format!("{}/src/globals", project_name);
96 fs::create_dir_all(&globals_path)?;
97
98 let devices_path = format!("{}/src/devices", project_name);
100 fs::create_dir_all(&devices_path)?;
101
102 let mut device_modules = Vec::new();
104 for subdir in DEVICES_DIR.dirs() {
105 if let Some(device_name) = subdir.path().file_name().and_then(|n| n.to_str()) {
106 for file in subdir.files() {
107 if file.path().file_name().and_then(|n| n.to_str()) == Some("device.rs") {
108 let dest = format!("{}/{}.rs", devices_path, device_name);
109 fs::write(&dest, file.contents())?;
110 device_modules.push(device_name.to_string());
111 }
112 }
113 }
114 }
115
116 let mut devices_mod_content = String::new();
118 for module in device_modules {
119 devices_mod_content.push_str(&format!("pub mod {};\n", module));
120 devices_mod_content.push_str(&format!("pub use {}::*;\n", module));
122 }
123 fs::write(format!("{}/mod.rs", devices_path), devices_mod_content)?;
124
125 let mut globals_mod_exists = false;
126 for file in GLOBALS_DIR.files() {
127 let file_path_str = file.path().to_str().unwrap();
128 let path = format!("{}/{}", globals_path, file_path_str);
129 if path.ends_with(".rs") {
130 if file_path_str == "mod.rs" {
131 globals_mod_exists = true;
132 }
133 fs::write(&path, file.contents())?;
134 }
135 }
136
137 if !globals_mod_exists {
139 fs::write(format!("{}/mod.rs", globals_path), "")?;
140 }
141
142 update_cargo_manifest(project_name)?;
144
145 Ok(())
146}
147
148fn update_cargo_manifest(project_name: &str) -> Result<()> {
149 let cargo_path = format!("{}/Cargo.toml", project_name);
150 let cargo_content = fs::read_to_string(&cargo_path)
151 .with_context(|| format!("Failed to read {}", cargo_path))?;
152
153 let mut root_manifest: toml::Table =
155 toml::from_str(&cargo_content).with_context(|| "Failed to parse generated Cargo.toml")?;
156
157 if !root_manifest.contains_key("workspace") {
160 root_manifest.insert(
161 "workspace".to_string(),
162 toml::Value::Table(toml::Table::new()),
163 );
164 }
165
166 let root_deps = root_manifest
169 .entry("dependencies".to_string())
170 .or_insert(toml::Value::Table(toml::Table::new()))
171 .as_table_mut()
172 .ok_or_else(|| anyhow::anyhow!("'dependencies' in Cargo.toml is not a table"))?;
173
174 for subdir in DEVICES_DIR.dirs() {
175 let cargo_file_opt = subdir
176 .files()
177 .find(|f| f.path().file_name().and_then(|n| n.to_str()) == Some("Cargo.toml.tera"));
178
179 if let Some(cargo_file) = cargo_file_opt {
180 let device_toml_str = cargo_file
181 .contents_utf8()
182 .ok_or_else(|| anyhow::anyhow!("Device Cargo.toml.tera is not valid UTF-8"))?;
183
184 let device_manifest: toml::Value =
186 toml::from_str(device_toml_str).with_context(|| {
187 format!("Failed to parse Cargo.toml.tera for device {:?}", subdir.path())
188 })?;
189
190 if let Some(deps) = device_manifest
192 .get("dependencies")
193 .and_then(|d| d.as_table())
194 {
195 for (k, v) in deps {
196 root_deps.insert(k.clone(), v.clone());
197 }
198 }
199 }
200 }
201
202 let new_content = toml::to_string_pretty(&root_manifest)?;
204 fs::write(&cargo_path, new_content)?;
205
206 Ok(())
207}