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 let Some(path) = dependency.path.as_deref() else {
90 continue;
91 };
92
93 if path_to_package.get(path).and_then(|id| workspace_packages.get(id)).is_none() {
94 continue;
95 }
96
97 let table = match dependency.kind {
98 DependencyKind::Normal => "dependencies",
99 DependencyKind::Build => "build-dependencies",
100 DependencyKind::Development => "dev-dependencies",
101 _ => continue,
102 };
103
104 let dep = doc[table][&dependency.name].as_table_like_mut().expect("expected table");
105
106 dep.insert(
107 "path",
108 toml_edit::value(relative_path(package.manifest_path.parent().unwrap(), path).to_string()),
109 );
110 if let Some(rename) = dependency.rename.clone() {
111 dep.insert("rename", toml_edit::value(rename));
112 }
113
114 if !dependency.features.is_empty() {
115 let mut array = toml_edit::Array::new();
116 for feature in dependency.features.iter().cloned() {
117 array.push(feature);
118 }
119 dep.insert("features", toml_edit::value(array));
120 }
121 if dependency.optional {
122 dep.insert("optional", toml_edit::value(true));
123 }
124
125 dep.remove("workspace");
126
127 println!("Replaced path in {} for '{}' to '{}'", package.name, dependency.name, path);
128 changes = true;
129 }
130
131 if changes {
132 std::fs::write(&package.manifest_path, doc.to_string())
133 .with_context(|| format!("failed to write manifest for {}", package.name))?;
134 println!("Replaced paths in {} for {}", package.name, package.manifest_path);
135 }
136 }
137
138 println!("Done in {:?}", start.elapsed());
139
140 Ok(())
141 }
142}