@@ -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 ] ) {
@@ -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