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 = "%s: terminated by signal %d\n " % (name , - proc .returncode )
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
@@ -821,7 +848,9 @@ def create_review_file(
821
848
return review
822
849
823
850
824
- def make_timing_summary (clang_tidy_profiling : Dict , sha : Optional [str ] = None ) -> str :
851
+ def make_timing_summary (
852
+ clang_tidy_profiling : Dict , real_time : datetime .timedelta , sha : Optional [str ] = None
853
+ ) -> str :
825
854
if not clang_tidy_profiling :
826
855
return ""
827
856
top_amount = 10
@@ -898,7 +927,9 @@ def make_timing_summary(clang_tidy_profiling: Dict, sha: Optional[str] = None) -
898
927
c = decorate_check_names (f"[{ c } ]" ).replace ("[[" , "[" ).rstrip ("]" )
899
928
check_summary += f"|{ c } |{ u :.2f} |{ s :.2f} |{ w :.2f} |\n "
900
929
901
- return f"## Timing\n { file_summary } { check_summary } "
930
+ return (
931
+ f"## Timing\n Real time: { real_time .seconds :.2f} \n { file_summary } { check_summary } "
932
+ )
902
933
903
934
904
935
def filter_files (diff , include : List [str ], exclude : List [str ]) -> List :
@@ -920,6 +951,7 @@ def create_review(
920
951
clang_tidy_checks : str ,
921
952
clang_tidy_binary : pathlib .Path ,
922
953
config_file : str ,
954
+ max_task : int ,
923
955
include : List [str ],
924
956
exclude : List [str ],
925
957
) -> Optional [PRReview ]:
@@ -928,6 +960,9 @@ def create_review(
928
960
929
961
"""
930
962
963
+ if max_task == 0 :
964
+ max_task = multiprocessing .cpu_count ()
965
+
931
966
diff = pull_request .get_pr_diff ()
932
967
print (f"\n Diff from GitHub PR:\n { diff } \n " )
933
968
@@ -967,18 +1002,68 @@ def create_review(
967
1002
username = pull_request .get_pr_author () or "your name here"
968
1003
969
1004
# Run clang-tidy with the configured parameters and produce the CLANG_TIDY_FIXES file
970
- build_clang_tidy_warnings (
971
- line_ranges ,
972
- build_dir ,
973
- clang_tidy_checks ,
1005
+ return_code = 0
1006
+ export_fixes_dir = tempfile .mkdtemp ()
1007
+ env = dict (os .environ , USER = username )
1008
+ config = config_file_or_checks (clang_tidy_binary , clang_tidy_checks , config_file )
1009
+ base_invocation = [
974
1010
clang_tidy_binary ,
975
- config_file ,
976
- files ,
977
- username ,
978
- )
1011
+ f"-p={ build_dir } " ,
1012
+ f"-line-filter={ line_ranges } " ,
1013
+ "--enable-check-profile" ,
1014
+ f"-store-check-profile={ PROFILE_DIR } " ,
1015
+ ]
1016
+ if config :
1017
+ print (f"Using config: { config } " )
1018
+ base_invocation .append (config )
1019
+ else :
1020
+ print ("Using recursive directory config" )
1021
+
1022
+ print (f"Spawning a task queue with { max_task } processes" )
1023
+ start = datetime .datetime .now ()
1024
+ try :
1025
+ # Spin up a bunch of tidy-launching threads.
1026
+ task_queue = queue .Queue (max_task )
1027
+ # List of files with a non-zero return code.
1028
+ failed_files = []
1029
+ lock = threading .Lock ()
1030
+ for _ in range (max_task ):
1031
+ t = threading .Thread (
1032
+ target = build_clang_tidy_warnings ,
1033
+ args = (
1034
+ base_invocation ,
1035
+ env ,
1036
+ export_fixes_dir ,
1037
+ task_queue ,
1038
+ lock ,
1039
+ failed_files ,
1040
+ ),
1041
+ )
1042
+ t .daemon = True
1043
+ t .start ()
1044
+
1045
+ # Fill the queue with files.
1046
+ for name in files :
1047
+ task_queue .put (name )
1048
+
1049
+ # Wait for all threads to be done.
1050
+ task_queue .join ()
1051
+ if len (failed_files ):
1052
+ return_code = 1
1053
+
1054
+ except KeyboardInterrupt :
1055
+ # This is a sad hack. Unfortunately subprocess goes
1056
+ # bonkers with ctrl-c and we start forking merrily.
1057
+ print ("\n Ctrl-C detected, goodbye." )
1058
+ os .kill (0 , 9 )
1059
+ raise
1060
+ real_duration = datetime .datetime .now () - start
979
1061
980
1062
# Read and parse the CLANG_TIDY_FIXES file
981
- clang_tidy_warnings = load_clang_tidy_warnings ()
1063
+ print ("Writing fixes to " + FIXES_FILE + " ..." )
1064
+ merge_replacement_files (export_fixes_dir , FIXES_FILE )
1065
+ shutil .rmtree (export_fixes_dir )
1066
+ clang_tidy_warnings = load_clang_tidy_warnings (FIXES_FILE )
982
1067
983
1068
# Read and parse the timing data
984
1069
clang_tidy_profiling = load_and_merge_profiling ()
@@ -990,7 +1075,7 @@ def create_review(
990
1075
991
1076
# Post to the action job summary
992
1077
step_summary = ""
993
- step_summary += make_timing_summary (clang_tidy_profiling , sha )
1078
+ step_summary += make_timing_summary (clang_tidy_profiling , real_duration , sha )
994
1079
set_summary (step_summary )
995
1080
996
1081
print ("clang-tidy had the following warnings:\n " , clang_tidy_warnings , flush = True )
0 commit comments