1use crate::Error;
4use anyhow;
5pub use cargo_toml::{Dependency, LtoSetting, Manifest, Profile, Profiles};
6use std::{
7 fs::{read_to_string, write},
8 path::{Path, PathBuf},
9};
10use toml_edit::{value, Array, DocumentMut, Item, Value};
11
12pub fn from_path(path: Option<&Path>) -> Result<Manifest, Error> {
18 let path = match path {
20 Some(path) => match path.ends_with("Cargo.toml") {
21 true => path.to_path_buf(),
22 false => path.join("Cargo.toml"),
23 },
24 None => PathBuf::from("./Cargo.toml"),
25 };
26 if !path.exists() {
27 return Err(Error::ManifestPath(path.display().to_string()));
28 }
29 Ok(Manifest::from_path(path.canonicalize()?)?)
30}
31
32pub fn find_workspace_toml(target_dir: &Path) -> Option<PathBuf> {
38 let mut dir = target_dir;
39 while let Some(parent) = dir.parent() {
40 if parent.to_str() == Some("") {
44 return None;
45 }
46 let cargo_toml = parent.join("Cargo.toml");
47 if cargo_toml.exists() {
48 if let Ok(contents) = read_to_string(&cargo_toml) {
49 if contents.contains("[workspace]") {
50 return Some(cargo_toml);
51 }
52 }
53 }
54 dir = parent;
55 }
56 None
57}
58
59pub fn add_crate_to_workspace(workspace_toml: &Path, crate_path: &Path) -> anyhow::Result<()> {
65 let toml_contents = read_to_string(workspace_toml)?;
66 let mut doc = toml_contents.parse::<DocumentMut>()?;
67
68 let workspace_dir = workspace_toml.parent().expect("A file always lives inside a dir; qed");
70 let crate_relative_path = crate_path.strip_prefix(workspace_dir)?;
72
73 if let Some(Item::Table(workspace_table)) = doc.get_mut("workspace") {
74 if let Some(Item::Value(members_array)) = workspace_table.get_mut("members") {
75 if let Value::Array(array) = members_array {
76 let crate_relative_path =
77 crate_relative_path.to_str().expect("target's always a valid string; qed");
78 let already_in_array = array
79 .iter()
80 .any(|member| matches!(member.as_str(), Some(s) if s == crate_relative_path));
81 if !already_in_array {
82 array.push(crate_relative_path);
83 }
84 } else {
85 return Err(anyhow::anyhow!("Corrupted workspace"));
86 }
87 } else {
88 let mut toml_array = Array::new();
89 toml_array
90 .push(crate_relative_path.to_str().expect("target's always a valid string; qed"));
91 workspace_table["members"] = value(toml_array);
92 }
93 } else {
94 return Err(anyhow::anyhow!("Corrupted workspace"));
95 }
96
97 write(workspace_toml, doc.to_string())?;
98 Ok(())
99}
100
101pub fn add_production_profile(project: &Path) -> anyhow::Result<()> {
106 let root_toml_path = project.join("Cargo.toml");
107 let mut manifest = Manifest::from_path(&root_toml_path)?;
108 if manifest.profile.custom.contains_key("production") {
110 return Ok(());
111 }
112 let production_profile = Profile {
114 opt_level: None,
115 debug: None,
116 split_debuginfo: None,
117 rpath: None,
118 lto: Some(LtoSetting::Fat),
119 debug_assertions: None,
120 codegen_units: Some(1),
121 panic: None,
122 incremental: None,
123 overflow_checks: None,
124 strip: None,
125 package: std::collections::BTreeMap::new(),
126 build_override: None,
127 inherits: Some("release".to_string()),
128 };
129 manifest.profile.custom.insert("production".to_string(), production_profile);
131
132 let toml_string = toml::to_string(&manifest)?;
134 write(&root_toml_path, toml_string)?;
135
136 Ok(())
137}
138
139pub fn add_feature(project: &Path, (key, items): (String, Vec<String>)) -> anyhow::Result<()> {
145 let root_toml_path = project.join("Cargo.toml");
146 let mut manifest = Manifest::from_path(&root_toml_path)?;
147 if manifest.features.contains_key(&key) {
149 return Ok(());
150 }
151 manifest.features.insert(key, items);
152
153 let toml_string = toml::to_string(&manifest)?;
155 write(&root_toml_path, toml_string)?;
156
157 Ok(())
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use std::fs::{write, File};
164 use tempfile::TempDir;
165
166 struct TestBuilder {
167 main_tempdir: TempDir,
168 workspace: Option<TempDir>,
169 inside_workspace_dir: Option<TempDir>,
170 workspace_cargo_toml: Option<PathBuf>,
171 outside_workspace_dir: Option<TempDir>,
172 }
173
174 impl Default for TestBuilder {
175 fn default() -> Self {
176 Self {
177 main_tempdir: TempDir::new().expect("Failed to create tempdir"),
178 workspace: None,
179 inside_workspace_dir: None,
180 workspace_cargo_toml: None,
181 outside_workspace_dir: None,
182 }
183 }
184 }
185
186 impl TestBuilder {
187 fn add_workspace(self) -> Self {
188 Self { workspace: TempDir::new_in(self.main_tempdir.as_ref()).ok(), ..self }
189 }
190
191 fn add_inside_workspace_dir(self) -> Self {
192 Self {
193 inside_workspace_dir: TempDir::new_in(self.workspace.as_ref().expect(
194 "add_inside_workspace_dir is only callable if workspace has been created",
195 ))
196 .ok(),
197 ..self
198 }
199 }
200
201 fn add_workspace_cargo_toml(self, cargo_toml_content: &str) -> Self {
202 let workspace_cargo_toml = self
203 .workspace
204 .as_ref()
205 .expect("add_workspace_cargo_toml is only callable if workspace has been created")
206 .path()
207 .join("Cargo.toml");
208 File::create(&workspace_cargo_toml).expect("Failed to create Cargo.toml");
209 write(&workspace_cargo_toml, cargo_toml_content).expect("Failed to write Cargo.toml");
210 Self { workspace_cargo_toml: Some(workspace_cargo_toml.to_path_buf()), ..self }
211 }
212
213 fn add_outside_workspace_dir(self) -> Self {
214 Self { outside_workspace_dir: TempDir::new_in(self.main_tempdir.as_ref()).ok(), ..self }
215 }
216 }
217
218 #[test]
219 fn from_path_works() -> anyhow::Result<()> {
220 from_path(Some(Path::new("../../")))?;
222 from_path(Some(Path::new("../../Cargo.toml")))?;
224 from_path(Some(Path::new(".")))?;
226 from_path(Some(Path::new("./Cargo.toml")))?;
228 from_path(None)?;
230 Ok(())
231 }
232
233 #[test]
234 fn from_path_ensures_manifest_exists() -> Result<(), Error> {
235 assert!(matches!(
236 from_path(Some(Path::new("./none.toml"))),
237 Err(super::Error::ManifestPath(..))
238 ));
239 Ok(())
240 }
241
242 #[test]
243 fn find_workspace_toml_works_well() {
244 let test_builder = TestBuilder::default()
245 .add_workspace()
246 .add_inside_workspace_dir()
247 .add_workspace_cargo_toml(
248 r#"[workspace]
249 resolver = "2"
250 members = ["member1"]
251 "#,
252 )
253 .add_outside_workspace_dir();
254 assert!(find_workspace_toml(
255 test_builder
256 .inside_workspace_dir
257 .as_ref()
258 .expect("Inside workspace dir should exist")
259 .path()
260 )
261 .is_some());
262 assert_eq!(
263 find_workspace_toml(
264 test_builder
265 .inside_workspace_dir
266 .as_ref()
267 .expect("Inside workspace dir should exist")
268 .path()
269 )
270 .expect("The Cargo.toml should exist at this point"),
271 test_builder.workspace_cargo_toml.expect("Cargo.toml should exist")
272 );
273 assert!(find_workspace_toml(
274 test_builder
275 .outside_workspace_dir
276 .as_ref()
277 .expect("Outside workspace dir should exist")
278 .path()
279 )
280 .is_none());
281 assert!(find_workspace_toml(&PathBuf::from("..")).is_none());
283 }
284
285 #[test]
286 fn add_crate_to_workspace_works_well_if_members_exists() {
287 let test_builder = TestBuilder::default()
288 .add_workspace()
289 .add_workspace_cargo_toml(
290 r#"[workspace]
291 resolver = "2"
292 members = ["member1"]
293 "#,
294 )
295 .add_inside_workspace_dir();
296 let add_crate = add_crate_to_workspace(
297 test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"),
298 test_builder
299 .inside_workspace_dir
300 .as_ref()
301 .expect("Inside workspace dir should exist")
302 .path(),
303 );
304 assert!(add_crate.is_ok());
305 let content = read_to_string(
306 test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"),
307 )
308 .expect("Cargo.toml should be readable");
309 let doc = content.parse::<DocumentMut>().expect("This should work");
310 if let Some(Item::Table(workspace_table)) = doc.get("workspace") {
311 if let Some(Item::Value(Value::Array(array))) = workspace_table.get("members") {
312 assert!(array.iter().any(|item| {
313 if let Value::String(item) = item {
314 test_builder
318 .inside_workspace_dir
319 .as_ref()
320 .expect("Inside workspace should exist")
321 .path()
322 .to_str()
323 .expect("Dir should be mapped to a str")
324 .contains(item.value())
325 } else {
326 false
327 }
328 }));
329 } else {
330 panic!("This shouldn't be reached");
331 }
332 } else {
333 panic!("This shouldn't be reached");
334 }
335
336 let add_crate = add_crate_to_workspace(
338 test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"),
339 test_builder
340 .inside_workspace_dir
341 .as_ref()
342 .expect("Inside workspace dir should exist")
343 .path(),
344 );
345 assert!(add_crate.is_ok());
346 let doc = content.parse::<DocumentMut>().expect("This should work");
347 if let Some(Item::Table(workspace_table)) = doc.get("workspace") {
348 if let Some(Item::Value(Value::Array(array))) = workspace_table.get("members") {
349 assert_eq!(
350 array
351 .iter()
352 .filter(|item| {
353 if let Value::String(item) = item {
354 test_builder
355 .inside_workspace_dir
356 .as_ref()
357 .expect("Inside workspace should exist")
358 .path()
359 .to_str()
360 .expect("Dir should be mapped to a str")
361 .contains(item.value())
362 } else {
363 false
364 }
365 })
366 .count(),
367 1
368 );
369 } else {
370 panic!("This shouldn't be reached");
371 }
372 } else {
373 panic!("This shouldn't be reached");
374 }
375 }
376
377 #[test]
378 fn add_crate_to_workspace_works_well_if_members_doesnt_exist() {
379 let test_builder = TestBuilder::default()
380 .add_workspace()
381 .add_workspace_cargo_toml(
382 r#"[workspace]
383 resolver = "2"
384 "#,
385 )
386 .add_inside_workspace_dir();
387 let add_crate = add_crate_to_workspace(
388 test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"),
389 test_builder
390 .inside_workspace_dir
391 .as_ref()
392 .expect("Inside workspace dir should exist")
393 .path(),
394 );
395 assert!(add_crate.is_ok());
396 let content = read_to_string(
397 test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"),
398 )
399 .expect("Cargo.toml should be readable");
400 let doc = content.parse::<DocumentMut>().expect("This should work");
401 if let Some(Item::Table(workspace_table)) = doc.get("workspace") {
402 if let Some(Item::Value(Value::Array(array))) = workspace_table.get("members") {
403 assert!(array.iter().any(|item| {
404 if let Value::String(item) = item {
405 test_builder
406 .inside_workspace_dir
407 .as_ref()
408 .expect("Inside workspace should exist")
409 .path()
410 .to_str()
411 .expect("Dir should be mapped to a str")
412 .contains(item.value())
413 } else {
414 false
415 }
416 }));
417 } else {
418 panic!("This shouldn't be reached");
419 }
420 } else {
421 panic!("This shouldn't be reached");
422 }
423 }
424
425 #[test]
426 fn add_crate_to_workspace_fails_if_crate_path_not_inside_workspace() {
427 let test_builder = TestBuilder::default()
428 .add_workspace()
429 .add_workspace_cargo_toml(
430 r#"[workspace]
431 resolver = "2"
432 members = ["member1"]
433 "#,
434 )
435 .add_outside_workspace_dir();
436 let add_crate = add_crate_to_workspace(
437 test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"),
438 test_builder
439 .outside_workspace_dir
440 .expect("Inside workspace dir should exist")
441 .path(),
442 );
443 assert!(add_crate.is_err());
444 }
445
446 #[test]
447 fn add_crate_to_workspace_fails_if_members_not_an_array() {
448 let test_builder = TestBuilder::default()
449 .add_workspace()
450 .add_workspace_cargo_toml(
451 r#"[workspace]
452 resolver = "2"
453 members = "member1"
454 "#,
455 )
456 .add_inside_workspace_dir();
457 let add_crate = add_crate_to_workspace(
458 test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"),
459 test_builder
460 .inside_workspace_dir
461 .expect("Inside workspace dir should exist")
462 .path(),
463 );
464 assert!(add_crate.is_err());
465 }
466
467 #[test]
468 fn add_crate_to_workspace_fails_if_workspace_isnt_workspace() {
469 let test_builder = TestBuilder::default()
470 .add_workspace()
471 .add_workspace_cargo_toml(r#""#)
472 .add_inside_workspace_dir();
473 let add_crate = add_crate_to_workspace(
474 test_builder.workspace_cargo_toml.as_ref().expect("Workspace should exist"),
475 test_builder
476 .inside_workspace_dir
477 .expect("Inside workspace dir should exist")
478 .path(),
479 );
480 assert!(add_crate.is_err());
481 }
482
483 #[test]
484 fn add_production_profile_works() {
485 let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
486 r#"[profile.release]
487 opt-level = 3
488 "#,
489 );
490
491 let binding = test_builder.workspace.expect("Workspace should exist");
492 let project_path = binding.path();
493 let cargo_toml_path = project_path.join("Cargo.toml");
494
495 let result = add_production_profile(project_path);
497 assert!(result.is_ok());
498
499 let manifest =
501 Manifest::from_path(&cargo_toml_path).expect("Should parse updated Cargo.toml");
502 let production_profile = manifest
503 .profile
504 .custom
505 .get("production")
506 .expect("Production profile should exist");
507 assert_eq!(production_profile.codegen_units, Some(1));
508 assert_eq!(production_profile.inherits.as_deref(), Some("release"));
509 assert_eq!(production_profile.lto, Some(LtoSetting::Fat));
510
511 let initial_toml_content =
513 read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
514 let second_result = add_production_profile(project_path);
515 assert!(second_result.is_ok());
516 let final_toml_content =
517 read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
518 assert_eq!(initial_toml_content, final_toml_content);
519 }
520
521 #[test]
522 fn add_feature_works() {
523 let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml(
524 r#"[profile.release]
525 opt-level = 3
526 "#,
527 );
528
529 let expected_feature_key = "runtime-benchmarks";
530 let expected_feature_items =
531 vec!["feature-a".to_string(), "feature-b".to_string(), "feature-c".to_string()];
532 let binding = test_builder.workspace.expect("Workspace should exist");
533 let project_path = binding.path();
534 let cargo_toml_path = project_path.join("Cargo.toml");
535
536 let result = add_feature(
538 project_path,
539 (expected_feature_key.to_string(), expected_feature_items.clone()),
540 );
541 assert!(result.is_ok());
542
543 let manifest =
545 Manifest::from_path(&cargo_toml_path).expect("Should parse updated Cargo.toml");
546 let feature_items = manifest
547 .features
548 .get(expected_feature_key)
549 .expect("Production profile should exist");
550 assert_eq!(feature_items, &expected_feature_items);
551
552 let initial_toml_content =
554 read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
555 let second_result = add_feature(
556 project_path,
557 (expected_feature_key.to_string(), expected_feature_items.clone()),
558 );
559 assert!(second_result.is_ok());
560 let final_toml_content =
561 read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable");
562 assert_eq!(initial_toml_content, final_toml_content);
563 }
564}