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 in the 56 repository are processed. 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 198def GetDefaultFilesToLint(): 199 if git.is_git_repository_root(config.dir_root): 200 files = git.get_tracked_files().split() 201 files = filter(IsLinterInput, files) 202 files = FilterOutTestTraceHeaders(files) 203 return 0, files 204 else: 205 printer.Print(printer.COLOUR_ORANGE + 'WARNING: This script is not run ' \ 206 'from its Git repository. The linter will not run.' + \ 207 printer.NO_COLOUR) 208 return 1, [] 209 210 211cached_results_pkl_filename = \ 212 os.path.join(config.dir_tools, '.cached_lint_results.pkl') 213 214 215def ReadCachedResults(): 216 cached_results = {} 217 if os.path.isfile(cached_results_pkl_filename): 218 with open(cached_results_pkl_filename, 'rb') as pkl_file: 219 cached_results = pickle.load(pkl_file) 220 return cached_results 221 222 223def CacheResults(results): 224 with open(cached_results_pkl_filename, 'wb') as pkl_file: 225 pickle.dump(results, pkl_file) 226 227 228def FilterOutTestTraceHeaders(files): 229 def IsTraceHeader(f): 230 relative_aarch32_traces_path = os.path.relpath(config.dir_aarch32_traces,'.') 231 relative_aarch64_traces_path = os.path.relpath(config.dir_aarch64_traces,'.') 232 return \ 233 fnmatch.fnmatch(f, os.path.join(relative_aarch32_traces_path, '*.h')) or \ 234 fnmatch.fnmatch(f, os.path.join(relative_aarch64_traces_path, '*.h')) 235 return filter(lambda f: not IsTraceHeader(f), files) 236 237 238def RunLinter(files, jobs=1, progress_prefix='', cached=True): 239 results = {} if not cached else ReadCachedResults() 240 241 rc = LintFiles(files, 242 jobs=jobs, 243 progress_prefix=progress_prefix, 244 cached_results=results) 245 246 CacheResults(results) 247 return rc 248 249 250if __name__ == '__main__': 251 # Parse the arguments. 252 args = BuildOptions() 253 254 files = args.files 255 if not files: 256 retcode, files = GetDefaultFilesToLint() 257 if retcode: 258 sys.exit(retcode) 259 260 cached = not args.no_cache 261 retcode = RunLinter(files, jobs=args.jobs, cached=cached) 262 263 sys.exit(retcode) 264