use crate::rust_builder::error::{BuilderResult, RustBuilderError};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BinaryTarget {
pub name: String,
pub path: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CargoMetadata {
pub name: String,
pub version: String,
pub binaries: Vec<BinaryTarget>,
pub has_lib: bool,
pub lib_name: Option<String>,
pub examples: Vec<String>,
pub edition: String,
pub is_workspace: bool,
pub workspace_members: Vec<String>,
}
impl CargoMetadata {
pub fn empty() -> Self {
Self {
name: String::new(),
version: String::new(),
binaries: Vec::new(),
has_lib: false,
lib_name: None,
examples: Vec::new(),
edition: "2021".to_string(),
is_workspace: false,
workspace_members: Vec::new(),
}
}
}
pub(crate) fn find_cargo_toml<P: AsRef<Path>>(start: P) -> Option<PathBuf> {
let mut current = start.as_ref().to_path_buf();
if current.is_file() {
current = current.parent()?.to_path_buf();
}
loop {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists() {
return Some(cargo_toml);
}
match current.parent() {
Some(parent) if parent != current => {
current = parent.to_path_buf();
}
_ => return None,
}
}
}
mod toml_models {
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct CargoToml {
pub package: Option<Package>,
pub workspace: Option<Workspace>,
#[serde(default)]
pub bin: Vec<BinTarget>,
#[serde(default)]
pub example: Vec<ExampleTarget>,
#[serde(default)]
pub lib: Option<LibTarget>,
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum Version {
String(String),
#[serde(rename_all = "lowercase")]
Workspace { #[allow(dead_code)] workspace: bool },
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum Edition {
String(String),
#[serde(rename_all = "lowercase")]
Workspace { #[allow(dead_code)] workspace: bool },
}
#[derive(Deserialize, Debug)]
pub struct Package {
pub name: String,
#[serde(default)]
pub version: Option<Version>,
#[serde(default)]
pub edition: Option<Edition>,
}
#[derive(Deserialize, Debug)]
pub struct Workspace {
#[serde(default)]
pub members: Vec<String>,
#[serde(default)]
#[allow(dead_code)]
pub exclude: Vec<String>,
}
#[derive(Deserialize, Debug)]
pub struct LibTarget {
pub name: Option<String>,
#[allow(dead_code)]
pub path: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct BinTarget {
pub name: String,
pub path: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct ExampleTarget {
pub name: String,
#[allow(dead_code)]
pub path: Option<String>,
}
}
pub(crate) fn parse_cargo_toml<P: AsRef<Path>>(path: P) -> BuilderResult<CargoMetadata> {
let path = path.as_ref();
if !path.exists() {
return Err(RustBuilderError::PathNotFound {
path: path.to_path_buf(),
});
}
let content = std::fs::read_to_string(path).map_err(|e| RustBuilderError::Io(e))?;
let toml_data: toml_models::CargoToml = toml::from_str(&content).map_err(|e| {
RustBuilderError::CargoTomlParseError {
path: path.to_path_buf(),
message: e.to_string(),
}
})?;
let mut metadata = CargoMetadata::empty();
if let Some(package) = toml_data.package {
metadata.name = package.name;
metadata.version = match package.version {
Some(toml_models::Version::String(v)) => v,
Some(toml_models::Version::Workspace { .. }) => "0.0.0".to_string(), None => "0.0.0".to_string(),
};
metadata.edition = match package.edition {
Some(toml_models::Edition::String(e)) => e,
Some(toml_models::Edition::Workspace { .. }) => "2021".to_string(), None => "2021".to_string(),
};
}
metadata.has_lib = toml_data.lib.is_some();
if let Some(lib) = toml_data.lib {
metadata.lib_name = lib.name;
}
for bin in toml_data.bin {
metadata.binaries.push(BinaryTarget {
name: bin.name,
path: bin.path.map(PathBuf::from),
});
}
for example in toml_data.example {
metadata.examples.push(example.name);
}
if let Some(workspace) = toml_data.workspace {
metadata.is_workspace = true;
metadata.workspace_members = workspace.members;
}
Ok(metadata)
}
pub(crate) fn get_target_dir<P: AsRef<Path>>(project_root: P) -> PathBuf {
if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
return PathBuf::from(target_dir);
}
let mut current = project_root.as_ref().to_path_buf();
loop {
let target_candidate = current.join("target");
if let Ok(cargo_content) = std::fs::read_to_string(current.join("Cargo.toml")) {
if cargo_content.contains("[workspace]") {
return target_candidate;
}
}
match current.parent() {
Some(parent) if parent != current => {
current = parent.to_path_buf();
}
_ => {
return project_root.as_ref().join("target");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_find_cargo_toml_in_current_dir() {
let temp_dir = tempdir().unwrap();
let cargo_path = temp_dir.path().join("Cargo.toml");
fs::write(&cargo_path, "").unwrap();
let found = find_cargo_toml(temp_dir.path());
assert_eq!(found, Some(cargo_path));
}
#[test]
fn test_find_cargo_toml_walking_up() {
let temp_dir = tempdir().unwrap();
let cargo_path = temp_dir.path().join("Cargo.toml");
fs::write(&cargo_path, "").unwrap();
let sub_dir = temp_dir.path().join("src");
fs::create_dir(&sub_dir).unwrap();
let found = find_cargo_toml(&sub_dir);
assert_eq!(found, Some(cargo_path));
}
#[test]
fn test_find_cargo_toml_from_file() {
let temp_dir = tempdir().unwrap();
let cargo_path = temp_dir.path().join("Cargo.toml");
fs::write(&cargo_path, "").unwrap();
let src_dir = temp_dir.path().join("src");
fs::create_dir(&src_dir).unwrap();
let main_file = src_dir.join("main.rs");
fs::write(&main_file, "").unwrap();
let found = find_cargo_toml(&main_file);
assert_eq!(found, Some(cargo_path));
}
#[test]
fn test_parse_cargo_toml_basic() {
let temp_dir = tempdir().unwrap();
let cargo_path = temp_dir.path().join("Cargo.toml");
let content = r#"
[package]
name = "test-project"
version = "1.0.0"
edition = "2021"
[[bin]]
name = "test-app"
path = "src/main.rs"
"#;
fs::write(&cargo_path, content).unwrap();
let metadata = parse_cargo_toml(&cargo_path).unwrap();
assert_eq!(metadata.name, "test-project");
assert_eq!(metadata.version, "1.0.0");
assert_eq!(metadata.edition, "2021");
assert_eq!(metadata.binaries.len(), 1);
assert_eq!(metadata.binaries[0].name, "test-app");
}
#[test]
fn test_parse_cargo_toml_with_examples() {
let temp_dir = tempdir().unwrap();
let cargo_path = temp_dir.path().join("Cargo.toml");
let content = r#"
[package]
name = "example-project"
version = "0.5.0"
edition = "2021"
[[example]]
name = "demo"
[[example]]
name = "advanced"
"#;
fs::write(&cargo_path, content).unwrap();
let metadata = parse_cargo_toml(&cargo_path).unwrap();
assert_eq!(metadata.name, "example-project");
assert_eq!(metadata.examples.len(), 2);
assert!(metadata.examples.contains(&"demo".to_string()));
assert!(metadata.examples.contains(&"advanced".to_string()));
}
}