herolib_code/rust_builder/
cargo.rs1use crate::rust_builder::error::{BuilderResult, RustBuilderError};
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct BinaryTarget {
8 pub name: String,
10 pub path: Option<PathBuf>,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct CargoMetadata {
17 pub name: String,
19
20 pub version: String,
22
23 pub binaries: Vec<BinaryTarget>,
25
26 pub has_lib: bool,
28
29 pub lib_name: Option<String>,
31
32 pub examples: Vec<String>,
34
35 pub edition: String,
37
38 pub is_workspace: bool,
40
41 pub workspace_members: Vec<String>,
43}
44
45impl CargoMetadata {
46 pub fn empty() -> Self {
48 Self {
49 name: String::new(),
50 version: String::new(),
51 binaries: Vec::new(),
52 has_lib: false,
53 lib_name: None,
54 examples: Vec::new(),
55 edition: "2021".to_string(),
56 is_workspace: false,
57 workspace_members: Vec::new(),
58 }
59 }
60}
61
62pub(crate) fn find_cargo_toml<P: AsRef<Path>>(start: P) -> Option<PathBuf> {
64 let mut current = start.as_ref().to_path_buf();
65
66 if current.is_file() {
68 current = current.parent()?.to_path_buf();
69 }
70
71 loop {
73 let cargo_toml = current.join("Cargo.toml");
74 if cargo_toml.exists() {
75 return Some(cargo_toml);
76 }
77
78 match current.parent() {
80 Some(parent) if parent != current => {
81 current = parent.to_path_buf();
82 }
83 _ => return None,
84 }
85 }
86}
87
88mod toml_models {
90 use serde::Deserialize;
91
92 #[derive(Deserialize, Debug)]
93 pub struct CargoToml {
94 pub package: Option<Package>,
95 pub workspace: Option<Workspace>,
96 #[serde(default)]
97 pub bin: Vec<BinTarget>,
98 #[serde(default)]
99 pub example: Vec<ExampleTarget>,
100 #[serde(default)]
101 pub lib: Option<LibTarget>,
102 }
103
104 #[derive(Deserialize, Debug)]
105 #[serde(untagged)]
106 pub enum Version {
107 String(String),
108 #[serde(rename_all = "lowercase")]
109 Workspace { #[allow(dead_code)] workspace: bool },
110 }
111
112 #[derive(Deserialize, Debug)]
113 #[serde(untagged)]
114 pub enum Edition {
115 String(String),
116 #[serde(rename_all = "lowercase")]
117 Workspace { #[allow(dead_code)] workspace: bool },
118 }
119
120 #[derive(Deserialize, Debug)]
121 pub struct Package {
122 pub name: String,
123 #[serde(default)]
124 pub version: Option<Version>,
125 #[serde(default)]
126 pub edition: Option<Edition>,
127 }
128
129 #[derive(Deserialize, Debug)]
130 pub struct Workspace {
131 #[serde(default)]
132 pub members: Vec<String>,
133 #[serde(default)]
134 #[allow(dead_code)]
135 pub exclude: Vec<String>,
136 }
137
138 #[derive(Deserialize, Debug)]
139 pub struct LibTarget {
140 pub name: Option<String>,
141 #[allow(dead_code)]
142 pub path: Option<String>,
143 }
144
145 #[derive(Deserialize, Debug)]
146 pub struct BinTarget {
147 pub name: String,
148 pub path: Option<String>,
149 }
150
151 #[derive(Deserialize, Debug)]
152 pub struct ExampleTarget {
153 pub name: String,
154 #[allow(dead_code)]
155 pub path: Option<String>,
156 }
157}
158
159pub(crate) fn parse_cargo_toml<P: AsRef<Path>>(path: P) -> BuilderResult<CargoMetadata> {
161 let path = path.as_ref();
162
163 if !path.exists() {
164 return Err(RustBuilderError::PathNotFound {
165 path: path.to_path_buf(),
166 });
167 }
168
169 let content = std::fs::read_to_string(path).map_err(|e| RustBuilderError::Io(e))?;
170
171 let toml_data: toml_models::CargoToml = toml::from_str(&content).map_err(|e| {
172 RustBuilderError::CargoTomlParseError {
173 path: path.to_path_buf(),
174 message: e.to_string(),
175 }
176 })?;
177
178 let mut metadata = CargoMetadata::empty();
179
180 if let Some(package) = toml_data.package {
182 metadata.name = package.name;
183 metadata.version = match package.version {
184 Some(toml_models::Version::String(v)) => v,
185 Some(toml_models::Version::Workspace { .. }) => "0.0.0".to_string(), None => "0.0.0".to_string(),
187 };
188 metadata.edition = match package.edition {
189 Some(toml_models::Edition::String(e)) => e,
190 Some(toml_models::Edition::Workspace { .. }) => "2021".to_string(), None => "2021".to_string(),
192 };
193 }
194
195 metadata.has_lib = toml_data.lib.is_some();
197 if let Some(lib) = toml_data.lib {
198 metadata.lib_name = lib.name;
199 }
200
201 for bin in toml_data.bin {
203 metadata.binaries.push(BinaryTarget {
204 name: bin.name,
205 path: bin.path.map(PathBuf::from),
206 });
207 }
208
209 for example in toml_data.example {
211 metadata.examples.push(example.name);
212 }
213
214 if let Some(workspace) = toml_data.workspace {
216 metadata.is_workspace = true;
217 metadata.workspace_members = workspace.members;
218 }
219
220 Ok(metadata)
221}
222
223pub(crate) fn get_target_dir<P: AsRef<Path>>(project_root: P) -> PathBuf {
225 if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
227 return PathBuf::from(target_dir);
228 }
229
230 let mut current = project_root.as_ref().to_path_buf();
232 loop {
233 let target_candidate = current.join("target");
234
235 if let Ok(cargo_content) = std::fs::read_to_string(current.join("Cargo.toml")) {
237 if cargo_content.contains("[workspace]") {
238 return target_candidate;
240 }
241 }
242
243 match current.parent() {
245 Some(parent) if parent != current => {
246 current = parent.to_path_buf();
247 }
248 _ => {
249 return project_root.as_ref().join("target");
251 }
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use std::fs;
260 use tempfile::tempdir;
261
262 #[test]
263 fn test_find_cargo_toml_in_current_dir() {
264 let temp_dir = tempdir().unwrap();
265 let cargo_path = temp_dir.path().join("Cargo.toml");
266 fs::write(&cargo_path, "").unwrap();
267
268 let found = find_cargo_toml(temp_dir.path());
269 assert_eq!(found, Some(cargo_path));
270 }
271
272 #[test]
273 fn test_find_cargo_toml_walking_up() {
274 let temp_dir = tempdir().unwrap();
275 let cargo_path = temp_dir.path().join("Cargo.toml");
276 fs::write(&cargo_path, "").unwrap();
277
278 let sub_dir = temp_dir.path().join("src");
279 fs::create_dir(&sub_dir).unwrap();
280
281 let found = find_cargo_toml(&sub_dir);
282 assert_eq!(found, Some(cargo_path));
283 }
284
285 #[test]
286 fn test_find_cargo_toml_from_file() {
287 let temp_dir = tempdir().unwrap();
288 let cargo_path = temp_dir.path().join("Cargo.toml");
289 fs::write(&cargo_path, "").unwrap();
290
291 let src_dir = temp_dir.path().join("src");
292 fs::create_dir(&src_dir).unwrap();
293 let main_file = src_dir.join("main.rs");
294 fs::write(&main_file, "").unwrap();
295
296 let found = find_cargo_toml(&main_file);
297 assert_eq!(found, Some(cargo_path));
298 }
299
300 #[test]
301 fn test_parse_cargo_toml_basic() {
302 let temp_dir = tempdir().unwrap();
303 let cargo_path = temp_dir.path().join("Cargo.toml");
304 let content = r#"
305[package]
306name = "test-project"
307version = "1.0.0"
308edition = "2021"
309
310[[bin]]
311name = "test-app"
312path = "src/main.rs"
313"#;
314 fs::write(&cargo_path, content).unwrap();
315
316 let metadata = parse_cargo_toml(&cargo_path).unwrap();
317 assert_eq!(metadata.name, "test-project");
318 assert_eq!(metadata.version, "1.0.0");
319 assert_eq!(metadata.edition, "2021");
320 assert_eq!(metadata.binaries.len(), 1);
321 assert_eq!(metadata.binaries[0].name, "test-app");
322 }
323
324 #[test]
325 fn test_parse_cargo_toml_with_examples() {
326 let temp_dir = tempdir().unwrap();
327 let cargo_path = temp_dir.path().join("Cargo.toml");
328 let content = r#"
329[package]
330name = "example-project"
331version = "0.5.0"
332edition = "2021"
333
334[[example]]
335name = "demo"
336
337[[example]]
338name = "advanced"
339"#;
340 fs::write(&cargo_path, content).unwrap();
341
342 let metadata = parse_cargo_toml(&cargo_path).unwrap();
343 assert_eq!(metadata.name, "example-project");
344 assert_eq!(metadata.examples.len(), 2);
345 assert!(metadata.examples.contains(&"demo".to_string()));
346 assert!(metadata.examples.contains(&"advanced".to_string()));
347 }
348}