Skip to main content

herolib_code/rust_builder/
cargo.rs

1use crate::rust_builder::error::{BuilderResult, RustBuilderError};
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4
5/// Binary target information.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct BinaryTarget {
8    /// Binary name
9    pub name: String,
10    /// Path to main.rs or binary source
11    pub path: Option<PathBuf>,
12}
13
14/// Parsed metadata from Cargo.toml.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct CargoMetadata {
17    /// Package name
18    pub name: String,
19
20    /// Package version
21    pub version: String,
22
23    /// List of binary targets
24    pub binaries: Vec<BinaryTarget>,
25
26    /// Whether this is a library
27    pub has_lib: bool,
28
29    /// Library name (if different from package name)
30    pub lib_name: Option<String>,
31
32    /// List of examples
33    pub examples: Vec<String>,
34
35    /// Edition (2021, 2024, etc.)
36    pub edition: String,
37
38    /// Whether this is a workspace root
39    pub is_workspace: bool,
40
41    /// Workspace members (if workspace)
42    pub workspace_members: Vec<String>,
43}
44
45impl CargoMetadata {
46    /// Creates an empty CargoMetadata with defaults
47    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
62/// Walks up from path to find Cargo.toml.
63pub(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 the start path is a file, go to its parent
67    if current.is_file() {
68        current = current.parent()?.to_path_buf();
69    }
70
71    // Walk up the directory tree
72    loop {
73        let cargo_toml = current.join("Cargo.toml");
74        if cargo_toml.exists() {
75            return Some(cargo_toml);
76        }
77
78        // Move to parent directory
79        match current.parent() {
80            Some(parent) if parent != current => {
81                current = parent.to_path_buf();
82            }
83            _ => return None,
84        }
85    }
86}
87
88/// Internal TOML structures for parsing
89mod 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
159/// Parses a Cargo.toml file into CargoMetadata.
160pub(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    // Parse package information
181    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(), // Default for workspace inheritance
186            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(), // Default for workspace inheritance
191            None => "2021".to_string(),
192        };
193    }
194
195    // Check for library at root level
196    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    // Parse binary targets at root level
202    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    // Parse examples at root level
210    for example in toml_data.example {
211        metadata.examples.push(example.name);
212    }
213
214    // Check for workspace
215    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
223/// Determines the target directory (respects CARGO_TARGET_DIR and .cargo/config.toml).
224pub(crate) fn get_target_dir<P: AsRef<Path>>(project_root: P) -> PathBuf {
225    // First check environment variable
226    if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
227        return PathBuf::from(target_dir);
228    }
229
230    // For workspace packages, walk up to find workspace root
231    let mut current = project_root.as_ref().to_path_buf();
232    loop {
233        let target_candidate = current.join("target");
234
235        // Check if this is a workspace root by looking for Cargo.toml with [workspace]
236        if let Ok(cargo_content) = std::fs::read_to_string(current.join("Cargo.toml")) {
237            if cargo_content.contains("[workspace]") {
238                // This is the workspace root
239                return target_candidate;
240            }
241        }
242
243        // Try parent directory
244        match current.parent() {
245            Some(parent) if parent != current => {
246                current = parent.to_path_buf();
247            }
248            _ => {
249                // Reached filesystem root, use project_root/target as fallback
250                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}