xtask/cmd/
workspace_deps.rs1use std::collections::{HashMap, HashSet};
2
3use anyhow::Context;
4use cargo_metadata::DependencyKind;
5use cargo_metadata::camino::{Utf8Path, Utf8PathBuf};
6
7use crate::cmd::IGNORED_PACKAGES;
8
9#[derive(Debug, Clone, clap::Parser)]
10pub struct WorkspaceDeps {
11 #[clap(long, short, value_delimiter = ',')]
12 #[clap(alias = "package")]
13 packages: Vec<String>,
15 #[clap(long, short, value_delimiter = ',')]
16 #[clap(alias = "exclude-package")]
17 exclude_packages: Vec<String>,
19}
20
21fn relative_path(start: &Utf8Path, end: &Utf8Path) -> Utf8PathBuf {
23 let start_components: Vec<&str> = start.components().map(|c| c.as_str()).collect();
25 let end_components: Vec<&str> = end.components().map(|c| c.as_str()).collect();
26
27 let mut i = 0;
29 while i < start_components.len() && i < end_components.len() && start_components[i] == end_components[i] {
30 i += 1;
31 }
32
33 let mut result = Utf8PathBuf::new();
35
36 for _ in i..start_components.len() {
38 result.push("..");
39 }
40
41 for comp in &end_components[i..] {
43 result.push(comp);
44 }
45
46 if result.as_str().is_empty() {
48 result.push(".");
49 }
50
51 result
52}
53
54impl WorkspaceDeps {
55 pub fn run(self) -> anyhow::Result<()> {
56 let start = std::time::Instant::now();
57
58 let metadata = crate::utils::metadata()?;
59
60 let workspace_package_ids = metadata.workspace_members.iter().cloned().collect::<HashSet<_>>();
61
62 let workspace_packages = metadata
63 .packages
64 .iter()
65 .filter(|p| workspace_package_ids.contains(&p.id))
66 .map(|p| (&p.id, p))
67 .collect::<HashMap<_, _>>();
68
69 let path_to_package = workspace_packages
70 .values()
71 .map(|p| (p.manifest_path.parent().unwrap(), &p.id))
72 .collect::<HashMap<_, _>>();
73
74 for package in metadata.packages.iter().filter(|p| workspace_package_ids.contains(&p.id)) {
75 if (IGNORED_PACKAGES.contains(&package.name.as_str()) || self.exclude_packages.contains(&package.name))
76 && (self.packages.is_empty() || !self.packages.contains(&package.name))
77 {
78 continue;
79 }
80
81 let toml = std::fs::read_to_string(&package.manifest_path)
82 .with_context(|| format!("failed to read manifest for {}", package.name))?;
83 let mut doc = toml
84 .parse::<toml_edit::DocumentMut>()
85 .with_context(|| format!("failed to parse manifest for {}", package.name))?;
86 let mut changes = false;
87
88 for dependency in package.dependencies.iter() {
89 if dependency.kind != DependencyKind::Development {
90 continue;
91 }
92
93 let Some(path) = dependency.path.as_deref() else {
94 continue;
95 };
96
97 if path_to_package.get(path).and_then(|id| workspace_packages.get(id)).is_none() {
98 continue;
99 }
100
101 let dep = doc["dev-dependencies"][&dependency.name]
102 .as_table_like_mut()
103 .expect("expected table");
104
105 dep.insert(
106 "path",
107 toml_edit::value(relative_path(package.manifest_path.parent().unwrap(), path).to_string()),
108 );
109 if let Some(rename) = dependency.rename.clone() {
110 dep.insert("rename", toml_edit::value(rename));
111 }
112
113 if !dependency.features.is_empty() {
114 let mut array = toml_edit::Array::new();
115 for feature in dependency.features.iter().cloned() {
116 array.push(feature);
117 }
118 dep.insert("features", toml_edit::value(array));
119 }
120 if dependency.optional {
121 dep.insert("optional", toml_edit::value(true));
122 }
123
124 dep.remove("workspace");
125
126 println!("Replaced path in {} for '{}' to '{}'", package.name, dependency.name, path);
127 changes = true;
128 }
129
130 if changes {
131 std::fs::write(&package.manifest_path, doc.to_string())
132 .with_context(|| format!("failed to write manifest for {}", package.name))?;
133 println!("Replaced paths in {} for {}", package.name, package.manifest_path);
134 }
135 }
136
137 println!("Done in {:?}", start.elapsed());
138
139 Ok(())
140 }
141}