wowlab_tidy/languages/toml/rules/cargo/
target_cpu.rs1#[cfg(test)]
2use googletest::prelude::*;
3use wowlab_fs::{
4 directory::{self, EntryKind},
5 file,
6 path::{Path, PathBuf},
7};
8
9use crate::{Example, TomlCtx, Violation, violation};
10
11#[rustfmt::skip]
14const EXAMPLES: &[Example] = &[
15 Example { label: "split-form rustflags", code: "[target.x86_64-unknown-linux-gnu]\nrustflags = [\"-C\", \"target-cpu=x86-64-v3\"]\n", pass: true },
16 Example { label: "string rustflags", code: "[build]\nrustflags = \"-C target-cpu=native\"\n", pass: true },
17 Example { label: "aliases only", code: "[alias]\nforge = \"run\"\n", pass: false },
18 Example { label: "rustflags without target-cpu", code: "[build]\nrustflags = [\"-C\", \"opt-level=3\"]\n", pass: false },
19];
20
21crate::toml_rule!(
22 toml_cargo_target_cpu,
23 "Require a -C target-cpu rustflags entry in .cargo/config.toml for workspaces with native binaries.",
24 "Server binaries left on the generic CPU baseline forfeit fleet performance the deployment environment already guarantees (M-TARGET-CPU).",
25 Low,
26 params {
27 expected: [String] = ["target-cpu="],
28 },
29);
30
31fn check_toml_cargo_target_cpu(ctx: &TomlCtx<'_>) -> Vec<Violation> {
32 if ctx.file.rel != "crates/Cargo.toml" || !ctx.parse.errors.is_empty() {
33 return Vec::new();
34 }
35
36 let Ok(document) = toml::from_str::<toml::Table>(ctx.file.contents) else {
37 return Vec::new();
38 };
39
40 if !has_native_binary_member(&document, ctx.file.path) {
41 return Vec::new();
42 }
43
44 let expected = ctx
45 .file
46 .config
47 .get_str_array("toml_cargo_target_cpu", &PARAMS[0]);
48
49 let Some(config_path) = find_cargo_config(ctx.file.path) else {
50 return vec![violation(
51 ctx.file.rel,
52 1,
53 "workspace has native binaries but no .cargo/config.toml sets -C target-cpu (M-TARGET-CPU)",
54 )];
55 };
56 let sets_target_cpu = file::read_text(&config_path)
57 .ok()
58 .and_then(|source| toml::from_str::<toml::Table>(&source).ok())
59 .is_some_and(|config| config_sets_target_cpu(&config, &expected));
60
61 if sets_target_cpu {
62 Vec::new()
63 } else {
64 vec![violation(
65 ctx.file.rel,
66 1,
67 format!(
68 "workspace has native binaries but {} has no rustflags entry with -C target-cpu (M-TARGET-CPU)",
69 config_path.display()
70 ),
71 )]
72 }
73}
74
75fn has_native_binary_member(document: &toml::Table, manifest_path: &Path) -> bool {
76 let Some(crates_dir) = manifest_path.parent() else {
77 return false;
78 };
79 let Some(members) = document
80 .get("workspace")
81 .and_then(toml::Value::as_table)
82 .and_then(|workspace| workspace.get("members"))
83 .and_then(toml::Value::as_array)
84 else {
85 return false;
86 };
87 let patterns: Vec<&str> = members.iter().filter_map(toml::Value::as_str).collect();
88
89 crate::infra::workspace::member_manifests(crates_dir)
90 .iter()
91 .any(|manifest| {
92 let Some(member_dir) = manifest.parent() else {
93 return false;
94 };
95
96 member_dir.parent() == Some(crates_dir)
97 && member_dir
98 .file_name()
99 .and_then(|name| name.to_str())
100 .is_some_and(|name| {
101 patterns
102 .iter()
103 .any(|pattern| glob_match::glob_match(pattern, name))
104 })
105 && is_binary_member(member_dir, manifest)
106 })
107}
108
109fn is_binary_member(member_dir: &Path, manifest: &Path) -> bool {
110 if directory::inspect(&member_dir.join("src/main.rs"))
111 .ok()
112 .flatten()
113 .is_some_and(|entry| entry.kind() == EntryKind::File)
114 {
115 return true;
116 }
117
118 file::read_text(manifest)
119 .ok()
120 .and_then(|source| toml::from_str::<toml::Table>(&source).ok())
121 .and_then(|document| {
122 document
123 .get("bin")
124 .and_then(toml::Value::as_array)
125 .map(|targets| !targets.is_empty())
126 })
127 .unwrap_or(false)
128}
129
130fn find_cargo_config(manifest_path: &Path) -> Option<PathBuf> {
131 manifest_path
132 .ancestors()
133 .map(|ancestor| ancestor.join(".cargo/config.toml"))
134 .find(|candidate| {
135 directory::inspect(candidate)
136 .ok()
137 .flatten()
138 .is_some_and(|entry| entry.kind() == EntryKind::File)
139 })
140}
141
142fn config_sets_target_cpu(config: &toml::Table, expected: &[String]) -> bool {
143 let mut stack: Vec<&toml::Table> = vec![config];
144
145 while let Some(table) = stack.pop() {
146 for (key, value) in table {
147 if key == "rustflags" && rustflags_contain(value, expected) {
148 return true;
149 }
150
151 if let Some(child) = value.as_table() {
152 stack.push(child);
153 }
154 }
155 }
156
157 false
158}
159
160fn rustflags_contain(value: &toml::Value, expected: &[String]) -> bool {
161 match value {
162 toml::Value::String(flags) => expected.iter().any(|needle| flags.contains(needle)),
163 toml::Value::Array(items) => items
164 .iter()
165 .filter_map(toml::Value::as_str)
166 .any(|flag| expected.iter().any(|needle| flag.contains(needle))),
167 _ => false,
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[gtest]
176 fn examples() -> Result<()> {
177 let expected = vec!["target-cpu=".to_string()];
178
179 for ex in EXAMPLES {
180 let config: toml::Table = toml::from_str(ex.code).or_fail()?;
181
182 verify_eq!(config_sets_target_cpu(&config, &expected), ex.pass)?;
183 }
184
185 Ok(())
186 }
187
188 #[gtest]
189 fn gate_skips_member_manifests() -> Result<()> {
190 let violations = crate::test_support::check_source_toml_at(
191 "crates/foo/Cargo.toml",
192 "[package]\nname = \"foo\"\n",
193 check_toml_cargo_target_cpu,
194 );
195
196 verify_true!(violations.is_empty())?;
197
198 Ok(())
199 }
200
201 #[gtest]
202 fn workspace_without_binary_members_passes() -> Result<()> {
203 let violations = crate::test_support::check_source_toml_at(
204 "crates/Cargo.toml",
205 "[workspace]\nmembers = []\n",
206 check_toml_cargo_target_cpu,
207 );
208
209 verify_true!(violations.is_empty())?;
210
211 Ok(())
212 }
213}