1#!/usr/bin/env python2.7 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 git 42import printer 43import util 44 45 46# Catch SIGINT to gracefully exit when ctrl+C is pressed. 47def sigint_handler(signal, frame): 48 sys.exit(1) 49signal.signal(signal.SIGINT, sigint_handler) 50 51def BuildOptions(): 52 parser = argparse.ArgumentParser( 53 description = 54 '''This tool lints C++ files and produces a summary of the errors found. 55 If no files are provided on the command-line, all C++ source files are 56 processed, except for the test traces. 57 Results are cached to speed up the process. 58 ''', 59 # Print default values. 60 formatter_class=argparse.ArgumentDefaultsHelpFormatter) 61 parser.add_argument('files', nargs = '*') 62 parser.add_argument('--jobs', '-j', metavar='N', type=int, nargs='?', 63 default=multiprocessing.cpu_count(), 64 const=multiprocessing.cpu_count(), 65 help='''Runs the tests using N jobs. If the option is set 66 but no value is provided, the script will use as many jobs 67 as it thinks useful.''') 68 parser.add_argument('--no-cache', 69 action='store_true', default=False, 70 help='Do not use cached lint results.') 71 return parser.parse_args() 72 73 74 75# Returns a tuple (filename, number of lint errors). 76def Lint(filename, progress_prefix = ''): 77 command = ['cpplint.py', filename] 78 process = subprocess.Popen(command, 79 stdout=subprocess.PIPE, 80 stderr=subprocess.STDOUT) 81 82 outerr, _ = process.communicate() 83 84 if process.returncode == 0: 85 printer.PrintOverwritableLine( 86 progress_prefix + "Done processing %s" % filename, 87 type = printer.LINE_TYPE_LINTER) 88 return (filename, 0) 89 90 if progress_prefix: 91 outerr = re.sub('^', progress_prefix, outerr, flags=re.MULTILINE) 92 printer.Print(outerr) 93 94 # Find the number of errors in this file. 95 res = re.search('Total errors found: (\d+)', outerr) 96 if res: 97 n_errors_str = res.string[res.start(1):res.end(1)] 98 n_errors = int(n_errors_str) 99 else: 100 print("Couldn't parse cpplint.py output.") 101 n_errors = -1 102 103 return (filename, n_errors) 104 105 106# The multiprocessing map_async function does not allow passing multiple 107# arguments directly, so use a wrapper. 108def LintWrapper(args): 109 # Run under a try-catch to avoid flooding the output when the script is 110 # interrupted from the keyboard with ctrl+C. 111 try: 112 return Lint(*args) 113 except: 114 sys.exit(1) 115 116 117def ShouldLint(filename, cached_results): 118 filename = os.path.realpath(filename) 119 if filename not in cached_results: 120 return True 121 with open(filename, 'rb') as f: 122 file_hash = hashlib.md5(f.read()).hexdigest() 123 return file_hash != cached_results[filename] 124 125 126# Returns the total number of errors found in the files linted. 127# `cached_results` must be a dictionary, with the format: 128# { 'filename': file_hash, 'other_filename': other_hash, ... } 129# If not `None`, `cached_results` is used to avoid re-linting files, and new 130# results are stored in it. 131def LintFiles(files, 132 jobs = 1, 133 progress_prefix = '', 134 cached_results = None): 135 if not IsCppLintAvailable(): 136 print( 137 printer.COLOUR_RED + \ 138 ("cpplint.py not found. Please ensure the depot" 139 " tools are installed and in your PATH. See" 140 " http://dev.chromium.org/developers/how-tos/install-depot-tools for" 141 " details.") + \ 142 printer.NO_COLOUR) 143 return -1 144 145 # Filter out directories. 146 files = filter(os.path.isfile, files) 147 148 # Filter out files for which we have a cached correct result. 149 if cached_results is not None and len(cached_results) != 0: 150 n_input_files = len(files) 151 files = filter(lambda f: ShouldLint(f, cached_results), files) 152 n_skipped_files = n_input_files - len(files) 153 if n_skipped_files != 0: 154 printer.Print( 155 progress_prefix + 156 'Skipping %d correct files that were already processed.' % 157 n_skipped_files) 158 159 pool = multiprocessing.Pool(jobs) 160 # The '.get(9999999)' is workaround to allow killing the test script with 161 # ctrl+C from the shell. This bug is documented at 162 # http://bugs.python.org/issue8296. 163 tasks = [(f, progress_prefix) for f in files] 164 # Run under a try-catch to avoid flooding the output when the script is 165 # interrupted from the keyboard with ctrl+C. 166 try: 167 results = pool.map_async(LintWrapper, tasks).get(9999999) 168 pool.close() 169 pool.join() 170 except KeyboardInterrupt: 171 pool.terminate() 172 sys.exit(1) 173 174 n_errors = sum(map(lambda (filename, errors): errors, results)) 175 176 if cached_results is not None: 177 for filename, errors in results: 178 if errors == 0: 179 with open(filename, 'rb') as f: 180 filename = os.path.realpath(filename) 181 file_hash = hashlib.md5(f.read()).hexdigest() 182 cached_results[filename] = file_hash 183 184 185 printer.PrintOverwritableLine( 186 progress_prefix + 'Total errors found: %d' % n_errors) 187 printer.EnsureNewLine() 188 return n_errors 189 190 191def IsCppLintAvailable(): 192 retcode, unused_output = util.getstatusoutput('which cpplint.py') 193 return retcode == 0 194 195 196CPP_EXT_REGEXP = re.compile('\.(cc|h)$') 197def IsLinterInput(filename): 198 # lint all C++ files. 199 return CPP_EXT_REGEXP.search(filename) != None 200 201 202cached_results_pkl_filename = \ 203 os.path.join(config.dir_tools, '.cached_lint_results.pkl') 204 205 206def ReadCachedResults(): 207 cached_results = {} 208 if os.path.isfile(cached_results_pkl_filename): 209 with open(cached_results_pkl_filename, 'rb') as pkl_file: 210 cached_results = pickle.load(pkl_file) 211 return cached_results 212 213 214def CacheResults(results): 215 with open(cached_results_pkl_filename, 'wb') as pkl_file: 216 pickle.dump(results, pkl_file) 217 218 219def FilterOutTestTraceHeaders(files): 220 def IsTraceHeader(f): 221 relative_aarch32_traces_path = os.path.relpath(config.dir_aarch32_traces,'.') 222 relative_aarch64_traces_path = os.path.relpath(config.dir_aarch64_traces,'.') 223 return \ 224 fnmatch.fnmatch(f, os.path.join(relative_aarch32_traces_path, '*.h')) or \ 225 fnmatch.fnmatch(f, os.path.join(relative_aarch64_traces_path, '*.h')) 226 return filter(lambda f: not IsTraceHeader(f), files) 227 228 229def RunLinter(files, jobs=1, progress_prefix='', cached=True): 230 results = {} if not cached else ReadCachedResults() 231 232 rc = LintFiles(files, 233 jobs=jobs, 234 progress_prefix=progress_prefix, 235 cached_results=results) 236 237 CacheResults(results) 238 return rc 239 240 241if __name__ == '__main__': 242 # Parse the arguments. 243 args = BuildOptions() 244 245 files = args.files or util.get_source_files() 246 247 cached = not args.no_cache 248 retcode = RunLinter(files, jobs=args.jobs, cached=cached) 249 250 sys.exit(retcode) 251