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                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}