espforge_lib/generate/
cargo.rs

1use super::manifest;
2use anyhow::{Context, Result};
3use log::{debug, info, warn};
4use std::fs;
5use std::path::Path;
6use toml_edit::{Array, DocumentMut, Item, Table, Value};
7
8const CARGO_TEMPLATE_NAME: &str = "Cargo.toml.tera";
9
10pub fn update_manifest(
11    project_path: &Path, 
12    config_dir: &Path, 
13    chip: &str,
14    context: &tera::Context
15) -> Result<()> {
16    let cargo_path = project_path.join("Cargo.toml");
17    let cargo_content = fs::read_to_string(&cargo_path)
18        .with_context(|| format!("Failed to read {}", cargo_path.display()))?;
19    
20    let mut doc = cargo_content.parse::<DocumentMut>()
21        .context("Failed to parse Cargo.toml")?;
22
23    // 1. Merge dependencies from _dynamic/Cargo.toml.tera (Embedded)
24    if let Some(dynamic_file) = crate::template_utils::get_templates()
25        .get_file("_dynamic/Cargo.toml.tera") 
26    {
27        let content = dynamic_file.contents_utf8()
28            .context("_dynamic/Cargo.toml.tera is invalid UTF-8")?;
29        let rendered = tera::Tera::one_off(content, context, true)
30            .context("Failed to render _dynamic/Cargo.toml.tera")?;
31        let dynamic_doc = rendered.parse::<DocumentMut>()
32            .context("Failed to parse rendered _dynamic cargo template")?;
33        
34        merge_documents(&mut doc, &dynamic_doc);
35    }
36
37    // 2. Merge dependencies from user's config_dir/Cargo.toml.tera (Filesystem)
38    let user_tera = config_dir.join("Cargo.toml.tera");
39    if user_tera.exists() {
40        let content = fs::read_to_string(&user_tera)?;
41        let rendered = tera::Tera::one_off(&content, context, true)
42            .context("Failed to render user Cargo.toml.tera")?;
43        let user_doc = rendered.parse::<DocumentMut>()
44            .context("Failed to parse user Cargo.toml.tera")?;
45        
46        merge_documents(&mut doc, &user_doc);
47    }
48
49    // 3. System updates
50    add_chip_features(&mut doc, chip)?;
51    ensure_workspace_exists(&mut doc);
52    merge_device_dependencies(&mut doc)?;
53
54    fs::write(&cargo_path, doc.to_string())
55        .with_context(|| format!("Failed to write {}", cargo_path.display()))?;
56
57    info!("Updated Cargo.toml");
58    Ok(())
59}
60
61fn merge_documents(target: &mut DocumentMut, source: &DocumentMut) {
62    // FIX: Added "features" to the list so it gets copied from the template
63    for section in ["dependencies", "build-dependencies", "dev-dependencies", "features"] {
64        if let Some(source_table) = source.get(section).and_then(|i| i.as_table()) {
65            if !target.contains_key(section) {
66                target[section] = Item::Table(Table::new());
67            }
68            let target_table = target[section].as_table_mut().unwrap();
69            
70            for (k, v) in source_table.iter() {
71                // Don't overwrite existing keys to prefer esp-generate defaults or previous merges
72                if !target_table.contains_key(k) {
73                    target_table.insert(k, v.clone());
74                }
75            }
76        }
77    }
78}
79
80fn add_chip_features(doc: &mut DocumentMut, chip: &str) -> Result<()> {
81    if !doc.contains_key("features") {
82        doc["features"] = Item::Table(Table::new());
83    }
84    
85    let features = doc["features"]
86        .as_table_mut()
87        .context("'features' is not a table")?;
88
89    // let mut default_array = Array::new();
90    // default_array.push(chip);
91    // features["default"] = Item::Value(Value::Array(default_array));
92    
93    let default_item = features.entry("default")
94        .or_insert(Item::Value(Value::Array(Array::new())));
95
96    let default_array = default_item
97        .as_value_mut()
98        .context("'default' feature is not a value")?
99        .as_array_mut()
100        .context("'default' feature is not an array")?;
101
102    // Add chip to default if not present
103    let exists = default_array.iter().any(|v| v.as_str() == Some(chip));
104    if !exists {
105        default_array.push(chip);
106    }
107
108    if !features.contains_key(chip) {
109        features[chip] = Item::Value(Value::Array(Array::new()));
110    }
111    
112    debug!("Added feature for chip: {}", chip);
113    Ok(())
114}
115
116fn ensure_workspace_exists(doc: &mut DocumentMut) {
117    if !doc.contains_key("workspace") {
118        doc["workspace"] = Item::Table(Table::new());
119    }
120}
121
122fn merge_device_dependencies(doc: &mut DocumentMut) -> Result<()> {
123    if !doc.contains_key("dependencies") {
124        doc["dependencies"] = Item::Table(Table::new());
125    }
126    
127    let root_deps = doc["dependencies"]
128        .as_table_mut()
129        .context("'dependencies' is not a table")?;
130
131    for subdir in manifest::devices_dir().dirs() {
132        if let Some(cargo_file) = find_cargo_template(subdir) {
133            let device_toml_str = cargo_file
134                .contents_utf8()
135                .context("Device Cargo.toml.tera is not valid UTF-8")?;
136
137            let device_doc = device_toml_str.parse::<DocumentMut>()
138                .with_context(|| {
139                    format!("Failed to parse Cargo.toml.tera for device {:?}", subdir.path())
140                })?;
141
142            if let Some(deps) = device_doc.get("dependencies").and_then(|d| d.as_table()) {
143                for (key, value) in deps.iter() {
144                    if root_deps.contains_key(key) {
145                        warn!("Dependency '{}' already exists, skipping from device", key);
146                    } else {
147                        root_deps.insert(key, value.clone());
148                        debug!("Added dependency: {}", key);
149                    }
150                }
151            }
152        }
153    }
154    
155    Ok(())
156}
157
158fn find_cargo_template<'a>(subdir: &'a include_dir::Dir<'a>) -> Option<&'a include_dir::File<'a>> {
159    subdir.files()
160        .find(|f| f.path().file_name().and_then(|n| n.to_str()) == Some(CARGO_TEMPLATE_NAME))
161}
162