xtask/cmd/
workspace_deps.rs

1use 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 to test
14    packages: Vec<String>,
15    #[clap(long, short, value_delimiter = ',')]
16    #[clap(alias = "exclude-package")]
17    /// Packages to exclude from testing
18    exclude_packages: Vec<String>,
19}
20
21// the path that would need to be added to start to get to end
22fn relative_path(start: &Utf8Path, end: &Utf8Path) -> Utf8PathBuf {
23    // Break down the paths into components
24    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    // Find the common prefix length
28    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    // Start building the relative path
34    let mut result = Utf8PathBuf::new();
35
36    // For each remaining component in `start`, add ".."
37    for _ in i..start_components.len() {
38        result.push("..");
39    }
40
41    // Append the remaining components from `end`
42    for comp in &end_components[i..] {
43        result.push(comp);
44    }
45
46    // If the resulting path is empty, use "." to represent the current directory
47    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}