Skip to content

Commit 248aac5

Browse files
committed
Check C++ files when running ./x fmt
1 parent 89d4c65 commit 248aac5

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
@@ -65,6 +65,7 @@ xz2 = "0.1"
6565

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

6970
[target.'cfg(windows)'.dependencies.junction]
7071
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]) {
@@ -134,6 +309,23 @@ pub fn format(build: &Builder<'_>, check: bool, all: bool, paths: &[PathBuf]) {
134309
let mut builder = ignore::types::TypesBuilder::new();
135310
builder.add_defaults();
136311
builder.select("rust");
312+
313+
let mut c_family_builder = ignore::types::TypesBuilder::new();
314+
c_family_builder.add_defaults();
315+
c_family_builder.select("c");
316+
c_family_builder.select("cpp");
317+
318+
// TODO: Allow to configure the path of clang-format?
319+
let clang_format_path = get_clang_format();
320+
let clang_format_available = clang_format_path.is_some();
321+
// TODO: Check the clang-format version.
322+
if !clang_format_available && CiEnv::is_ci() {
323+
eprintln!("fmt error: Can't find `clang-format`.");
324+
crate::exit!(1);
325+
}
326+
let mut c_family_override_builder = ignore::overrides::OverrideBuilder::new(&build.src);
327+
let mut run_clang_format = clang_format_available;
328+
137329
let matcher = builder.build().unwrap();
138330
let rustfmt_config = build.src.join("rustfmt.toml");
139331
if !rustfmt_config.exists() {
@@ -156,8 +348,29 @@ pub fn format(build: &Builder<'_>, check: bool, all: bool, paths: &[PathBuf]) {
156348
crate::exit!(1);
157349
} else {
158350
override_builder.add(&format!("!{ignore}")).expect(&ignore);
351+
c_family_override_builder.add(&format!("!{ignore}")).expect(&ignore);
159352
}
160353
}
354+
355+
let clang_format_config = build.src.join("clang-format.toml");
356+
if !clang_format_config.exists() {
357+
eprintln!("fmt error: Not running formatting checks; clang-format.toml does not exist.");
358+
eprintln!("fmt error: This may happen in distributed tarballs.");
359+
return;
360+
}
361+
let clang_format_config = t!(std::fs::read_to_string(&clang_format_config));
362+
let clang_format_config: ClangFormatConfig = t!(toml::from_str(&clang_format_config));
363+
for ignore in clang_format_config.ignore {
364+
if ignore.starts_with('!') {
365+
eprintln!(
366+
"fmt error: `!`-prefixed entries are not supported in clang-format.toml, sorry"
367+
);
368+
crate::exit!(1);
369+
} else {
370+
c_family_override_builder.add(&format!("!{ignore}")).expect(&ignore);
371+
}
372+
}
373+
161374
let git_available = match Command::new("git")
162375
.arg("--version")
163376
.stdout(Stdio::null())
@@ -208,6 +421,9 @@ pub fn format(build: &Builder<'_>, check: bool, all: bool, paths: &[PathBuf]) {
208421
// against anything like `compiler/rustc_foo/src/foo.rs`,
209422
// preventing the latter from being formatted.
210423
override_builder.add(&format!("!/{untracked_path}")).expect(&untracked_path);
424+
c_family_override_builder
425+
.add(&format!("!/{untracked_path}"))
426+
.expect(&untracked_path);
211427
}
212428
if !all {
213429
adjective = Some("modified");
@@ -224,6 +440,28 @@ pub fn format(build: &Builder<'_>, check: bool, all: bool, paths: &[PathBuf]) {
224440
eprintln!("fmt warning: Falling back to formatting all files.");
225441
}
226442
}
443+
match get_modified_c_family_files(build) {
444+
Ok(Some(files)) => {
445+
if !files.is_empty() && !clang_format_available {
446+
eprintln!(
447+
"fmt warning: Modified C++ files but couldn't find the available clang-format."
448+
);
449+
}
450+
run_clang_format = run_clang_format && !files.is_empty();
451+
for file in files {
452+
c_family_override_builder.add(&format!("/{file}")).expect(&file);
453+
}
454+
}
455+
Ok(None) => {
456+
eprintln!("no check");
457+
run_clang_format = false;
458+
}
459+
Err(err) => {
460+
eprintln!("fmt warning: Something went wrong running git commands:");
461+
eprintln!("fmt warning: {err}");
462+
eprintln!("fmt warning: Falling back to formatting all files.");
463+
}
464+
}
227465
}
228466
} else {
229467
eprintln!("fmt: warning: Not in git tree. Skipping git-aware format checks");
@@ -241,6 +479,7 @@ pub fn format(build: &Builder<'_>, check: bool, all: bool, paths: &[PathBuf]) {
241479
assert!(rustfmt_path.exists(), "{}", rustfmt_path.display());
242480
let src = build.src.clone();
243481
let (tx, rx): (SyncSender<PathBuf>, _) = std::sync::mpsc::sync_channel(128);
482+
let (c_family_tx, c_family_rx): (SyncSender<PathBuf>, _) = std::sync::mpsc::sync_channel(128);
244483
let walker = WalkBuilder::new(src.clone()).types(matcher).overrides(override_).build_parallel();
245484

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

0 commit comments

Comments
 (0)