@@ -6,6 +6,7 @@ use build_helper::ci::CiEnv;
66use build_helper:: git:: get_git_modified_files;
77use ignore:: WalkBuilder ;
88use std:: collections:: VecDeque ;
9+ use std:: io:: Read ;
910use std:: path:: { Path , PathBuf } ;
1011use std:: process:: { Command , Stdio } ;
1112use 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 ) ]
100109struct 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.\n If 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.\n If 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".
106281fn 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