use crate::Error;
use anyhow;
pub use cargo_toml::{Dependency, LtoSetting, Manifest, Profile, Profiles};
use glob::glob;
use std::{
fs::write,
path::{Path, PathBuf},
};
pub fn from_path(path: &Path) -> Result<Manifest, Error> {
let path = match path.ends_with("Cargo.toml") {
true => path.to_path_buf(),
false => path.join("Cargo.toml"),
};
if !path.is_file() {
return Err(Error::ManifestPath(path.display().to_string()));
}
Ok(Manifest::from_path(path.canonicalize()?)?)
}
pub fn get_workspace_project_names(project_path: &Path) -> Result<Vec<(String, PathBuf)>, Error> {
let mut result = Vec::new();
let manifest = from_path(project_path)?;
let workspace = manifest
.workspace
.as_ref()
.ok_or_else(|| Error::Config("Manifest is not a workspace manifest".into()))?;
for member in &workspace.members {
for entry in glob(&project_path.join(member).to_string_lossy())
.map_err(|e| Error::Config(format!("Invalid glob pattern '{}': {}", member, e)))?
.filter_map(Result::ok)
{
let member_manifest_path = entry.join("Cargo.toml");
if member_manifest_path.is_file() {
if let Ok(member_manifest) = from_path(&member_manifest_path) &&
let Some(package) = &member_manifest.package
{
result.push((package.name.clone(), entry));
}
}
}
}
Ok(result)
}
pub fn add_production_profile(project: &Path) -> anyhow::Result<()> {
let root_toml_path = project.join("Cargo.toml");
let mut manifest = Manifest::from_path(&root_toml_path)?;
if manifest.profile.custom.contains_key("production") {
return Ok(());
}
let production_profile = Profile {
opt_level: None,
debug: None,
split_debuginfo: None,
rpath: None,
lto: Some(LtoSetting::Fat),
debug_assertions: None,
codegen_units: Some(1),
panic: None,
incremental: None,
overflow_checks: None,
strip: None,
package: std::collections::BTreeMap::new(),
build_override: None,
inherits: Some("release".to_string()),
};
manifest.profile.custom.insert("production".to_string(), production_profile);
let toml_string = toml::to_string(&manifest)?;
write(&root_toml_path, toml_string)?;
Ok(())
}
pub fn add_feature(project: &Path, (key, items): (String, Vec<String>)) -> anyhow::Result<()> {
let root_toml_path = project.join("Cargo.toml");
let mut manifest = Manifest::from_path(&root_toml_path)?;
if manifest.features.contains_key(&key) {
return Ok(());
}
manifest.features.insert(key, items);
let toml_string = toml::to_string(&manifest)?;
write(&root_toml_path, toml_string)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{File, read_to_string, write};
use tempfile::TempDir;
struct TestBuilder {
main_tempdir: TempDir,
workspace: Option<TempDir>,
workspace_cargo_toml: Option<PathBuf>,
}
impl Default for TestBuilder {
fn default() -> Self {
Self {
main_tempdir: TempDir::new().expect("Failed to create tempdir"),
workspace: None,
workspace_cargo_toml: None,
}
}
}
impl TestBuilder {
fn add_workspace(self) -> Self {
Self { workspace: TempDir::new_in(self.main_tempdir.as_ref()).ok(), ..self }
}
fn add_workspace_cargo_toml(self, cargo_toml_content: &str) -> Self {
let workspace_cargo_toml = self
.workspace
.as_ref()
.expect("add_workspace_cargo_toml is only callable if workspace has been created")
.path()
.join("Cargo.toml");
File::create(&workspace_cargo_toml).expect("Failed to create Cargo.toml");
write(&workspace_cargo_toml, cargo_toml_content).expect("Failed to write Cargo.toml");
Self { workspace_cargo_toml: Some(workspace_cargo_toml.to_path_buf()), ..self }
}
}
#[test]
fn from_path_works() -> anyhow::Result<()> {
from_path(Path::new("../../"))?;
from_path(Path::new("../../Cargo.toml"))?;
from_path(Path::new("."))?;
from_path(Path::new("./Cargo.toml"))?;
Ok(())
}
#[test]
fn from_path_ensures_manifest_exists() -> Result<(), Error> {
assert!(matches!(from_path(Path::new("./none.toml")), Err(super::Error::ManifestPath(..))));
Ok(())
}
#[test]
fn add_production_profile_works() {
let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
r#"[profile.release]
opt-level = 3
"#,
);
let binding = test_builder.workspace.expect("Workspace should exist");
let project_path = binding.path();
let cargo_toml_path = test_builder.workspace_cargo_toml.clone().unwrap();
let result = add_production_profile(project_path);
assert!(result.is_ok());
let manifest =
Manifest::from_path(&cargo_toml_path).expect("Should parse updated Cargo.toml");
let production_profile = manifest
.profile
.custom
.get("production")
.expect("Production profile should exist");
assert_eq!(production_profile.codegen_units, Some(1));
assert_eq!(production_profile.inherits.as_deref(), Some("release"));
assert_eq!(production_profile.lto, Some(LtoSetting::Fat));
let initial_toml_content =
read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
let second_result = add_production_profile(project_path);
assert!(second_result.is_ok());
let final_toml_content =
read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
assert_eq!(initial_toml_content, final_toml_content);
}
#[test]
fn add_feature_works() {
let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
r#"[profile.release]
opt-level = 3
"#,
);
let expected_feature_key = "runtime-benchmarks";
let expected_feature_items =
vec!["feature-a".to_string(), "feature-b".to_string(), "feature-c".to_string()];
let binding = test_builder.workspace.expect("Workspace should exist");
let project_path = binding.path();
let cargo_toml_path = test_builder.workspace_cargo_toml.clone().unwrap();
let result = add_feature(
project_path,
(expected_feature_key.to_string(), expected_feature_items.clone()),
);
assert!(result.is_ok());
let manifest =
Manifest::from_path(&cargo_toml_path).expect("Should parse updated Cargo.toml");
let feature_items = manifest
.features
.get(expected_feature_key)
.expect("Production profile should exist");
assert_eq!(feature_items, &expected_feature_items);
let initial_toml_content =
read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
let second_result = add_feature(
project_path,
(expected_feature_key.to_string(), expected_feature_items.clone()),
);
assert!(second_result.is_ok());
let final_toml_content =
read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
assert_eq!(initial_toml_content, final_toml_content);
}
#[test]
fn get_workspace_project_names_works() {
let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
r#"[workspace]
members = ["crate1", "crate2"]
[workspace.package]
name = "test-workspace"
"#,
);
let binding = test_builder.workspace.expect("Workspace should exist");
let workspace_path = binding.path();
let crate1_path = workspace_path.join("crate1");
std::fs::create_dir(&crate1_path).expect("Should create crate1 directory");
write(
crate1_path.join("Cargo.toml"),
r#"[package]
name = "crate1"
version = "0.1.0"
"#,
)
.expect("Should write crate1 Cargo.toml");
let crate2_path = workspace_path.join("crate2");
std::fs::create_dir(&crate2_path).expect("Should create crate2 directory");
write(
crate2_path.join("Cargo.toml"),
r#"[package]
name = "crate2"
version = "0.1.0"
"#,
)
.expect("Should write crate2 Cargo.toml");
let result = get_workspace_project_names(workspace_path).expect("Should succeed");
assert_eq!(result.len(), 2);
let names: Vec<String> = result.iter().map(|(name, _)| name.clone()).collect();
assert!(names.contains(&"crate1".to_string()));
assert!(names.contains(&"crate2".to_string()));
let paths: Vec<PathBuf> = result.iter().map(|(_, path)| path.clone()).collect();
assert!(paths.contains(&crate1_path));
assert!(paths.contains(&crate2_path));
}
#[test]
fn get_workspace_project_names_with_glob_patterns_works() {
let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
r#"[workspace]
members = ["crates/*"]
[workspace.package]
name = "test-workspace"
"#,
);
let binding = test_builder.workspace.expect("Workspace should exist");
let workspace_path = binding.path();
let crates_dir = workspace_path.join("crates");
std::fs::create_dir(&crates_dir).expect("Should create crates directory");
let crate1_path = crates_dir.join("crate1");
std::fs::create_dir(&crate1_path).expect("Should create crate1 directory");
write(
crate1_path.join("Cargo.toml"),
r#"[package]
name = "crate1"
version = "0.1.0"
"#,
)
.expect("Should write crate1 Cargo.toml");
let crate2_path = crates_dir.join("crate2");
std::fs::create_dir(&crate2_path).expect("Should create crate2 directory");
write(
crate2_path.join("Cargo.toml"),
r#"[package]
name = "crate2"
version = "0.1.0"
"#,
)
.expect("Should write crate2 Cargo.toml");
let result = get_workspace_project_names(workspace_path).expect("Should succeed");
assert_eq!(result.len(), 2);
let names: Vec<String> = result.iter().map(|(name, _)| name.clone()).collect();
assert!(names.contains(&"crate1".to_string()));
assert!(names.contains(&"crate2".to_string()));
}
#[test]
fn get_workspace_project_names_fails_for_non_workspace() {
let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
r#"[package]
name = "not-a-workspace"
version = "0.1.0"
"#,
);
let binding = test_builder.workspace.expect("Workspace should exist");
let workspace_path = binding.path();
let result = get_workspace_project_names(workspace_path);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::Config(_)));
}
#[test]
fn get_workspace_project_names_returns_empty_for_no_members() {
let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
r#"[workspace]
members = []
[workspace.package]
name = "test-workspace"
"#,
);
let binding = test_builder.workspace.expect("Workspace should exist");
let workspace_path = binding.path();
let result = get_workspace_project_names(workspace_path).expect("Should succeed");
assert_eq!(result.len(), 0);
}
}