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