use anyhow::{Context, Result};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CargoToml {
pub package: Option<TomlPackage>,
pub workspace: Option<TomlWorkspace>,
pub lib: Option<TomlLib>,
#[serde(default)]
pub bin: Vec<TomlBin>,
#[serde(default)]
pub dependencies: HashMap<String, TomlDependency>,
#[serde(default, rename = "build-dependencies")]
pub build_dependencies: HashMap<String, TomlDependency>,
#[serde(default, rename = "dev-dependencies", alias = "dev_dependencies")]
pub dev_dependencies: HashMap<String, TomlDependency>,
#[serde(default)]
pub test: Vec<TomlTest>,
#[serde(default)]
pub example: Vec<TomlTest>,
#[serde(default)]
pub bench: Vec<TomlTest>,
#[serde(default)]
pub features: HashMap<String, Vec<String>>,
#[serde(default)]
pub target: HashMap<String, TomlTargetSpec>,
#[serde(default)]
pub lints: Option<toml::Value>,
}
pub use super::manifest_models::*;
impl CargoToml {
pub fn parse(content: &str) -> Result<Self> {
toml::from_str(content).context("Failed to parse Cargo.toml content")
}
pub fn from_file(path: &Path) -> Result<Self> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read manifest at {:?}", path))?;
let mut toml = Self::parse(&content)?;
toml.resolve_inherited_workspace_fields(path);
Ok(toml)
}
pub fn resolve_inherited_workspace_fields(&mut self, path: &Path) {
super::manifest_inherit::resolve_inherited_workspace_fields(self, path);
}
pub fn effective_resolver(&self) -> &str {
if let Some(ref ws) = self.workspace {
if let Some(ref r) = ws.resolver {
return r.as_str();
}
}
if let Some(ref pkg) = self.package {
if let Some(ref r) = pkg.resolver {
return r.as_str();
}
if let Some(ref ed) = pkg.edition {
if ed == "2021" || ed == "2024" {
return "2";
}
}
}
"1"
}
pub fn has_build_script(&self, pkg_dir: &Path) -> bool {
if let Some(ref pkg) = self.package {
if let Some(ref b) = pkg.build {
return match b {
TomlBuild::Disabled(false) => false,
TomlBuild::Disabled(true) => pkg_dir.join("build.rs").is_file(),
TomlBuild::Path(ref p) => pkg_dir.join(p).is_file(),
};
}
}
pkg_dir.join("build.rs").is_file()
}
pub fn dependency_aliases(&self) -> DependencyAliases {
let mut res = DependencyAliases::default();
let mut record = |alias: &str, dep: &TomlDependency| {
if let TomlDependency::Detailed(ref d) = dep {
if let Some(ref real) = d.package_name {
res.alias_to_real.insert(alias.to_string(), real.clone());
res.alias_to_real
.insert(alias.replace('-', "_"), real.clone());
res.real_to_aliases
.entry(real.clone())
.or_default()
.push((alias.to_string(), d.version.clone()));
let norm = real.replace('-', "_");
if norm != *real {
res.real_to_aliases
.entry(norm)
.or_default()
.push((alias.to_string(), d.version.clone()));
}
}
}
};
for (alias, dep) in &self.dependencies {
record(alias, dep);
}
for (alias, dep) in &self.build_dependencies {
record(alias, dep);
}
for (alias, dep) in &self.dev_dependencies {
record(alias, dep);
}
for target_spec in self.target.values() {
for (alias, dep) in &target_spec.dependencies {
record(alias, dep);
}
for (alias, dep) in &target_spec.build_dependencies {
record(alias, dep);
}
for (alias, dep) in &target_spec.dev_dependencies {
record(alias, dep);
}
}
res
}
pub fn has_direct_dep_named(&self, pkg_name: &str, dep_version: Option<&str>) -> bool {
let norm = pkg_name.replace('-', "_");
let matches_dep = |alias: &str, dep: &TomlDependency| -> bool {
let alias_norm = alias.replace('-', "_");
if alias_norm != norm {
return false;
}
match dep {
TomlDependency::Simple(req_str) => {
if let Some(ver_str) = dep_version {
match (
semver::VersionReq::parse(req_str),
semver::Version::parse(ver_str),
) {
(Ok(req), Ok(ver)) => req.matches(&ver),
_ => ver_str.starts_with(req_str) || req_str == ver_str,
}
} else {
true
}
}
TomlDependency::Detailed(d) => {
let is_unaliased = match d.package_name {
None => true,
Some(ref p) => p.replace('-', "_") == norm,
};
if !is_unaliased {
return false;
}
if let (Some(req_str), Some(ver_str)) = (&d.version, dep_version) {
match (
semver::VersionReq::parse(req_str),
semver::Version::parse(ver_str),
) {
(Ok(req), Ok(ver)) => req.matches(&ver),
_ => ver_str.starts_with(req_str) || req_str == ver_str,
}
} else {
true
}
}
}
};
for (alias, dep) in &self.dependencies {
if matches_dep(alias, dep) {
return true;
}
}
for (alias, dep) in &self.build_dependencies {
if matches_dep(alias, dep) {
return true;
}
}
for (alias, dep) in &self.dev_dependencies {
if matches_dep(alias, dep) {
return true;
}
}
for target_spec in self.target.values() {
for (alias, dep) in &target_spec.dependencies {
if matches_dep(alias, dep) {
return true;
}
}
for (alias, dep) in &target_spec.build_dependencies {
if matches_dep(alias, dep) {
return true;
}
}
for (alias, dep) in &target_spec.dev_dependencies {
if matches_dep(alias, dep) {
return true;
}
}
}
false
}
pub fn discover_targets(&self, pkg_dir: &Path) -> Vec<DiscoveredTarget> {
let mut targets = Vec::new();
let pkg_name = self.package.as_ref().map(|p| p.name.as_str()).unwrap_or("");
let edition = self.package.as_ref().and_then(|p| p.edition.clone());
if let Some(ref lib) = self.lib {
let lib_name = lib
.name
.clone()
.unwrap_or_else(|| pkg_name.replace('-', "_"));
let src = match lib.path {
Some(ref p) => pkg_dir.join(p),
None => pkg_dir.join("src").join("lib.rs"),
};
let kind = if lib.proc_macro.unwrap_or(false) {
"proc-macro".to_string()
} else {
"lib".to_string()
};
let crate_types = if !lib.crate_type.is_empty() {
lib.crate_type.clone()
} else {
vec![kind.clone()]
};
if src.is_file() {
targets.push(DiscoveredTarget {
name: lib_name,
kind,
src_path: src,
edition: edition.clone(),
crate_types,
required_features: None,
harness: self.lib.as_ref().and_then(|l| l.harness).unwrap_or(true),
test_role: None,
explicit_only: false,
});
}
} else {
let default_lib = pkg_dir.join("src").join("lib.rs");
if default_lib.is_file() {
targets.push(DiscoveredTarget {
name: pkg_name.replace('-', "_"),
kind: "lib".to_string(),
src_path: default_lib,
edition: edition.clone(),
crate_types: vec!["lib".to_string()],
required_features: None,
harness: true,
test_role: None,
explicit_only: false,
});
}
}
if !self.bin.is_empty() {
for b in &self.bin {
let src = match b.path {
Some(ref p) => pkg_dir.join(p),
None => {
let candidate1 = pkg_dir
.join("src")
.join("bin")
.join(format!("{}.rs", b.name));
if candidate1.is_file() {
candidate1
} else {
pkg_dir.join("src").join("main.rs")
}
}
};
if src.is_file() {
targets.push(DiscoveredTarget {
name: b.name.clone(),
kind: "bin".to_string(),
src_path: src,
edition: edition.clone(),
crate_types: vec!["bin".to_string()],
required_features: b.required_features.clone(),
harness: b.harness.unwrap_or(true),
test_role: None,
explicit_only: false,
});
}
}
} else {
let default_main = pkg_dir.join("src").join("main.rs");
if default_main.is_file() && !pkg_name.is_empty() {
targets.push(DiscoveredTarget {
name: pkg_name.to_string(),
kind: "bin".to_string(),
src_path: default_main,
edition: edition.clone(),
crate_types: vec!["bin".to_string()],
required_features: None,
harness: true,
test_role: None,
explicit_only: false,
});
}
let autobins = self
.package
.as_ref()
.and_then(|p| p.autobins)
.unwrap_or(true);
if autobins {
let bin_dir = pkg_dir.join("src").join("bin");
if let Ok(entries) = fs::read_dir(&bin_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && path.extension().and_then(|e| e.to_str()) == Some("rs")
{
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
targets.push(DiscoveredTarget {
name: stem.to_string(),
kind: "bin".to_string(),
src_path: path,
edition: edition.clone(),
crate_types: vec!["bin".to_string()],
required_features: None,
harness: true,
test_role: None,
explicit_only: false,
});
}
} else if path.is_dir() {
let main_rs = path.join("main.rs");
if main_rs.is_file() {
if let Some(dir_name) = path.file_name().and_then(|s| s.to_str()) {
targets.push(DiscoveredTarget {
name: dir_name.to_string(),
kind: "bin".to_string(),
src_path: main_rs,
edition: edition.clone(),
crate_types: vec!["bin".to_string()],
required_features: None,
harness: true,
test_role: None,
explicit_only: false,
});
}
}
}
}
}
}
}
targets
}
}