cli_xtask/
workspace.rs

1//! Utility functions for working with workspaces.
2
3use std::{
4    cmp::Ordering,
5    collections::{hash_map::Entry, HashMap, HashSet},
6};
7
8use cargo_metadata::{
9    camino::{Utf8Path, Utf8PathBuf},
10    Metadata, MetadataCommand,
11};
12use once_cell::sync::Lazy;
13use walkdir::WalkDir;
14
15use crate::{fs::ToRelative, Result};
16
17mod metadata;
18mod package;
19
20pub use self::{metadata::*, package::*};
21
22static WORKSPACES: Lazy<Vec<Metadata>> = Lazy::new(|| {
23    let current_dir = std::env::current_dir().unwrap();
24    let current_dir = Utf8PathBuf::try_from(current_dir).unwrap();
25    collect_workspaces(&current_dir).unwrap()
26});
27
28/// Returns a current cargo workspace metadata.
29pub fn current() -> &'static Metadata {
30    &WORKSPACES[0]
31}
32
33/// Returns metadata for all cargo workspaces under the current workspace.
34pub fn all() -> &'static [Metadata] {
35    &WORKSPACES
36}
37
38fn collect_workspaces(base_dir: &Utf8Path) -> Result<Vec<Metadata>> {
39    let mut workspaces = HashMap::new();
40    let mut target_dirs = HashSet::new();
41
42    let current_workspace = MetadataCommand::new().current_dir(base_dir).exec()?;
43    let current_workspace_root = &current_workspace.workspace_root;
44
45    let mut it = WalkDir::new(current_workspace_root)
46        .sort_by(
47            // Sort files before directories.
48            // This is to make sure that `target_dirs` is updated before files in it are iterated.
49            |a, b| match (a.file_type().is_file(), b.file_type().is_file()) {
50                (true, true) => a.file_name().cmp(b.file_name()),
51                (true, false) => Ordering::Less,
52                (false, true) => Ordering::Greater,
53                (false, false) => a.file_name().cmp(b.file_name()),
54            },
55        )
56        .into_iter();
57
58    while let Some(entry) = it.next() {
59        let entry = entry?;
60        let path = <&Utf8Path>::try_from(entry.path())?;
61
62        // Check if the path is a cargo manifest file.
63        if entry.file_type().is_file() && path.file_name() == Some("Cargo.toml") {
64            tracing::debug!("Found manifest {}", path.to_relative());
65            let workspace = MetadataCommand::new().manifest_path(path).exec()?;
66            match workspaces.entry(workspace.workspace_root.clone()) {
67                Entry::Occupied(_e) => {}
68                Entry::Vacant(e) => {
69                    if workspace.target_directory.is_dir() {
70                        let target_dir = workspace.target_directory.canonicalize_utf8()?;
71                        tracing::debug!(
72                            "Found workspace {}",
73                            workspace.workspace_root.to_relative()
74                        );
75                        target_dirs.insert(target_dir);
76                    }
77                    e.insert(workspace);
78                }
79            }
80        }
81
82        // Skip the .git directory.
83        if entry.file_type().is_dir() && path.file_name() == Some(".git") {
84            tracing::debug!("Skipping git directory {}", path.to_relative());
85            it.skip_current_dir();
86            continue;
87        }
88
89        // Skip the current workspace's target directories.
90        // This prevents the `target/package` directory from being included in the
91        // workspace.
92        if entry.file_type().is_dir() && target_dirs.contains(&path.canonicalize_utf8()?) {
93            tracing::debug!("Skipping target directory {}", path.to_relative());
94            it.skip_current_dir();
95            continue;
96        }
97    }
98
99    // Sort workspaces by their root directory.
100    // The shallowest workspace should come first.
101    let mut workspaces = workspaces.into_values().collect::<Vec<_>>();
102    workspaces.sort_by(|a, b| a.workspace_root.cmp(&b.workspace_root));
103
104    Ok(workspaces)
105}