flowlang/builder/
cargo.rs

1//! This file is dedicated to all interactions with Cargo.toml files,
2//! including creating them and updating their dependencies.
3
4use std::collections::HashMap;
5use std::fs::create_dir_all;
6use std::path::PathBuf;
7
8use ndata::dataobject::DataObject;
9
10use super::util::{get_project_top_level_path, read_lines_from_file, write_lines_to_file};
11
12/// Creates or updates a Cargo.toml file for a given library.
13/// It dynamically determines core dependency versions from the root Cargo.toml.
14pub(crate) fn update_cargo_toml(cargo_toml_path: &PathBuf, cargo_config: &DataObject, lib_name: &str, default_package_name: &str, is_ffi: bool) -> bool {
15    let mut file_was_created = false;
16    if !cargo_toml_path.exists() {
17        if default_package_name != "main_project" {
18            println!("Cargo.toml not found at {:?} for sub-project '{}' (library {}), creating default.", cargo_toml_path, default_package_name, lib_name);
19
20            // Dynamically get the core dependency lines from the root Cargo.toml.
21            let (flowlang_dep_line, ndata_dep_line) = get_core_dependency_lines();
22
23            let crate_types_str = if is_ffi {
24                "[\"cdylib\", \"rlib\"]".to_string()
25            } else if cargo_config.has("crate_types") {
26                let crate_types_da = cargo_config.get_array("crate_types");
27                let types: Vec<String> = crate_types_da.objects().iter()
28                    .map(|val| val.string())
29                    .collect();
30                if !types.is_empty() {
31                    format!("[{}]", types.iter().map(|s| format!("\"{}\"", s)).collect::<Vec<String>>().join(", "))
32                } else {
33                    "[\"rlib\"]".to_string()
34                }
35            } else {
36                "[\"rlib\"]".to_string()
37            };
38
39            let default_content = format!(
40r#"[package]
41name = "{}"
42version = "0.1.0"
43edition = "2021"
44
45[lib]
46crate-type = {}
47
48[dependencies]
49{}
50{}
51serde = {{ version = "1.0", features = ["derive"], optional = true }}
52serde_json = {{ version = "1.0", optional = true }}
53
54[features]
55reload = []
56default = []
57"# , default_package_name, crate_types_str, flowlang_dep_line, ndata_dep_line);
58
59            if let Some(parent_dir) = cargo_toml_path.parent() {
60                create_dir_all(parent_dir).expect("Failed to create parent directory for new Cargo.toml");
61            }
62            std::fs::write(&cargo_toml_path, default_content)
63                .expect(&format!("Failed to write default Cargo.toml to {:?}", cargo_toml_path));
64            file_was_created = true;
65        } else {
66            println!("WARNING: Main project Cargo.toml not found at {:?}, cannot perform updates.", cargo_toml_path);
67            return false;
68        }
69    }
70
71    let mut config_caused_modification = false;
72    let mut lines = read_lines_from_file(&cargo_toml_path)
73        .expect(&format!("Failed to read Cargo.toml at {:?}", cargo_toml_path));
74
75    if cargo_config.has("dependencies") {
76        let mut dependencies_map = HashMap::new();
77        let dependencies_insertion_line = find_section_insertion_line(&lines, "[dependencies]", &mut dependencies_map);
78        let new_dependencies = cargo_config.get_object("dependencies");
79        if new_dependencies.clone().keys().len() > 0 {
80            if update_cargo_section_lines(
81                &mut lines,
82                &new_dependencies,
83                &mut dependencies_map,
84                dependencies_insertion_line,
85                "Dependency",
86                lib_name,
87            ).0 {
88                config_caused_modification = true;
89            }
90        }
91    }
92
93    if config_caused_modification {
94        println!("Rewriting {}", cargo_toml_path.display());
95        write_lines_to_file(&cargo_toml_path, &lines)
96            .expect(&format!("Failed to write to Cargo.toml at {:?}", cargo_toml_path));
97    }
98
99    file_was_created || config_caused_modification
100}
101
102/// Reads the root Cargo.toml to find the dependency lines for flowlang and ndata.
103fn get_core_dependency_lines() -> (String, String) {
104    let root_cargo_path = get_project_top_level_path().join("Cargo.toml");
105    let fallback_flowlang = "flowlang = { version = \"0.3.25\" }".to_string();
106    let fallback_ndata = "ndata = { version = \"0.3.14\" }".to_string();
107
108    if !root_cargo_path.exists() {
109        println!("WARNING: Root Cargo.toml not found. Falling back to default dependency versions.");
110        return (fallback_flowlang, fallback_ndata);
111    }
112
113    let lines = read_lines_from_file(&root_cargo_path).unwrap_or_else(|_| {
114        println!("WARNING: Could not read root Cargo.toml. Falling back to default dependency versions.");
115        vec![]
116    });
117
118    let mut flowlang_line = None;
119    let mut ndata_line = None;
120    let mut in_deps_section = false;
121
122    for line in lines {
123        let trimmed_line = line.trim();
124        if trimmed_line == "[dependencies]" {
125            in_deps_section = true;
126            continue;
127        }
128        if in_deps_section && trimmed_line.starts_with('[') {
129            break; // Moved to the next section
130        }
131        if in_deps_section {
132            if trimmed_line.starts_with("flowlang") {
133                flowlang_line = Some(line.clone());
134            } else if trimmed_line.starts_with("ndata") {
135                ndata_line = Some(line.clone());
136            }
137        }
138        if flowlang_line.is_some() && ndata_line.is_some() {
139            break;
140        }
141    }
142    (flowlang_line.unwrap_or(fallback_flowlang), ndata_line.unwrap_or(fallback_ndata))
143}
144
145/// Parses a `Cargo.toml`'s lines to find where a new entry in a section should be inserted.
146pub(crate) fn find_section_insertion_line(
147    lines: &[String],
148    section_marker: &str,
149    existing_items_map: &mut HashMap<String, String>,
150) -> usize {
151    let mut section_start_idx: Option<usize> = None;
152    let mut in_section = false;
153
154    for (i, line_content) in lines.iter().enumerate() {
155        let trimmed_line = line_content.trim();
156        if trimmed_line == section_marker {
157            section_start_idx = Some(i);
158            in_section = true;
159            existing_items_map.clear();
160            continue;
161        }
162
163        if trimmed_line.starts_with('[') && in_section {
164            break;
165        }
166
167        if in_section {
168            if let Some(eq_offset) = trimmed_line.find('=') {
169                let key = trimmed_line[..eq_offset].trim().to_string();
170                let value_part = trimmed_line[eq_offset + 1..].trim().to_string();
171                existing_items_map.insert(key, value_part);
172            }
173        }
174    }
175    section_start_idx.map_or(lines.len(), |idx| idx + 1)
176}
177
178
179/// Updates a section in a `Cargo.toml` (represented as a Vec of lines) with new items.
180pub(crate) fn update_cargo_section_lines(
181    cargo_lines: &mut Vec<String>,
182    new_items_config: &DataObject,
183    section_items_map: &mut HashMap<String, String>,
184    mut current_insertion_idx: usize,
185    item_type_name: &str,
186    lib_name: &str,
187) -> (bool, usize) {
188    let mut section_modified = false;
189
190    if current_insertion_idx > cargo_lines.len() {
191        current_insertion_idx = cargo_lines.len();
192    }
193
194    for (key, value_obj) in new_items_config.objects() {
195        let value_from_meta = value_obj.string();
196
197        let new_line_content = if value_from_meta.trim().starts_with('{') {
198            format!("{} = {}", key, value_from_meta)
199        } else {
200            let normalized_version = value_from_meta.trim().trim_matches('"');
201            format!("{} = \"{}\"", key, normalized_version)
202        };
203
204        if let Some(existing_line_value) = section_items_map.get(&key) {
205            if existing_line_value.contains("path") {
206                continue;
207            }
208
209            let normalized_existing = existing_line_value.trim().trim_matches('"');
210            let normalized_meta = value_from_meta.trim().trim_matches('"');
211
212            if normalized_existing != normalized_meta {
213                 println!(
214                    "WARNING: {} '{}' in Cargo.toml for library \"{}\" (current value: {}) does not match config value ({}). Updating.",
215                    item_type_name, key, lib_name, existing_line_value, value_from_meta
216                );
217
218                let mut updated = false;
219                for line_idx in (0..cargo_lines.len()).rev() {
220                    if cargo_lines[line_idx].trim().starts_with(&format!("{} =", key)) {
221                        cargo_lines[line_idx] = new_line_content.clone();
222                        section_modified = true;
223                        updated = true;
224                        break;
225                    }
226                }
227                if !updated {
228                    println!("WARNING: Could not find existing {} line for '{}' in Cargo.toml to update.", item_type_name, key);
229                }
230            }
231
232        } else {
233            println!("Adding new {} to Cargo.toml for library \"{}\": {}", item_type_name, lib_name, new_line_content);
234            cargo_lines.insert(current_insertion_idx, new_line_content.clone());
235            section_modified = true;
236            current_insertion_idx += 1;
237        }
238    }
239    (section_modified, current_insertion_idx)
240}