1#!/usr/bin/env python3 2 3# Copyright 2015, VIXL authors 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 9# * Redistributions of source code must retain the above copyright notice, 10# this list of conditions and the following disclaimer. 11# * Redistributions in binary form must reproduce the above copyright notice, 12# this list of conditions and the following disclaimer in the documentation 13# and/or other materials provided with the distribution. 14# * Neither the name of ARM Limited nor the names of its contributors may be 15# used to endorse or promote products derived from this software without 16# specific prior written permission. 17# 18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS CONTRIBUTORS "AS IS" AND 19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 22# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29import argparse 30import fnmatch 31import hashlib 32import multiprocessing 33import os 34import pickle 35import re 36import signal 37import subprocess 38import sys 39 40import config 41import printer 42import util 43 44 45# Catch SIGINT to gracefully exit when ctrl+C is pressed. 46def sigint_handler(signal, frame): 47 sys.exit(1) 48signal.signal(signal.SIGINT, sigint_handler) 49 50def BuildOptions(): 51 parser = argparse.ArgumentParser( 52 description = 53 '''This tool lints C++ files and produces a summary of the errors found. 54 If no files are provided on the command-line, all C++ source files are 55 processed, except for the test traces. 56 Results are cached to speed up the process. 57 ''', 58 # Print default values. 59 formatter_class=argparse.ArgumentDefaultsHelpFormatter) 60 parser.add_argument('files', nargs = '*') 61 parser.add_argument('--jobs', '-j', metavar='N', type=int, nargs='?', 62 default=multiprocessing.cpu_count(), 63 const=multiprocessing.cpu_count(), 64 help='''Runs the tests using N jobs. If the option is set 65 but no value is provided, the script will use as many jobs 66 as it thinks useful.''') 67 parser.add_argument('--no-cache', 68 action='store_true', default=False, 69 help='Do not use cached lint results.') 70 return parser.parse_args() 71 72 73 74# Returns a tuple (filename, number of lint errors). 75def Lint(filename, progress_prefix = ''): 76 command = ['cpplint.py', filename] 77 process = subprocess.Popen(command, 78 stdout=subprocess.PIPE, 79 stderr=subprocess.STDOUT) 80 81 outerr, _ = process.communicate() 82 83 if process.returncode == 0: 84 printer.PrintOverwritableLine( 85 progress_prefix + "Done processing %s" % filename, 86 type = printer.LINE_TYPE_LINTER) 87 return (filename, 0) 88 89 if progress_prefix: 90 outerr = re.sub('^', progress_prefix, outerr, flags=re.MULTILINE) 91 printer.Print(outerr) 92 93 # Find the number of errors in this file. 94 res = re.search('Total errors found: (\d+)', outerr) 95 if res: 96 n_errors_str = res.string[res.start(1):res.end(1)] 97 n_errors = int(n_errors_str) 98 else: 99 print("Couldn't parse cpplint.py output.") 100 n_errors = -1 101 102 return (filename, n_errors) 103 104 105# The multiprocessing map_async function does not allow passing multiple 106# arguments directly, so use a wrapper. 107def LintWrapper(args): 108 # Run under a try-catch to avoid flooding the output when the script is 109 # interrupted from the keyboard with ctrl+C. 110 try: 111 return Lint(*args) 112 except: 113 sys.exit(1) 114 115 116def ShouldLint(filename, cached_results): 117 filename = os.path.realpath(filename) 118 if filename not in cached_results: 119 return True 120 with open(filename, 'rb') as f: 121 file_hash = hashlib.md5(f.read()).hexdigest() 122 return file_hash != cached_results[filename] 123 124 125# Returns the total number of errors found in the files linted. 126# `cached_results` must be a dictionary, with the format: 127# { 'filename': file_hash, 'other_filename': other_hash, ... } 128# If not `None`, `cached_results` is used to avoid re-linting files, and new 129# results are stored in it. 130def LintFiles(files, 131 jobs = 1, 132 progress_prefix = '', 133 cached_results = None): 134 if not IsCppLintAvailable(): 135 print( 136 printer.COLOUR_RED + \ 137 ("cpplint.py not found. Please ensure the depot" 138 " tools are installed and in your PATH. See" 139 " http://dev.chromium.org/developers/how-tos/install-depot-tools for" 140 " details.") + \ 141 printer.NO_COLOUR) 142 return -1 143 144 # Filter out directories. 145 files = list(filter(os.path.isfile, files)) 146 147 # Filter out files for which we have a cached correct result. 148 if cached_results is not None and len(cached_results) != 0: 149 n_input_files = len(files) 150 files = [f for f in files if ShouldLint(f, cached_results)] 151 n_skipped_files = n_input_files - len(files) 152 if n_skipped_files != 0: 153 printer.Print( 154 progress_prefix + 155 'Skipping %d correct files that were already processed.' % 156 n_skipped_files) 157 158 pool = multiprocessing.Pool(jobs) 159 # The '.get(9999999)' is workaround to allow killing the test script with 160 # ctrl+C from the shell. This bug is documented at 161 # http://bugs.python.org/issue8296. 162 tasks = [(f, progress_prefix) for f in files] 163 # Run under a try-catch to avoid flooding the output when the script is 164 # interrupted from the keyboard with ctrl+C. 165 try: 166 results = pool.map_async(LintWrapper, tasks).get(9999999) 167 pool.close() 168 pool.join() 169 except KeyboardInterrupt: 170 pool.terminate() 171 sys.exit(1) 172 173 n_errors = sum([filename_errors[1] for filename_errors in results]) 174 175 if cached_results is not None: 176 for filename, errors in results: 177 if errors == 0: 178 with open(filename, 'rb') as f: 179 filename = os.path.realpath(filename) 180 file_hash = hashlib.md5(f.read()).hexdigest() 181 cached_results[filename] = file_hash 182 183 184 printer.PrintOverwritableLine( 185 progress_prefix + 'Total errors found: %d' % n_errors) 186 printer.EnsureNewLine() 187 return n_errors 188 189 190def IsCppLintAvailable(): 191 retcode, unused_output = util.getstatusoutput('which cpplint.py') 192 return retcode == 0 193 194 195CPP_EXT_REGEXP = re.compile('\.(cc|h)$') 196def IsLinterInput(filename): 197 # lint all C++ files. 198 return CPP_EXT_REGEXP.search(filename) != None 199 200 201cached_results_pkl_filename = \ 202 os.path.join(config.dir_tools, '.cached_lint_results.pkl') 203 204 205def ReadCachedResults(): 206 cached_results = {} 207 if os.path.isfile(cached_results_pkl_filename): 208 with open(cached_results_pkl_filename, 'rb') as pkl_file: 209 cached_results = pickle.load(pkl_file) 210 return cached_results 211 212 213def CacheResults(results): 214 with open(cached_results_pkl_filename, 'wb') as pkl_file: 215 pickle.dump(results, pkl_file) 216 217 218def FilterOutTestTraceHeaders(files): 219 def IsTraceHeader(f): 220 relative_aarch32_traces_path = os.path.relpath(config.dir_aarch32_traces,'.') 221 relative_aarch64_traces_path = os.path.relpath(config.dir_aarch64_traces,'.') 222 return \ 223 fnmatch.fnmatch(f, os.path.join(relative_aarch32_traces_path, '*.h')) or \ 224 fnmatch.fnmatch(f, os.path.join(relative_aarch64_traces_path, '*.h')) 225 return [f for f in files if not IsTraceHeader(f)] 226 227 228def RunLinter(files, jobs=1, progress_prefix='', cached=True): 229 results = {} if not cached else ReadCachedResults() 230 231 rc = LintFiles(files, 232 jobs=jobs, 233 progress_prefix=progress_prefix, 234 cached_results=results) 235 236 CacheResults(results) 237 return rc 238 239 240if __name__ == '__main__': 241 # Parse the arguments. 242 args = BuildOptions() 243 244 files = args.files or util.get_source_files() 245 246 cached = not args.no_cache 247 retcode = RunLinter(files, jobs=args.jobs, cached=cached) 248 249 sys.exit(retcode) 250