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 n_errors_str = res.string[res.start(1):res.end(1)] 97 n_errors = int(n_errors_str) 98 99 return (filename, n_errors) 100 101 102# The multiprocessing map_async function does not allow passing multiple 103# arguments directly, so use a wrapper. 104def LintWrapper(args): 105 # Run under a try-catch to avoid flooding the output when the script is 106 # interrupted from the keyboard with ctrl+C. 107 try: 108 return Lint(*args) 109 except: 110 sys.exit(1) 111 112 113def ShouldLint(filename, cached_results): 114 filename = os.path.realpath(filename) 115 if filename not in cached_results: 116 return True 117 with open(filename, 'rb') as f: 118 file_hash = hashlib.md5(f.read()).hexdigest() 119 return file_hash != cached_results[filename] 120 121 122# Returns the total number of errors found in the files linted. 123# `cached_results` must be a dictionary, with the format: 124# { 'filename': file_hash, 'other_filename': other_hash, ... } 125# If not `None`, `cached_results` is used to avoid re-linting files, and new 126# results are stored in it. 127def LintFiles(files, 128 jobs = 1, 129 progress_prefix = '', 130 cached_results = None): 131 if not IsCppLintAvailable(): 132 print( 133 printer.COLOUR_RED + \ 134 ("cpplint.py not found. Please ensure the depot" 135 " tools are installed and in your PATH. See" 136 " http://dev.chromium.org/developers/how-tos/install-depot-tools for" 137 " details.") + \ 138 printer.NO_COLOUR) 139 return -1 140 141 # Filter out directories. 142 files = filter(os.path.isfile, files) 143 144 # Filter out files for which we have a cached correct result. 145 if cached_results is not None and len(cached_results) != 0: 146 n_input_files = len(files) 147 files = filter(lambda f: ShouldLint(f, cached_results), files) 148 n_skipped_files = n_input_files - len(files) 149 if n_skipped_files != 0: 150 printer.Print( 151 progress_prefix + 152 'Skipping %d correct files that were already processed.' % 153 n_skipped_files) 154 155 pool = multiprocessing.Pool(jobs) 156 # The '.get(9999999)' is workaround to allow killing the test script with 157 # ctrl+C from the shell. This bug is documented at 158 # http://bugs.python.org/issue8296. 159 tasks = [(f, progress_prefix) for f in files] 160 # Run under a try-catch to avoid flooding the output when the script is 161 # interrupted from the keyboard with ctrl+C. 162 try: 163 results = pool.map_async(LintWrapper, tasks).get(9999999) 164 pool.close() 165 pool.join() 166 except KeyboardInterrupt: 167 pool.terminate() 168 sys.exit(1) 169 170 n_errors = sum(map(lambda (filename, errors): errors, results)) 171 172 if cached_results is not None: 173 for filename, errors in results: 174 if errors == 0: 175 with open(filename, 'rb') as f: 176 filename = os.path.realpath(filename) 177 file_hash = hashlib.md5(f.read()).hexdigest() 178 cached_results[filename] = file_hash 179 180 181 printer.PrintOverwritableLine( 182 progress_prefix + 'Total errors found: %d' % n_errors) 183 printer.EnsureNewLine() 184 return n_errors 185 186 187def IsCppLintAvailable(): 188 retcode, unused_output = util.getstatusoutput('which cpplint.py') 189 return retcode == 0 190 191 192CPP_EXT_REGEXP = re.compile('\.(cc|h)$') 193def IsLinterInput(filename): 194 # lint all C++ files. 195 return CPP_EXT_REGEXP.search(filename) != None 196 197 198cached_results_pkl_filename = \ 199 os.path.join(config.dir_tools, '.cached_lint_results.pkl') 200 201 202def ReadCachedResults(): 203 cached_results = {} 204 if os.path.isfile(cached_results_pkl_filename): 205 with open(cached_results_pkl_filename, 'rb') as pkl_file: 206 cached_results = pickle.load(pkl_file) 207 return cached_results 208 209 210def CacheResults(results): 211 with open(cached_results_pkl_filename, 'wb') as pkl_file: 212 pickle.dump(results, pkl_file) 213 214 215def FilterOutTestTraceHeaders(files): 216 def IsTraceHeader(f): 217 relative_aarch32_traces_path = os.path.relpath(config.dir_aarch32_traces,'.') 218 relative_aarch64_traces_path = os.path.relpath(config.dir_aarch64_traces,'.') 219 return \ 220 fnmatch.fnmatch(f, os.path.join(relative_aarch32_traces_path, '*.h')) or \ 221 fnmatch.fnmatch(f, os.path.join(relative_aarch64_traces_path, '*.h')) 222 return filter(lambda f: not IsTraceHeader(f), files) 223 224 225def RunLinter(files, jobs=1, progress_prefix='', cached=True): 226 results = {} if not cached else ReadCachedResults() 227 228 rc = LintFiles(files, 229 jobs=jobs, 230 progress_prefix=progress_prefix, 231 cached_results=results) 232 233 CacheResults(results) 234 return rc 235 236 237if __name__ == '__main__': 238 # Parse the arguments. 239 args = BuildOptions() 240 241 files = args.files or util.get_source_files() 242 243 cached = not args.no_cache 244 retcode = RunLinter(files, jobs=args.jobs, cached=cached) 245 246 sys.exit(retcode) 247