1use crate::Error;
4use anyhow;
5pub use cargo_toml::{Dependency, LtoSetting, Manifest, Profile, Profiles};
6use glob::glob;
7use std::{
8 fs::write,
9 path::{Path, PathBuf},
10};
11
12pub fn from_path(path: &Path) -> Result<Manifest, Error> {
18 let path = match path.ends_with("Cargo.toml") {
20 true => path.to_path_buf(),
21 false => path.join("Cargo.toml"),
22 };
23 if !path.is_file() {
24 return Err(Error::ManifestPath(path.display().to_string()));
25 }
26 Ok(Manifest::from_path(path.canonicalize()?)?)
27}
28
29pub fn get_workspace_project_names(project_path: &Path) -> Result<Vec<(String, PathBuf)>, Error> {
34 let mut result = Vec::new();
35
36 let manifest = from_path(project_path)?;
38 let workspace = manifest
39 .workspace
40 .as_ref()
41 .ok_or_else(|| Error::Config("Manifest is not a workspace manifest".into()))?;
42
43 for member in &workspace.members {
45 for entry in glob(&project_path.join(member).to_string_lossy())
47 .map_err(|e| Error::Config(format!("Invalid glob pattern '{}': {}", member, e)))?
48 .filter_map(Result::ok)
49 {
50 let member_manifest_path = entry.join("Cargo.toml");
51 if member_manifest_path.is_file() {
52 if let Ok(member_manifest) = from_path(&member_manifest_path) &&
54 let Some(package) = &member_manifest.package
55 {
56 result.push((package.name.clone(), entry));
57 }
58 }
59 }
60 }
61
62 Ok(result)
63}
64
65pub fn add_production_profile(project: &Path) -> anyhow::Result<()> {
70 let root_toml_path = project.join("Cargo.toml");
71 let mut manifest = Manifest::from_path(&root_toml_path)?;
72 if manifest.profile.custom.contains_key("production") {
74 return Ok(());
75 }
76 let production_profile = Profile {
78 opt_level: None,
79 debug: None,
80 split_debuginfo: None,
81 rpath: None,
82 lto: Some(LtoSetting::Fat),
83 debug_assertions: None,
84 codegen_units: Some(1),
85 panic: None,
86 incremental: None,
87 overflow_checks: None,
88 strip: None,
89 package: std::collections::BTreeMap::new(),
90 build_override: None,
91 inherits: Some("release".to_string()),
92 };
93 manifest.profile.custom.insert("production".to_string(), production_profile);
95
96 let toml_string = toml::to_string(&manifest)?;
98 write(&root_toml_path, toml_string)?;
99
100 Ok(())
101}
102
103pub fn add_feature(project: &Path, (key, items): (String, Vec<String>)) -> anyhow::Result<()> {
109 let root_toml_path = project.join("Cargo.toml");
110 let mut manifest = Manifest::from_path(&root_toml_path)?;
111 if manifest.features.contains_key(&key) {
113 return Ok(());
114 }
115 manifest.features.insert(key, items);
116
117 let toml_string = toml::to_string(&manifest)?;
119 write(&root_toml_path, toml_string)?;
120
121 Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use std::fs::{File, read_to_string, write};
128 use tempfile::TempDir;
129
130 struct TestBuilder {
131 main_tempdir: TempDir,
132 workspace: Option<TempDir>,
133 workspace_cargo_toml: Option<PathBuf>,
134 }
135
136 impl Default for TestBuilder {
137 fn default() -> Self {
138 Self {
139 main_tempdir: TempDir::new().expect("Failed to create tempdir"),
140 workspace: None,
141 workspace_cargo_toml: None,
142 }
143 }
144 }
145
146 impl TestBuilder {
147 fn add_workspace(self) -> Self {
148 Self { workspace: TempDir::new_in(self.main_tempdir.as_ref()).ok(), ..self }
149 }
150
151 fn add_workspace_cargo_toml(self, cargo_toml_content: &str) -> Self {
152 let workspace_cargo_toml = self
153 .workspace
154 .as_ref()
155 .expect("add_workspace_cargo_toml is only callable if workspace has been created")
156 .path()
157 .join("Cargo.toml");
158 File::create(&workspace_cargo_toml).expect("Failed to create Cargo.toml");
159 write(&workspace_cargo_toml, cargo_toml_content).expect("Failed to write Cargo.toml");
160 Self { workspace_cargo_toml: Some(workspace_cargo_toml.to_path_buf()), ..self }
161 }
162 }
163
164 #[test]
165 fn from_path_works() -> anyhow::Result<()> {
166 from_path(Path::new("../../"))?;
168 from_path(Path::new("../../Cargo.toml"))?;
170 from_path(Path::new("."))?;
172 from_path(Path::new("./Cargo.toml"))?;
174 Ok(())
175 }
176
177 #[test]
178 fn from_path_ensures_manifest_exists() -> Result<(), Error> {
179 assert!(matches!(from_path(Path::new("./none.toml")), Err(super::Error::ManifestPath(..))));
180 Ok(())
181 }
182
183 #[test]
184 fn add_production_profile_works() {
185 let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
186 r#"[profile.release]
187 opt-level = 3
188 "#,
189 );
190
191 let binding = test_builder.workspace.expect("Workspace should exist");
192 let project_path = binding.path();
193 let cargo_toml_path = test_builder.workspace_cargo_toml.clone().unwrap();
194
195 let result = add_production_profile(project_path);
197 assert!(result.is_ok());
198
199 let manifest =
201 Manifest::from_path(&cargo_toml_path).expect("Should parse updated Cargo.toml");
202 let production_profile = manifest
203 .profile
204 .custom
205 .get("production")
206 .expect("Production profile should exist");
207 assert_eq!(production_profile.codegen_units, Some(1));
208 assert_eq!(production_profile.inherits.as_deref(), Some("release"));
209 assert_eq!(production_profile.lto, Some(LtoSetting::Fat));
210
211 let initial_toml_content =
213 read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
214 let second_result = add_production_profile(project_path);
215 assert!(second_result.is_ok());
216 let final_toml_content =
217 read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
218 assert_eq!(initial_toml_content, final_toml_content);
219 }
220
221 #[test]
222 fn add_feature_works() {
223 let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
224 r#"[profile.release]
225 opt-level = 3
226 "#,
227 );
228
229 let expected_feature_key = "runtime-benchmarks";
230 let expected_feature_items =
231 vec!["feature-a".to_string(), "feature-b".to_string(), "feature-c".to_string()];
232 let binding = test_builder.workspace.expect("Workspace should exist");
233 let project_path = binding.path();
234 let cargo_toml_path = test_builder.workspace_cargo_toml.clone().unwrap();
235
236 let result = add_feature(
238 project_path,
239 (expected_feature_key.to_string(), expected_feature_items.clone()),
240 );
241 assert!(result.is_ok());
242
243 let manifest =
245 Manifest::from_path(&cargo_toml_path).expect("Should parse updated Cargo.toml");
246 let feature_items = manifest
247 .features
248 .get(expected_feature_key)
249 .expect("Production profile should exist");
250 assert_eq!(feature_items, &expected_feature_items);
251
252 let initial_toml_content =
254 read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
255 let second_result = add_feature(
256 project_path,
257 (expected_feature_key.to_string(), expected_feature_items.clone()),
258 );
259 assert!(second_result.is_ok());
260 let final_toml_content =
261 read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
262 assert_eq!(initial_toml_content, final_toml_content);
263 }
264
265 #[test]
266 fn get_workspace_project_names_works() {
267 let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
268 r#"[workspace]
269members = ["crate1", "crate2"]
270
271[workspace.package]
272name = "test-workspace"
273"#,
274 );
275
276 let binding = test_builder.workspace.expect("Workspace should exist");
277 let workspace_path = binding.path();
278
279 let crate1_path = workspace_path.join("crate1");
281 std::fs::create_dir(&crate1_path).expect("Should create crate1 directory");
282 write(
283 crate1_path.join("Cargo.toml"),
284 r#"[package]
285name = "crate1"
286version = "0.1.0"
287"#,
288 )
289 .expect("Should write crate1 Cargo.toml");
290
291 let crate2_path = workspace_path.join("crate2");
292 std::fs::create_dir(&crate2_path).expect("Should create crate2 directory");
293 write(
294 crate2_path.join("Cargo.toml"),
295 r#"[package]
296name = "crate2"
297version = "0.1.0"
298"#,
299 )
300 .expect("Should write crate2 Cargo.toml");
301
302 let result = get_workspace_project_names(workspace_path).expect("Should succeed");
303 assert_eq!(result.len(), 2);
304
305 let names: Vec<String> = result.iter().map(|(name, _)| name.clone()).collect();
307 assert!(names.contains(&"crate1".to_string()));
308 assert!(names.contains(&"crate2".to_string()));
309
310 let paths: Vec<PathBuf> = result.iter().map(|(_, path)| path.clone()).collect();
312 assert!(paths.contains(&crate1_path));
313 assert!(paths.contains(&crate2_path));
314 }
315
316 #[test]
317 fn get_workspace_project_names_with_glob_patterns_works() {
318 let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
319 r#"[workspace]
320members = ["crates/*"]
321
322[workspace.package]
323name = "test-workspace"
324"#,
325 );
326
327 let binding = test_builder.workspace.expect("Workspace should exist");
328 let workspace_path = binding.path();
329
330 let crates_dir = workspace_path.join("crates");
332 std::fs::create_dir(&crates_dir).expect("Should create crates directory");
333
334 let crate1_path = crates_dir.join("crate1");
336 std::fs::create_dir(&crate1_path).expect("Should create crate1 directory");
337 write(
338 crate1_path.join("Cargo.toml"),
339 r#"[package]
340name = "crate1"
341version = "0.1.0"
342"#,
343 )
344 .expect("Should write crate1 Cargo.toml");
345
346 let crate2_path = crates_dir.join("crate2");
347 std::fs::create_dir(&crate2_path).expect("Should create crate2 directory");
348 write(
349 crate2_path.join("Cargo.toml"),
350 r#"[package]
351name = "crate2"
352version = "0.1.0"
353"#,
354 )
355 .expect("Should write crate2 Cargo.toml");
356
357 let result = get_workspace_project_names(workspace_path).expect("Should succeed");
358 assert_eq!(result.len(), 2);
359
360 let names: Vec<String> = result.iter().map(|(name, _)| name.clone()).collect();
362 assert!(names.contains(&"crate1".to_string()));
363 assert!(names.contains(&"crate2".to_string()));
364 }
365
366 #[test]
367 fn get_workspace_project_names_fails_for_non_workspace() {
368 let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
369 r#"[package]
370name = "not-a-workspace"
371version = "0.1.0"
372"#,
373 );
374
375 let binding = test_builder.workspace.expect("Workspace should exist");
376 let workspace_path = binding.path();
377
378 let result = get_workspace_project_names(workspace_path);
379 assert!(result.is_err());
380 assert!(matches!(result.unwrap_err(), Error::Config(_)));
381 }
382
383 #[test]
384 fn get_workspace_project_names_returns_empty_for_no_members() {
385 let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
386 r#"[workspace]
387members = []
388
389[workspace.package]
390name = "test-workspace"
391"#,
392 );
393
394 let binding = test_builder.workspace.expect("Workspace should exist");
395 let workspace_path = binding.path();
396
397 let result = get_workspace_project_names(workspace_path).expect("Should succeed");
398 assert_eq!(result.len(), 0);
399 }
400}