xtask/cmd/
workspace_deps.rs

1use std::collections::{HashMap, HashSet};
2
3use anyhow::Context;
4use cargo_metadata::camino::{Utf8Path, Utf8PathBuf};
5use cargo_metadata::DependencyKind;
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 mut dep = toml_edit::Table::new();
102
103                dep["path"] = toml_edit::value(relative_path(package.manifest_path.parent().unwrap(), path).to_string());
104                if let Some(rename) = dependency.rename.clone() {
105                    dep["rename"] = toml_edit::value(rename);
106                }
107
108                if !dependency.features.is_empty() {
109                    let mut array = toml_edit::Array::new();
110                    for feature in dependency.features.iter().cloned() {
111                        array.push(feature);
112                    }
113                    dep["features"] = toml_edit::value(array);
114                }
115                if dependency.optional {
116                    dep["optional"] = toml_edit::value(true);
117                }
118
119                doc["dev-dependencies"][&dependency.name] = toml_edit::Item::Table(dep);
120                changes = true;
121            }
122
123            if changes {
124                std::fs::write(&package.manifest_path, doc.to_string())
125                    .with_context(|| format!("failed to write manifest for {}", package.name))?;
126                println!("Replaced paths in {} for {}", package.name, package.manifest_path);
127            }
128        }
129
130        println!("Done in {:?}", start.elapsed());
131
132        Ok(())
133    }
134}