Skip to main content

wowlab_tidy/languages/toml/rules/cargo/
target_cpu.rs

1#[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// EXAMPLES are `.cargo/config.toml` contents evaluated by `config_sets_target_cpu`;
12// the check itself fires on `crates/Cargo.toml` and locates that config via fs.
13#[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}