Skip to main content

cargo_wsdeps/
lib.rs

1use cargo_metadata::{Dependency, Package, camino::Utf8PathBuf};
2use cargo_toml::{Manifest, Workspace};
3use std::collections::{BTreeMap, BTreeSet, HashSet};
4use std::fs;
5
6pub mod diff;
7pub mod show;
8
9/// Describes the dependency of a workspace member crate.
10#[derive(Debug, Clone)]
11pub struct MemberDependency {
12    /// Member crate name.
13    pub name: String,
14    /// Path to the manifest file of the member crate.
15    pub manifest_path: Utf8PathBuf,
16    /// Dependency of the member crate.
17    pub dependency: Dependency,
18}
19
20/// Depenndencies to add to and remove from the workspace.
21pub type PartitionedDependencies = (
22    BTreeMap<String, Vec<MemberDependency>>,
23    BTreeSet<String>,
24    BTreeMap<String, MemberDependency>,
25);
26
27pub fn partition_dependencies(
28    workspace: &Workspace,
29    selected: &[&Package],
30    aggressive: bool,
31) -> anyhow::Result<PartitionedDependencies> {
32    let current_deps: HashSet<_> = workspace.dependencies.keys().cloned().collect();
33
34    // Members of each dep, split by how the member declares it:
35    //   - `needed_deps`: declared inline (`foo = "1"` or `foo = { version = ... }`)
36    //   - `ws_users`: declared as `foo.workspace = true`
37    // cargo_metadata flattens dependencies and does not expose the `workspace`
38    // flag, so we re-parse each member manifest with cargo_toml to detect it.
39    let mut needed_deps: BTreeMap<String, Vec<MemberDependency>> = BTreeMap::new();
40    let mut ws_users: BTreeMap<String, Vec<MemberDependency>> = BTreeMap::new();
41
42    for &member in selected {
43        let content = fs::read_to_string(member.manifest_path.as_std_path())?;
44        let member_manifest: Manifest = Manifest::from_str(&content)?;
45        let mut inherited: HashSet<String> = HashSet::new();
46        for (name, dep) in member_manifest
47            .dependencies
48            .iter()
49            .chain(member_manifest.dev_dependencies.iter())
50            .chain(member_manifest.build_dependencies.iter())
51        {
52            if matches!(dep, cargo_toml::Dependency::Inherited(_)) {
53                inherited.insert(name.clone());
54            }
55        }
56
57        for dep in member.dependencies.iter() {
58            let is_inherited = inherited.contains(&dep.name);
59            // Inline `path = ".."` references to sibling crates aren't workspace
60            // candidates — skip them. But inherited deps (`foo.workspace = true`)
61            // must always be tracked, even when the workspace entry itself is a
62            // path dependency: cargo_metadata resolves the member's reference
63            // with `path` set, and dropping it here would cause the tool to
64            // delete the still-referenced workspace entry.
65            if !is_inherited && dep.path.is_some() {
66                continue;
67            }
68            let md = MemberDependency {
69                name: member.name.to_string(),
70                manifest_path: member.manifest_path.clone(),
71                dependency: dep.clone(),
72            };
73            if is_inherited {
74                ws_users.entry(dep.name.clone()).or_default().push(md);
75            } else {
76                needed_deps.entry(dep.name.clone()).or_default().push(md);
77            }
78        }
79    }
80
81    // Sort inline users for stable output, then decide which inline members
82    // need to be converted to `workspace = true`. A dep is worth sharing only
83    // when 2+ members use it in total (inline + already-inherited), and there
84    // must be at least one inline holdout to convert.
85    needed_deps.retain(|name, members| {
86        members.sort_by(|a, b| a.name.cmp(&b.name));
87        let inherited_count = ws_users.get(name).map(|v| v.len()).unwrap_or(0);
88        !members.is_empty() && (members.len() + inherited_count) > 1
89    });
90
91    // A workspace dep is kept if at least one member still references it
92    // (either inline as a holdout, or via `workspace = true`).
93    let (common, mut remove) = current_deps
94        .into_iter()
95        .partition::<BTreeSet<_>, _>(|name| {
96            needed_deps.contains_key(name) || ws_users.contains_key(name)
97        });
98
99    // --aggressive: move a workspace dep back into the sole member that uses
100    // it, but only when exactly one member inherits it and no other member
101    // references it inline (otherwise consolidation would re-add it).
102    let mut inline: BTreeMap<String, MemberDependency> = BTreeMap::new();
103    if aggressive {
104        for name in &common {
105            let inherited_count = ws_users.get(name).map(|v| v.len()).unwrap_or(0);
106            let inline_count = needed_deps.get(name).map(|v| v.len()).unwrap_or(0);
107            if inherited_count == 1
108                && inline_count == 0
109                && let Some(mut users) = ws_users.remove(name)
110                && let Some(md) = users.pop()
111            {
112                inline.insert(name.clone(), md);
113                remove.insert(name.clone());
114            }
115        }
116    }
117
118    Ok((needed_deps, remove, inline))
119}