6
6
import argparse
7
7
import base64
8
8
import contextlib
9
+ import datetime
9
10
import fnmatch
11
+ import glob
10
12
import io
11
13
import itertools
12
14
import json
15
+ import multiprocessing
13
16
import os
14
17
import pathlib
15
18
import pprint
19
+ import queue
16
20
import re
21
+ import shutil
17
22
import subprocess
23
+ import sys
24
+ import tempfile
18
25
import textwrap
26
+ import threading
27
+ import traceback
19
28
import zipfile
20
29
from operator import itemgetter
21
30
from pathlib import Path
@@ -150,50 +159,48 @@ def get_auth_from_arguments(args: argparse.Namespace) -> Auth.Auth:
150
159
151
160
152
161
def build_clang_tidy_warnings (
153
- line_filter ,
154
- build_dir ,
155
- clang_tidy_checks ,
156
- clang_tidy_binary : pathlib .Path ,
157
- config_file ,
158
- files ,
159
- username : str ,
162
+ base_invocation : List ,
163
+ env : dict ,
164
+ tmpdir : str ,
165
+ task_queue : queue .Queue ,
166
+ lock : threading .Lock ,
167
+ failed_files : List ,
160
168
) -> None :
161
- """Run clang-tidy on the given files and save output into FIXES_FILE """
169
+ """Run clang-tidy on the given files and save output into a temporary file """
162
170
163
- config = config_file_or_checks (clang_tidy_binary , clang_tidy_checks , config_file )
171
+ while True :
172
+ name = task_queue .get ()
173
+ invocation = base_invocation [:]
164
174
165
- args = [
166
- clang_tidy_binary ,
167
- f"-p={ build_dir } " ,
168
- f"-line-filter={ line_filter } " ,
169
- f"--export-fixes={ FIXES_FILE } " ,
170
- "--enable-check-profile" ,
171
- f"-store-check-profile={ PROFILE_DIR } " ,
172
- ]
175
+ # Get a temporary file. We immediately close the handle so clang-tidy can
176
+ # overwrite it.
177
+ (handle , fixes_file ) = tempfile .mkstemp (suffix = ".yaml" , dir = tmpdir )
178
+ os .close (handle )
179
+ invocation .append (f"--export-fixes={ fixes_file } " )
173
180
174
- if config :
175
- print (f"Using config: { config } " )
176
- args .append (config )
177
- else :
178
- print ("Using recursive directory config" )
181
+ invocation .append (name )
179
182
180
- args += files
181
-
182
- try :
183
- with message_group (f"Running:\n \t { args } " ):
184
- env = dict (os .environ )
185
- env ["USER" ] = username
186
- subprocess .run (
187
- args ,
188
- capture_output = True ,
189
- check = True ,
190
- encoding = "utf-8" ,
191
- env = env ,
192
- )
193
- except subprocess .CalledProcessError as e :
194
- print (
195
- f"\n \n clang-tidy failed with return code { e .returncode } and error:\n { e .stderr } \n Output was:\n { e .stdout } "
183
+ proc = subprocess .Popen (
184
+ invocation , stdout = subprocess .PIPE , stderr = subprocess .PIPE , env = env
196
185
)
186
+ output , err = proc .communicate ()
187
+ end = datetime .datetime .now ()
188
+
189
+ if proc .returncode != 0 :
190
+ if proc .returncode < 0 :
191
+ msg = f"{ name } : terminated by signal { - proc .returncode } \n "
192
+ err += msg .encode ("utf-8" )
193
+ failed_files .append (name )
194
+ with lock :
195
+ subprocess .list2cmdline (invocation )
196
+ sys .stdout .write (
197
+ f'{ name } : { subprocess .list2cmdline (invocation )} \n { output .decode ("utf-8" )} '
198
+ )
199
+ if len (err ) > 0 :
200
+ sys .stdout .flush ()
201
+ sys .stderr .write (err .decode ("utf-8" ))
202
+
203
+ task_queue .task_done ()
197
204
198
205
199
206
def clang_tidy_version (clang_tidy_binary : pathlib .Path ):
@@ -239,8 +246,30 @@ def config_file_or_checks(
239
246
return "--config"
240
247
241
248
242
- def load_clang_tidy_warnings ():
243
- """Read clang-tidy warnings from FIXES_FILE. Can be produced by build_clang_tidy_warnings"""
249
+ def merge_replacement_files (tmpdir : str , mergefile : str ):
250
+ """Merge all replacement files in a directory into a single file"""
251
+ # The fixes suggested by clang-tidy >= 4.0.0 are given under
252
+ # the top level key 'Diagnostics' in the output yaml files
253
+ mergekey = "Diagnostics"
254
+ merged = []
255
+ for replacefile in glob .iglob (os .path .join (tmpdir , "*.yaml" )):
256
+ content = yaml .safe_load (open (replacefile , "r" ))
257
+ if not content :
258
+ continue # Skip empty files.
259
+ merged .extend (content .get (mergekey , []))
260
+
261
+ if merged :
262
+ # MainSourceFile: The key is required by the definition inside
263
+ # include/clang/Tooling/ReplacementsYaml.h, but the value
264
+ # is actually never used inside clang-apply-replacements,
265
+ # so we set it to '' here.
266
+ output = {"MainSourceFile" : "" , mergekey : merged }
267
+ with open (mergefile , "w" ) as out :
268
+ yaml .safe_dump (output , out )
269
+
270
+
271
+ def load_clang_tidy_warnings (fixes_file ) -> Dict :
272
+ """Read clang-tidy warnings from fixes_file. Can be produced by build_clang_tidy_warnings"""
244
273
try :
245
274
with Path (FIXES_FILE ).open () as fixes_file :
246
275
return yaml .safe_load (fixes_file )
@@ -807,7 +836,9 @@ def create_review_file(
807
836
return review
808
837
809
838
810
- def make_timing_summary (clang_tidy_profiling : Dict , sha : Optional [str ] = None ) -> str :
839
+ def make_timing_summary (
840
+ clang_tidy_profiling : Dict , real_time : datetime .timedelta , sha : Optional [str ] = None
841
+ ) -> str :
811
842
if not clang_tidy_profiling :
812
843
return ""
813
844
top_amount = 10
@@ -884,7 +915,9 @@ def make_timing_summary(clang_tidy_profiling: Dict, sha: Optional[str] = None) -
884
915
c = decorate_check_names (f"[{ c } ]" ).replace ("[[" , "[" ).rstrip ("]" )
885
916
check_summary += f"|{ c } |{ u :.2f} |{ s :.2f} |{ w :.2f} |\n "
886
917
887
- return f"## Timing\n { file_summary } { check_summary } "
918
+ return (
919
+ f"## Timing\n Real time: { real_time .seconds :.2f} \n { file_summary } { check_summary } "
920
+ )
888
921
889
922
890
923
def filter_files (diff , include : List [str ], exclude : List [str ]) -> List :
@@ -906,6 +939,7 @@ def create_review(
906
939
clang_tidy_checks : str ,
907
940
clang_tidy_binary : pathlib .Path ,
908
941
config_file : str ,
942
+ max_task : int ,
909
943
include : List [str ],
910
944
exclude : List [str ],
911
945
) -> Optional [PRReview ]:
@@ -914,6 +948,9 @@ def create_review(
914
948
915
949
"""
916
950
951
+ if max_task == 0 :
952
+ max_task = multiprocessing .cpu_count ()
953
+
917
954
diff = pull_request .get_pr_diff ()
918
955
print (f"\n Diff from GitHub PR:\n { diff } \n " )
919
956
@@ -955,18 +992,68 @@ def create_review(
955
992
username = pull_request .get_pr_author () or "your name here"
956
993
957
994
# Run clang-tidy with the configured parameters and produce the CLANG_TIDY_FIXES file
958
- build_clang_tidy_warnings (
959
- line_ranges ,
960
- build_dir ,
961
- clang_tidy_checks ,
995
+ return_code = 0
996
+ export_fixes_dir = tempfile .mkdtemp ()
997
+ env = dict (os .environ , USER = username )
998
+ config = config_file_or_checks (clang_tidy_binary , clang_tidy_checks , config_file )
999
+ base_invocation = [
962
1000
clang_tidy_binary ,
963
- config_file ,
964
- files ,
965
- username ,
966
- )
1001
+ f"-p={ build_dir } " ,
1002
+ f"-line-filter={ line_ranges } " ,
1003
+ "--enable-check-profile" ,
1004
+ f"-store-check-profile={ PROFILE_DIR } " ,
1005
+ ]
1006
+ if config :
1007
+ print (f"Using config: { config } " )
1008
+ base_invocation .append (config )
1009
+ else :
1010
+ print ("Using recursive directory config" )
1011
+
1012
+ print (f"Spawning a task queue with { max_task } processes" )
1013
+ start = datetime .datetime .now ()
1014
+ try :
1015
+ # Spin up a bunch of tidy-launching threads.
1016
+ task_queue = queue .Queue (max_task )
1017
+ # List of files with a non-zero return code.
1018
+ failed_files = []
1019
+ lock = threading .Lock ()
1020
+ for _ in range (max_task ):
1021
+ t = threading .Thread (
1022
+ target = build_clang_tidy_warnings ,
1023
+ args = (
1024
+ base_invocation ,
1025
+ env ,
1026
+ export_fixes_dir ,
1027
+ task_queue ,
1028
+ lock ,
1029
+ failed_files ,
1030
+ ),
1031
+ )
1032
+ t .daemon = True
1033
+ t .start ()
1034
+
1035
+ # Fill the queue with files.
1036
+ for name in files :
1037
+ task_queue .put (name )
1038
+
1039
+ # Wait for all threads to be done.
1040
+ task_queue .join ()
1041
+ if len (failed_files ):
1042
+ return_code = 1
1043
+
1044
+ except KeyboardInterrupt :
1045
+ # This is a sad hack. Unfortunately subprocess goes
1046
+ # bonkers with ctrl-c and we start forking merrily.
1047
+ print ("\n Ctrl-C detected, goodbye." )
1048
+ os .kill (0 , 9 )
1049
+ raise
1050
+ real_duration = datetime .datetime .now () - start
967
1051
968
1052
# Read and parse the CLANG_TIDY_FIXES file
969
- clang_tidy_warnings = load_clang_tidy_warnings ()
1053
+ print ("Writing fixes to " + FIXES_FILE + " ..." )
1054
+ merge_replacement_files (export_fixes_dir , FIXES_FILE )
1055
+ shutil .rmtree (export_fixes_dir )
1056
+ clang_tidy_warnings = load_clang_tidy_warnings (FIXES_FILE )
970
1057
971
1058
# Read and parse the timing data
972
1059
clang_tidy_profiling = load_and_merge_profiling ()
@@ -977,7 +1064,7 @@ def create_review(
977
1064
sha = os .environ .get ("GITHUB_SHA" )
978
1065
979
1066
# Post to the action job summary
980
- step_summary = make_timing_summary (clang_tidy_profiling , sha )
1067
+ step_summary = make_timing_summary (clang_tidy_profiling , real_duration , sha )
981
1068
set_summary (step_summary )
982
1069
983
1070
print ("clang-tidy had the following warnings:\n " , clang_tidy_warnings , flush = True )
0 commit comments