xtask/tasks/fmt/
workspace.rs1use crate::Xtask;
5use crate::shell::XtaskShell;
6use anyhow::Context;
7use clap::Parser;
8use rayon::prelude::*;
9use serde::Deserialize;
10use std::cell::Cell;
11use std::collections::HashSet;
12use std::path::PathBuf;
13use toml_edit::Item;
14use toml_edit::TableLike;
15use toml_edit::Value;
16
17#[derive(Parser)]
18#[clap(about = "Verify that all Cargo.toml files are valid and in the workspace")]
19pub struct VerifyWorkspace;
20
21static WORKSPACE_EXCEPTIONS: &[(&str, &[&str])] = &[
23 ("disk_blob", &["tokio"]),
27 ("mesh_rpc", &["tokio"]),
31];
32
33impl Xtask for VerifyWorkspace {
34 fn run(self, ctx: crate::XtaskCtx) -> anyhow::Result<()> {
35 let excluded = {
36 let contents = fs_err::read_to_string("Cargo.toml")?;
38 let parsed = contents.parse::<toml_edit::DocumentMut>()?;
39
40 if let Some(excluded) = parsed
41 .as_table()
42 .get("workspace")
43 .and_then(|w| w.get("exclude"))
44 .and_then(|e| e.as_array())
45 {
46 let mut exclude = Vec::new();
47 for entry in excluded {
48 let entry = entry.as_str().unwrap();
49 exclude.push(
50 std::path::absolute(entry)
51 .with_context(|| format!("cannot exclude {}", entry))?,
52 );
53 }
54 exclude
55 } else {
56 Vec::new()
57 }
58 };
59
60 let entries = ignore::WalkBuilder::new(ctx.root)
62 .filter_entry(move |e| {
63 for path in excluded.iter() {
64 if e.path().starts_with(path) {
65 return false;
66 }
67 }
68
69 true
70 })
71 .build()
72 .filter_map(|entry| match entry {
73 Ok(entry) if entry.file_name() == "Cargo.toml" => Some(entry.into_path()),
74 Err(err) => {
75 log::error!("error when walking over subdirectories: {}", err);
76 None
77 }
78 _ => None,
79 })
80 .collect::<Vec<_>>();
81
82 let manifests = workspace_manifests()?;
83
84 let all_present = entries.iter().all(|entry| {
85 if !manifests.contains(entry) {
86 log::error!("Error: {} is not present in the workspace", entry.display());
87 false
88 } else {
89 true
90 }
91 });
92
93 let dependencies_valid = manifests.par_iter().all(|entry| {
94 if let Err(err) = verify_dependencies(entry) {
95 log::error!("Error: failed to verify {}: {:#}", entry.display(), err);
96 false
97 } else {
98 true
99 }
100 });
101
102 if !all_present || !dependencies_valid {
103 anyhow::bail!("found invalid Cargo.toml");
104 }
105
106 Ok(())
107 }
108}
109
110#[derive(Deserialize)]
111struct CargoMetadata {
112 packages: Vec<Package>,
113 workspace_root: PathBuf,
114}
115
116#[derive(Deserialize)]
117struct Package {
118 manifest_path: PathBuf,
119}
120
121fn workspace_manifests() -> anyhow::Result<HashSet<PathBuf>> {
122 let json = XtaskShell::new()?
123 .cmd("cargo")
124 .arg("metadata")
125 .arg("--no-deps")
126 .arg("--format-version=1")
127 .read()?;
128 let metadata: CargoMetadata =
129 serde_json::from_str(&json).context("failed to parse JSON result")?;
130
131 Ok(metadata
132 .packages
133 .into_iter()
134 .map(|p| p.manifest_path)
135 .chain([metadata.workspace_root.join("Cargo.toml")])
136 .collect())
137}
138
139fn verify_dependencies(path: &PathBuf) -> Result<(), anyhow::Error> {
140 let contents = fs_err::read_to_string(path)?;
142 let parsed = contents.parse::<toml_edit::DocumentMut>()?;
143
144 let package_name = match parsed
145 .as_table()
146 .get("package")
147 .and_then(|p| p.get("name"))
148 .and_then(|n| n.as_str())
149 {
150 Some(name) => name,
151 None => return Ok(()), };
153
154 let mut dep_tables = Vec::new();
155 for (name, v) in parsed.iter() {
156 match name {
157 "dependencies" | "build-dependencies" | "dev-dependencies" => {
158 dep_tables.push(v.as_table_like().unwrap())
159 }
160 "target" => {
161 let flattened = v
162 .as_table_like()
163 .unwrap()
164 .iter()
165 .flat_map(|(_, v)| v.as_table_like().unwrap().iter());
166
167 for (k, v) in flattened {
168 match k {
169 "dependencies" | "build-dependencies" | "dev-dependencies" => {
170 dep_tables.push(v.as_table_like().unwrap())
171 }
172 _ => {}
173 }
174 }
175 }
176 _ => {}
177 }
178 }
179
180 let found_bad_deps = Cell::new(false);
181
182 let handle_non_workspaced_dep = |dep_name| {
183 let allowed = WORKSPACE_EXCEPTIONS
184 .iter()
185 .find_map(|&(p, crates)| (p == package_name).then_some(crates))
186 .unwrap_or(&[]);
187
188 if allowed.contains(&dep_name) {
189 log::debug!(
190 "{} contains non-workspaced dependency {}. Allowed by exception.",
191 package_name,
192 dep_name
193 );
194 } else {
195 found_bad_deps.set(true);
196 log::error!(
197 "{} contains non-workspaced dependency {}. Please move this dependency to the root Cargo.toml.",
198 package_name,
199 dep_name
200 );
201 }
202 };
203 let check_table_like = |t: &dyn TableLike, dep_name| {
204 if t.get("workspace").and_then(|x| x.as_bool()) != Some(true) {
205 handle_non_workspaced_dep(dep_name);
206 }
207 };
208
209 for table in dep_tables {
210 for (dep_name, value) in table.iter() {
211 match value {
212 Item::Value(Value::String(_)) => handle_non_workspaced_dep(dep_name),
213 Item::Value(Value::InlineTable(t)) => {
214 check_table_like(t, dep_name);
215
216 if t.len() == 1 {
217 found_bad_deps.set(true);
218 log::error!(
219 "{} uses inline table syntax for its dependency on {}, but only contains one table entry. Please change to the dotted syntax.",
220 package_name,
221 dep_name
222 );
223 }
224 }
225 Item::Table(t) => check_table_like(t, dep_name),
226
227 _ => unreachable!(),
228 }
229 }
230 }
231
232 if found_bad_deps.get() {
233 Err(anyhow::anyhow!("Found incorrectly defined dependencies."))
234 } else {
235 Ok(())
236 }
237}