9
9
import glob
10
10
import itertools
11
11
import json
12
+ import multiprocessing
12
13
import os
14
+ import queue
15
+ import shutil
16
+ import sys
17
+ import tempfile
18
+ import threading
19
+ import traceback
13
20
from operator import itemgetter
14
21
import pprint
15
22
import pathlib
@@ -161,50 +168,48 @@ def get_auth_from_arguments(args: argparse.Namespace) -> Auth:
161
168
162
169
163
170
def build_clang_tidy_warnings (
164
- line_filter ,
165
- build_dir ,
166
- clang_tidy_checks ,
167
- clang_tidy_binary : pathlib .Path ,
168
- config_file ,
169
- files ,
170
- username : str ,
171
+ base_invocation : List ,
172
+ env : dict ,
173
+ tmpdir : str ,
174
+ task_queue : queue .Queue ,
175
+ lock : threading .Lock ,
176
+ failed_files : List ,
171
177
) -> None :
172
- """Run clang-tidy on the given files and save output into FIXES_FILE """
178
+ """Run clang-tidy on the given files and save output into a temporary file """
173
179
174
- config = config_file_or_checks (clang_tidy_binary , clang_tidy_checks , config_file )
180
+ while True :
181
+ name = task_queue .get ()
182
+ invocation = base_invocation [:]
175
183
176
- args = [
177
- clang_tidy_binary ,
178
- f"-p={ build_dir } " ,
179
- f"-line-filter={ line_filter } " ,
180
- f"--export-fixes={ FIXES_FILE } " ,
181
- "--enable-check-profile" ,
182
- f"-store-check-profile={ PROFILE_DIR } " ,
183
- ]
184
+ # Get a temporary file. We immediately close the handle so clang-tidy can
185
+ # overwrite it.
186
+ (handle , fixes_file ) = tempfile .mkstemp (suffix = ".yaml" , dir = tmpdir )
187
+ os .close (handle )
188
+ invocation .append (f"--export-fixes={ fixes_file } " )
184
189
185
- if config :
186
- print (f"Using config: { config } " )
187
- args .append (config )
188
- else :
189
- print ("Using recursive directory config" )
190
+ invocation .append (name )
190
191
191
- args += files
192
-
193
- try :
194
- with message_group (f"Running:\n \t { args } " ):
195
- env = dict (os .environ )
196
- env ["USER" ] = username
197
- subprocess .run (
198
- args ,
199
- capture_output = True ,
200
- check = True ,
201
- encoding = "utf-8" ,
202
- env = env ,
203
- )
204
- except subprocess .CalledProcessError as e :
205
- print (
206
- f"\n \n clang-tidy failed with return code { e .returncode } and error:\n { e .stderr } \n Output was:\n { e .stdout } "
192
+ proc = subprocess .Popen (
193
+ invocation , stdout = subprocess .PIPE , stderr = subprocess .PIPE , env = env
207
194
)
195
+ output , err = proc .communicate ()
196
+ end = datetime .datetime .now ()
197
+
198
+ if proc .returncode != 0 :
199
+ if proc .returncode < 0 :
200
+ msg = f"{ name } : terminated by signal { - proc .returncode } \n "
201
+ err += msg .encode ("utf-8" )
202
+ failed_files .append (name )
203
+ with lock :
204
+ subprocess .list2cmdline (invocation )
205
+ sys .stdout .write (
206
+ f'{ name } : { subprocess .list2cmdline (invocation )} \n { output .decode ("utf-8" )} '
207
+ )
208
+ if len (err ) > 0 :
209
+ sys .stdout .flush ()
210
+ sys .stderr .write (err .decode ("utf-8" ))
211
+
212
+ task_queue .task_done ()
208
213
209
214
210
215
def clang_tidy_version (clang_tidy_binary : pathlib .Path ):
@@ -250,11 +255,33 @@ def config_file_or_checks(
250
255
return "--config"
251
256
252
257
253
- def load_clang_tidy_warnings ():
254
- """Read clang-tidy warnings from FIXES_FILE. Can be produced by build_clang_tidy_warnings"""
258
+ def merge_replacement_files (tmpdir : str , mergefile : str ):
259
+ """Merge all replacement files in a directory into a single file"""
260
+ # The fixes suggested by clang-tidy >= 4.0.0 are given under
261
+ # the top level key 'Diagnostics' in the output yaml files
262
+ mergekey = "Diagnostics"
263
+ merged = []
264
+ for replacefile in glob .iglob (os .path .join (tmpdir , "*.yaml" )):
265
+ content = yaml .safe_load (open (replacefile , "r" ))
266
+ if not content :
267
+ continue # Skip empty files.
268
+ merged .extend (content .get (mergekey , []))
269
+
270
+ if merged :
271
+ # MainSourceFile: The key is required by the definition inside
272
+ # include/clang/Tooling/ReplacementsYaml.h, but the value
273
+ # is actually never used inside clang-apply-replacements,
274
+ # so we set it to '' here.
275
+ output = {"MainSourceFile" : "" , mergekey : merged }
276
+ with open (mergefile , "w" ) as out :
277
+ yaml .safe_dump (output , out )
278
+
279
+
280
+ def load_clang_tidy_warnings (fixes_file ) -> Dict :
281
+ """Read clang-tidy warnings from fixes_file. Can be produced by build_clang_tidy_warnings"""
255
282
try :
256
- with open (FIXES_FILE , "r" ) as fixes_file :
257
- return yaml .safe_load (fixes_file )
283
+ with open (fixes_file , "r" ) as file :
284
+ return yaml .safe_load (file )
258
285
except FileNotFoundError :
259
286
return {}
260
287
@@ -824,7 +851,9 @@ def create_review_file(
824
851
return review
825
852
826
853
827
- def make_timing_summary (clang_tidy_profiling : Dict , sha : Optional [str ] = None ) -> str :
854
+ def make_timing_summary (
855
+ clang_tidy_profiling : Dict , real_time : datetime .timedelta , sha : Optional [str ] = None
856
+ ) -> str :
828
857
if not clang_tidy_profiling :
829
858
return ""
830
859
top_amount = 10
@@ -901,7 +930,9 @@ def make_timing_summary(clang_tidy_profiling: Dict, sha: Optional[str] = None) -
901
930
c = decorate_check_names (f"[{ c } ]" ).replace ("[[" , "[" ).rstrip ("]" )
902
931
check_summary += f"|{ c } |{ u :.2f} |{ s :.2f} |{ w :.2f} |\n "
903
932
904
- return f"## Timing\n { file_summary } { check_summary } "
933
+ return (
934
+ f"## Timing\n Real time: { real_time .seconds :.2f} \n { file_summary } { check_summary } "
935
+ )
905
936
906
937
907
938
def filter_files (diff , include : List [str ], exclude : List [str ]) -> List :
@@ -923,6 +954,7 @@ def create_review(
923
954
clang_tidy_checks : str ,
924
955
clang_tidy_binary : pathlib .Path ,
925
956
config_file : str ,
957
+ max_task : int ,
926
958
include : List [str ],
927
959
exclude : List [str ],
928
960
) -> Optional [PRReview ]:
@@ -931,6 +963,9 @@ def create_review(
931
963
932
964
"""
933
965
966
+ if max_task == 0 :
967
+ max_task = multiprocessing .cpu_count ()
968
+
934
969
diff = pull_request .get_pr_diff ()
935
970
print (f"\n Diff from GitHub PR:\n { diff } \n " )
936
971
@@ -970,18 +1005,68 @@ def create_review(
970
1005
username = pull_request .get_pr_author () or "your name here"
971
1006
972
1007
# Run clang-tidy with the configured parameters and produce the CLANG_TIDY_FIXES file
973
- build_clang_tidy_warnings (
974
- line_ranges ,
975
- build_dir ,
976
- clang_tidy_checks ,
1008
+ return_code = 0
1009
+ export_fixes_dir = tempfile .mkdtemp ()
1010
+ env = dict (os .environ , USER = username )
1011
+ config = config_file_or_checks (clang_tidy_binary , clang_tidy_checks , config_file )
1012
+ base_invocation = [
977
1013
clang_tidy_binary ,
978
- config_file ,
979
- files ,
980
- username ,
981
- )
1014
+ f"-p={ build_dir } " ,
1015
+ f"-line-filter={ line_ranges } " ,
1016
+ "--enable-check-profile" ,
1017
+ f"-store-check-profile={ PROFILE_DIR } " ,
1018
+ ]
1019
+ if config :
1020
+ print (f"Using config: { config } " )
1021
+ base_invocation .append (config )
1022
+ else :
1023
+ print ("Using recursive directory config" )
1024
+
1025
+ print (f"Spawning a task queue with { max_task } processes" )
1026
+ start = datetime .datetime .now ()
1027
+ try :
1028
+ # Spin up a bunch of tidy-launching threads.
1029
+ task_queue = queue .Queue (max_task )
1030
+ # List of files with a non-zero return code.
1031
+ failed_files = []
1032
+ lock = threading .Lock ()
1033
+ for _ in range (max_task ):
1034
+ t = threading .Thread (
1035
+ target = build_clang_tidy_warnings ,
1036
+ args = (
1037
+ base_invocation ,
1038
+ env ,
1039
+ export_fixes_dir ,
1040
+ task_queue ,
1041
+ lock ,
1042
+ failed_files ,
1043
+ ),
1044
+ )
1045
+ t .daemon = True
1046
+ t .start ()
1047
+
1048
+ # Fill the queue with files.
1049
+ for name in files :
1050
+ task_queue .put (name )
1051
+
1052
+ # Wait for all threads to be done.
1053
+ task_queue .join ()
1054
+ if len (failed_files ):
1055
+ return_code = 1
1056
+
1057
+ except KeyboardInterrupt :
1058
+ # This is a sad hack. Unfortunately subprocess goes
1059
+ # bonkers with ctrl-c and we start forking merrily.
1060
+ print ("\n Ctrl-C detected, goodbye." )
1061
+ os .kill (0 , 9 )
1062
+ raise
1063
+ real_duration = datetime .datetime .now () - start
982
1064
983
1065
# Read and parse the CLANG_TIDY_FIXES file
984
- clang_tidy_warnings = load_clang_tidy_warnings ()
1066
+ print ("Writing fixes to " + FIXES_FILE + " ..." )
1067
+ merge_replacement_files (export_fixes_dir , FIXES_FILE )
1068
+ shutil .rmtree (export_fixes_dir )
1069
+ clang_tidy_warnings = load_clang_tidy_warnings (FIXES_FILE )
985
1070
986
1071
# Read and parse the timing data
987
1072
clang_tidy_profiling = load_and_merge_profiling ()
@@ -992,7 +1077,7 @@ def create_review(
992
1077
sha = os .environ .get ("GITHUB_SHA" )
993
1078
994
1079
# Post to the action job summary
995
- step_summary = make_timing_summary (clang_tidy_profiling , sha )
1080
+ step_summary = make_timing_summary (clang_tidy_profiling , real_duration , sha )
996
1081
set_summary (step_summary )
997
1082
998
1083
print ("clang-tidy had the following warnings:\n " , clang_tidy_warnings , flush = True )
0 commit comments