Skip to content

Commit 236ec01

Browse files
committed
Check C++ files when running ./x fmt
1 parent bd67379 commit 236ec01

File tree

4 files changed

+317
-0
lines changed

4 files changed

+317
-0
lines changed

clang-format.toml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Files to ignore. Each entry uses gitignore syntax, but `!` prefixes aren't allowed.
2+
ignore = [
3+
"/build/",
4+
"/*-build/",
5+
"/build-*/",
6+
"/vendor/",
7+
"/tests/",
8+
"library",
9+
"src",
10+
]

src/bootstrap/Cargo.lock

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ dependencies = [
5353
"clap",
5454
"clap_complete",
5555
"cmake",
56+
"diff",
5657
"fd-lock",
5758
"filetime",
5859
"home",

src/bootstrap/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ xz2 = "0.1"
6666

6767
# Dependencies needed by the build-metrics feature
6868
sysinfo = { version = "0.30", default-features = false, optional = true }
69+
diff = "0.1.13"
6970

7071
[target.'cfg(windows)'.dependencies.junction]
7172
version = "1.0.0"

src/bootstrap/src/core/build_steps/format.rs

+305
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use build_helper::ci::CiEnv;
66
use build_helper::git::get_git_modified_files;
77
use ignore::WalkBuilder;
88
use std::collections::VecDeque;
9+
use std::io::Read;
910
use std::path::{Path, PathBuf};
1011
use std::process::{Command, Stdio};
1112
use std::sync::mpsc::SyncSender;
@@ -96,11 +97,185 @@ fn get_modified_rs_files(build: &Builder<'_>) -> Result<Option<Vec<String>>, Str
9697
get_git_modified_files(&build.config.git_config(), Some(&build.config.src), &vec!["rs"])
9798
}
9899

100+
fn get_modified_c_family_files(build: &Builder<'_>) -> Result<Option<Vec<String>>, String> {
101+
get_git_modified_files(
102+
&build.config.git_config(),
103+
Some(&build.config.src),
104+
&vec!["cpp", "c", "hpp", "h"],
105+
)
106+
}
107+
99108
#[derive(serde_derive::Deserialize)]
100109
struct RustfmtConfig {
101110
ignore: Vec<String>,
102111
}
103112

113+
#[derive(Debug, PartialEq)]
114+
pub enum DiffLine {
115+
Context(String),
116+
Expected(String),
117+
Resulting(String),
118+
}
119+
120+
#[derive(Debug, PartialEq)]
121+
pub struct Mismatch {
122+
pub line_number: u32,
123+
pub lines: Vec<DiffLine>,
124+
}
125+
126+
impl Mismatch {
127+
fn new(line_number: u32) -> Mismatch {
128+
Mismatch { line_number, lines: Vec::new() }
129+
}
130+
}
131+
132+
// Produces a diff between the expected output and actual output.
133+
pub fn make_diff(expected: &str, actual: &str, context_size: usize) -> Vec<Mismatch> {
134+
// FIXME: Can we stop copying this code?
135+
let mut line_number = 1;
136+
let mut context_queue: VecDeque<&str> = VecDeque::with_capacity(context_size);
137+
let mut lines_since_mismatch = context_size + 1;
138+
let mut results = Vec::new();
139+
let mut mismatch = Mismatch::new(0);
140+
141+
for result in diff::lines(expected, actual) {
142+
match result {
143+
diff::Result::Left(str) => {
144+
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
145+
results.push(mismatch);
146+
mismatch = Mismatch::new(line_number - context_queue.len() as u32);
147+
}
148+
149+
while let Some(line) = context_queue.pop_front() {
150+
mismatch.lines.push(DiffLine::Context(line.to_owned()));
151+
}
152+
153+
mismatch.lines.push(DiffLine::Expected(str.to_owned()));
154+
line_number += 1;
155+
lines_since_mismatch = 0;
156+
}
157+
diff::Result::Right(str) => {
158+
if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
159+
results.push(mismatch);
160+
mismatch = Mismatch::new(line_number - context_queue.len() as u32);
161+
}
162+
163+
while let Some(line) = context_queue.pop_front() {
164+
mismatch.lines.push(DiffLine::Context(line.to_owned()));
165+
}
166+
167+
mismatch.lines.push(DiffLine::Resulting(str.to_owned()));
168+
lines_since_mismatch = 0;
169+
}
170+
diff::Result::Both(str, _) => {
171+
if context_queue.len() >= context_size {
172+
let _ = context_queue.pop_front();
173+
}
174+
175+
if lines_since_mismatch < context_size {
176+
mismatch.lines.push(DiffLine::Context(str.to_owned()));
177+
} else if context_size > 0 {
178+
context_queue.push_back(str);
179+
}
180+
181+
line_number += 1;
182+
lines_since_mismatch += 1;
183+
}
184+
}
185+
}
186+
187+
results.push(mismatch);
188+
results.remove(0);
189+
190+
results
191+
}
192+
193+
#[derive(serde_derive::Deserialize)]
194+
struct ClangFormatConfig {
195+
ignore: Vec<String>,
196+
}
197+
198+
fn get_clang_format() -> Option<PathBuf> {
199+
std::env::var_os("PATH").and_then(|paths| {
200+
std::env::split_paths(&paths)
201+
.filter_map(|dir| {
202+
let full_path = dir.join("clang-format");
203+
if full_path.is_file() { Some(full_path) } else { None }
204+
})
205+
.next()
206+
})
207+
}
208+
209+
fn clang_format(
210+
src: &Path,
211+
clang_format: &Path,
212+
path: &Path,
213+
check: bool,
214+
) -> impl FnMut(bool) -> bool {
215+
let mut cmd = Command::new(clang_format);
216+
let clang_form_config = src.join(".clang-format");
217+
// avoid the submodule config paths from coming into play,
218+
// we only allow a single global config for the workspace for now
219+
cmd.arg("--style").arg(format!("file:{}", clang_form_config.display()));
220+
if !check {
221+
cmd.arg("-i");
222+
}
223+
cmd.arg(path);
224+
let cmd_debug = format!("{cmd:?}");
225+
let mut cmd = cmd.stdout(Stdio::piped()).spawn().expect("running clang-format");
226+
let path = path.to_path_buf();
227+
// poor man's async: return a closure that'll wait for clang-format's completion
228+
move |block: bool| -> bool {
229+
// let mut cmd = cmd; // Move `cmd` into the closure
230+
if !block {
231+
match cmd.try_wait() {
232+
Ok(Some(_)) => {}
233+
_ => return false,
234+
}
235+
}
236+
let mut expected = String::new();
237+
if check {
238+
if let Some(stdout) = &mut cmd.stdout {
239+
stdout.read_to_string(&mut expected).unwrap();
240+
}
241+
}
242+
let status = cmd.wait().unwrap();
243+
if !status.success() {
244+
eprintln!(
245+
"Running `{}` failed.\nIf you're running `tidy`, \
246+
try again with `--bless`. Or, if you just want to format \
247+
code, run `./x.py fmt` instead.",
248+
cmd_debug,
249+
);
250+
crate::exit!(1);
251+
}
252+
if check {
253+
let actual = std::fs::read_to_string(&path).expect("Failed");
254+
if expected != actual {
255+
for result in make_diff(&expected, &actual, 3) {
256+
println!("Diff in {} at line {}:", path.display(), result.line_number);
257+
for line in result.lines {
258+
match line {
259+
DiffLine::Expected(str) => println!("-{}", str),
260+
DiffLine::Context(str) => println!(" {}", str),
261+
DiffLine::Resulting(str) => println!("+{}", str),
262+
}
263+
}
264+
}
265+
let cmd_debug_diff = format!("{} | diff -u {} -", cmd_debug, path.display());
266+
eprintln!(
267+
"Running `{}` failed.\nIf you're running `tidy`, \
268+
try again with `--bless`. Or, if you just want to format \
269+
code, run `./x.py fmt` instead.",
270+
cmd_debug_diff,
271+
);
272+
crate::exit!(1);
273+
}
274+
}
275+
true
276+
}
277+
}
278+
104279
// Prints output describing a collection of paths, with lines such as "formatted modified file
105280
// foo/bar/baz" or "skipped 20 untracked files".
106281
fn print_paths(verb: &str, adjective: Option<&str>, paths: &[String]) {
@@ -136,6 +311,23 @@ pub fn format(build: &Builder<'_>, check: bool, all: bool, paths: &[PathBuf]) {
136311
let mut builder = ignore::types::TypesBuilder::new();
137312
builder.add_defaults();
138313
builder.select("rust");
314+
315+
let mut c_family_builder = ignore::types::TypesBuilder::new();
316+
c_family_builder.add_defaults();
317+
c_family_builder.select("c");
318+
c_family_builder.select("cpp");
319+
320+
// TODO: Allow to configure the path of clang-format?
321+
let clang_format_path = get_clang_format();
322+
let clang_format_available = clang_format_path.is_some();
323+
// TODO: Check the clang-format version.
324+
if !clang_format_available && CiEnv::is_ci() {
325+
eprintln!("fmt error: Can't find `clang-format`.");
326+
crate::exit!(1);
327+
}
328+
let mut c_family_override_builder = ignore::overrides::OverrideBuilder::new(&build.src);
329+
let mut run_clang_format = clang_format_available;
330+
139331
let matcher = builder.build().unwrap();
140332
let rustfmt_config = build.src.join("rustfmt.toml");
141333
if !rustfmt_config.exists() {
@@ -158,8 +350,29 @@ pub fn format(build: &Builder<'_>, check: bool, all: bool, paths: &[PathBuf]) {
158350
crate::exit!(1);
159351
} else {
160352
override_builder.add(&format!("!{ignore}")).expect(&ignore);
353+
c_family_override_builder.add(&format!("!{ignore}")).expect(&ignore);
161354
}
162355
}
356+
357+
let clang_format_config = build.src.join("clang-format.toml");
358+
if !clang_format_config.exists() {
359+
eprintln!("fmt error: Not running formatting checks; clang-format.toml does not exist.");
360+
eprintln!("fmt error: This may happen in distributed tarballs.");
361+
return;
362+
}
363+
let clang_format_config = t!(std::fs::read_to_string(&clang_format_config));
364+
let clang_format_config: ClangFormatConfig = t!(toml::from_str(&clang_format_config));
365+
for ignore in clang_format_config.ignore {
366+
if ignore.starts_with('!') {
367+
eprintln!(
368+
"fmt error: `!`-prefixed entries are not supported in clang-format.toml, sorry"
369+
);
370+
crate::exit!(1);
371+
} else {
372+
c_family_override_builder.add(&format!("!{ignore}")).expect(&ignore);
373+
}
374+
}
375+
163376
let git_available = match Command::new("git")
164377
.arg("--version")
165378
.stdout(Stdio::null())
@@ -210,6 +423,9 @@ pub fn format(build: &Builder<'_>, check: bool, all: bool, paths: &[PathBuf]) {
210423
// against anything like `compiler/rustc_foo/src/foo.rs`,
211424
// preventing the latter from being formatted.
212425
override_builder.add(&format!("!/{untracked_path}")).expect(&untracked_path);
426+
c_family_override_builder
427+
.add(&format!("!/{untracked_path}"))
428+
.expect(&untracked_path);
213429
}
214430
if !all {
215431
adjective = Some("modified");
@@ -226,6 +442,28 @@ pub fn format(build: &Builder<'_>, check: bool, all: bool, paths: &[PathBuf]) {
226442
eprintln!("fmt warning: Falling back to formatting all files.");
227443
}
228444
}
445+
match get_modified_c_family_files(build) {
446+
Ok(Some(files)) => {
447+
if !files.is_empty() && !clang_format_available {
448+
eprintln!(
449+
"fmt warning: Modified C++ files but couldn't find the available clang-format."
450+
);
451+
}
452+
run_clang_format = run_clang_format && !files.is_empty();
453+
for file in files {
454+
c_family_override_builder.add(&format!("/{file}")).expect(&file);
455+
}
456+
}
457+
Ok(None) => {
458+
eprintln!("no check");
459+
run_clang_format = false;
460+
}
461+
Err(err) => {
462+
eprintln!("fmt warning: Something went wrong running git commands:");
463+
eprintln!("fmt warning: {err}");
464+
eprintln!("fmt warning: Falling back to formatting all files.");
465+
}
466+
}
229467
}
230468
} else {
231469
eprintln!("fmt: warning: Not in git tree. Skipping git-aware format checks");
@@ -243,6 +481,7 @@ pub fn format(build: &Builder<'_>, check: bool, all: bool, paths: &[PathBuf]) {
243481
assert!(rustfmt_path.exists(), "{}", rustfmt_path.display());
244482
let src = build.src.clone();
245483
let (tx, rx): (SyncSender<PathBuf>, _) = std::sync::mpsc::sync_channel(128);
484+
let (c_family_tx, c_family_rx): (SyncSender<PathBuf>, _) = std::sync::mpsc::sync_channel(128);
246485
let walker = WalkBuilder::new(src.clone()).types(matcher).overrides(override_).build_parallel();
247486

248487
// There is a lot of blocking involved in spawning a child process and reading files to format.
@@ -305,6 +544,72 @@ pub fn format(build: &Builder<'_>, check: bool, all: bool, paths: &[PathBuf]) {
305544
ignore::WalkState::Continue
306545
})
307546
});
547+
548+
if run_clang_format {
549+
if let Some(clang_format_path) = clang_format_path {
550+
let src = build.src.clone();
551+
let c_family_walker = {
552+
let c_family_override = c_family_override_builder.build().unwrap();
553+
let c_family_matcher = c_family_builder.build().unwrap();
554+
WalkBuilder::new(src.clone())
555+
.types(c_family_matcher)
556+
.overrides(c_family_override)
557+
.build_parallel()
558+
};
559+
560+
let c_family_thread = std::thread::spawn(move || {
561+
let mut children = VecDeque::new();
562+
while let Ok(path) = c_family_rx.recv() {
563+
let child = clang_format(&src, &clang_format_path, &path, check);
564+
children.push_back(child);
565+
566+
// Poll completion before waiting.
567+
for i in (0..children.len()).rev() {
568+
if children[i](false) {
569+
children.swap_remove_back(i);
570+
break;
571+
}
572+
}
573+
574+
if children.len() >= max_processes {
575+
// Await oldest child.
576+
children.pop_front().unwrap()(true);
577+
}
578+
}
579+
580+
// Await remaining children.
581+
for mut child in children {
582+
child(true);
583+
}
584+
});
585+
586+
c_family_walker.run(|| {
587+
let tx = c_family_tx.clone();
588+
Box::new(move |entry| {
589+
let cwd = std::env::current_dir();
590+
let entry = t!(entry);
591+
if entry.file_type().map_or(false, |t| t.is_file()) {
592+
formatted_paths_ref.lock().unwrap().push({
593+
// `into_path` produces an absolute path. Try to strip `cwd` to get a shorter
594+
// relative path.
595+
let mut path = entry.clone().into_path();
596+
if let Ok(cwd) = cwd {
597+
if let Ok(path2) = path.strip_prefix(cwd) {
598+
path = path2.to_path_buf();
599+
}
600+
}
601+
path.display().to_string()
602+
});
603+
t!(tx.send(entry.into_path()));
604+
}
605+
ignore::WalkState::Continue
606+
})
607+
});
608+
drop(c_family_tx);
609+
c_family_thread.join().unwrap();
610+
}
611+
}
612+
308613
let mut paths = formatted_paths.into_inner().unwrap();
309614
paths.sort();
310615
print_paths(if check { "checked" } else { "formatted" }, adjective, &paths);

0 commit comments

Comments
 (0)