blob: b454bfbbd170796a71e4212d4d6cb1f7c5ccb3c4 [file] [log] [blame]
armvixl4a102ba2014-07-14 09:02:40 +01001#!/usr/bin/env python2.7
2
Alexandre Ramesb78f1392016-07-01 14:22:22 +01003# Copyright 2015, VIXL authors
armvixl4a102ba2014-07-14 09:02:40 +01004# 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
Alexandre Ramesb2746622016-07-11 16:12:39 +010030import hashlib
armvixl4a102ba2014-07-14 09:02:40 +010031import multiprocessing
Alexandre Ramesb2746622016-07-11 16:12:39 +010032import os
33import pickle
armvixl4a102ba2014-07-14 09:02:40 +010034import re
armvixldb644342015-07-21 11:37:10 +010035import signal
armvixl4a102ba2014-07-14 09:02:40 +010036import subprocess
37import sys
38
armvixldb644342015-07-21 11:37:10 +010039import config
armvixl4a102ba2014-07-14 09:02:40 +010040import git
41import printer
42import util
43
44
armvixldb644342015-07-21 11:37:10 +010045# 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)
armvixl4a102ba2014-07-14 09:02:40 +010049
50def BuildOptions():
armvixl0f35e362016-05-10 13:57:58 +010051 parser = argparse.ArgumentParser(
armvixl4a102ba2014-07-14 09:02:40 +010052 description =
armvixl0f35e362016-05-10 13:57:58 +010053 '''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 in the
Alexandre Ramesb2746622016-07-11 16:12:39 +010055 repository are processed.
56 Results are cached to speed up the process.
57 ''',
armvixl4a102ba2014-07-14 09:02:40 +010058 # Print default values.
59 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
armvixl0f35e362016-05-10 13:57:58 +010060 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(),
armvixl4a102ba2014-07-14 09:02:40 +010064 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.''')
Alexandre Ramesb2746622016-07-11 16:12:39 +010067 parser.add_argument('--no-cache',
68 action='store_true', default=False,
69 help='Do not use cached lint results.')
armvixl0f35e362016-05-10 13:57:58 +010070 return parser.parse_args()
armvixl4a102ba2014-07-14 09:02:40 +010071
72
73
74__lint_results_lock__ = multiprocessing.Lock()
75
Alexandre Ramesb2746622016-07-11 16:12:39 +010076# Returns a tuple (filename, number of lint errors).
armvixldb644342015-07-21 11:37:10 +010077def Lint(filename, progress_prefix = ''):
78 command = ['cpplint.py', filename]
armvixl4a102ba2014-07-14 09:02:40 +010079 process = subprocess.Popen(command,
80 stdout=subprocess.PIPE,
81 stderr=subprocess.PIPE)
82
83 # Use a lock to avoid mixing the output for different files.
84 with __lint_results_lock__:
85 # Process the output as the process is running, until it exits.
86 LINT_ERROR_LINE_REGEXP = re.compile('\[[1-5]\]$')
87 LINT_DONE_PROC_LINE_REGEXP = re.compile('Done processing')
88 LINT_STATUS_LINE_REGEXP = re.compile('Total errors found')
89 while True:
90 retcode = process.poll()
91 while True:
92 line = process.stderr.readline()
93 if line == '': break
94 output_line = progress_prefix + line.rstrip('\r\n')
95
96 if LINT_ERROR_LINE_REGEXP.search(line):
armvixldb644342015-07-21 11:37:10 +010097 printer.PrintOverwritableLine(output_line,
98 type = printer.LINE_TYPE_LINTER)
armvixl4a102ba2014-07-14 09:02:40 +010099 printer.EnsureNewLine()
100 elif LINT_DONE_PROC_LINE_REGEXP.search(line):
armvixldb644342015-07-21 11:37:10 +0100101 printer.PrintOverwritableLine(output_line,
102 type = printer.LINE_TYPE_LINTER)
armvixl4a102ba2014-07-14 09:02:40 +0100103 elif LINT_STATUS_LINE_REGEXP.search(line):
104 status_line = line
105
106 if retcode != None: break;
107
108 if retcode == 0:
Alexandre Ramesb2746622016-07-11 16:12:39 +0100109 return (filename, 0)
armvixl4a102ba2014-07-14 09:02:40 +0100110
111 # Return the number of errors in this file.
112 res = re.search('\d+$', status_line)
113 n_errors_str = res.string[res.start():res.end()]
114 n_errors = int(n_errors_str)
115 status_line = \
116 progress_prefix + 'Total errors found in %s : %d' % (filename, n_errors)
armvixldb644342015-07-21 11:37:10 +0100117 printer.PrintOverwritableLine(status_line, type = printer.LINE_TYPE_LINTER)
armvixl4a102ba2014-07-14 09:02:40 +0100118 printer.EnsureNewLine()
Alexandre Ramesb2746622016-07-11 16:12:39 +0100119
120 return (filename, n_errors)
armvixl4a102ba2014-07-14 09:02:40 +0100121
122
123# The multiprocessing map_async function does not allow passing multiple
124# arguments directly, so use a wrapper.
125def LintWrapper(args):
armvixldb644342015-07-21 11:37:10 +0100126 # Run under a try-catch to avoid flooding the output when the script is
127 # interrupted from the keyboard with ctrl+C.
128 try:
129 return Lint(*args)
130 except:
131 sys.exit(1)
armvixl4a102ba2014-07-14 09:02:40 +0100132
133
Alexandre Ramesb2746622016-07-11 16:12:39 +0100134def ShouldLint(filename, cached_results):
135 filename = os.path.realpath(filename)
136 if filename not in cached_results:
137 return True
138 with open(filename, 'rb') as f:
139 file_hash = hashlib.md5(f.read()).hexdigest()
140 return file_hash != cached_results[filename]
141
142
armvixl4a102ba2014-07-14 09:02:40 +0100143# Returns the total number of errors found in the files linted.
Alexandre Ramesb2746622016-07-11 16:12:39 +0100144# `cached_results` must be a dictionary, with the format:
145# { 'filename': file_hash, 'other_filename': other_hash, ... }
146# If not `None`, `cached_results` is used to avoid re-linting files, and new
147# results are stored in it.
148def LintFiles(files,
149 jobs = 1,
150 progress_prefix = '',
151 cached_results = None):
armvixldb644342015-07-21 11:37:10 +0100152 if not IsCppLintAvailable():
153 print(
154 printer.COLOUR_RED + \
155 ("cpplint.py not found. Please ensure the depot"
156 " tools are installed and in your PATH. See"
157 " http://dev.chromium.org/developers/how-tos/install-depot-tools for"
158 " details.") + \
159 printer.NO_COLOUR)
160 return -1
161
Alexandre Ramesb2746622016-07-11 16:12:39 +0100162 # Filter out directories.
163 files = filter(os.path.isfile, files)
164
165 # Filter out files for which we have a cached correct result.
166 if cached_results is not None and len(cached_results) != 0:
167 n_input_files = len(files)
168 files = filter(lambda f: ShouldLint(f, cached_results), files)
169 n_skipped_files = n_input_files - len(files)
170 if n_skipped_files != 0:
171 printer.Print(
172 progress_prefix +
173 'Skipping %d correct files that were already processed.' %
174 n_skipped_files)
175
armvixl4a102ba2014-07-14 09:02:40 +0100176 pool = multiprocessing.Pool(jobs)
177 # The '.get(9999999)' is workaround to allow killing the test script with
178 # ctrl+C from the shell. This bug is documented at
179 # http://bugs.python.org/issue8296.
armvixldb644342015-07-21 11:37:10 +0100180 tasks = [(f, progress_prefix) for f in files]
181 # Run under a try-catch to avoid flooding the output when the script is
182 # interrupted from the keyboard with ctrl+C.
183 try:
184 results = pool.map_async(LintWrapper, tasks).get(9999999)
185 pool.close()
186 pool.join()
187 except KeyboardInterrupt:
188 pool.terminate()
189 sys.exit(1)
Alexandre Ramesb2746622016-07-11 16:12:39 +0100190
191 n_errors = sum(map(lambda (filename, errors): errors, results))
192
193 if cached_results is not None:
194 for filename, errors in results:
195 if errors == 0:
196 with open(filename, 'rb') as f:
197 filename = os.path.realpath(filename)
198 file_hash = hashlib.md5(f.read()).hexdigest()
199 cached_results[filename] = file_hash
200
armvixl4a102ba2014-07-14 09:02:40 +0100201
202 printer.PrintOverwritableLine(
203 progress_prefix + 'Total errors found: %d' % n_errors)
204 printer.EnsureNewLine()
205 return n_errors
206
207
208def IsCppLintAvailable():
209 retcode, unused_output = util.getstatusoutput('which cpplint.py')
210 return retcode == 0
211
212
213CPP_EXT_REGEXP = re.compile('\.(cc|h)$')
armvixl4a102ba2014-07-14 09:02:40 +0100214def is_linter_input(filename):
armvixldb644342015-07-21 11:37:10 +0100215 # lint all C++ files.
armvixl4a102ba2014-07-14 09:02:40 +0100216 return CPP_EXT_REGEXP.search(filename) != None
armvixldb644342015-07-21 11:37:10 +0100217
Alexandre Ramesb2746622016-07-11 16:12:39 +0100218
219def GetDefaultFilesToLint():
armvixldb644342015-07-21 11:37:10 +0100220 if git.is_git_repository_root(config.dir_root):
221 default_tracked_files = git.get_tracked_files().split()
222 default_tracked_files = filter(is_linter_input, default_tracked_files)
223 return 0, default_tracked_files
224 else:
225 printer.Print(printer.COLOUR_ORANGE + 'WARNING: This script is not run ' \
226 'from its Git repository. The linter will not run.' + \
227 printer.NO_COLOUR)
228 return 1, []
armvixl4a102ba2014-07-14 09:02:40 +0100229
Alexandre Ramesb2746622016-07-11 16:12:39 +0100230
231cached_results_pkl_filename = \
232 os.path.join(config.dir_tools, '.cached_lint_results.pkl')
233
234
235def ReadCachedResults():
236 cached_results = {}
237 if os.path.isfile(cached_results_pkl_filename):
238 with open(cached_results_pkl_filename, 'rb') as pkl_file:
239 cached_results = pickle.load(pkl_file)
240 return cached_results
241
242
243def CacheResults(results):
244 with open(cached_results_pkl_filename, 'wb') as pkl_file:
245 pickle.dump(results, pkl_file)
246
247
248def RunLinter(files, jobs=1, progress_prefix='', cached=True):
249 results = {} if not cached else ReadCachedResults()
250
251 rc = LintFiles(files,
252 jobs=jobs,
253 progress_prefix=progress_prefix,
254 cached_results=results)
255
256 CacheResults(results)
257 return rc
258
259
260
armvixl4a102ba2014-07-14 09:02:40 +0100261if __name__ == '__main__':
262 # Parse the arguments.
263 args = BuildOptions()
264
armvixl0f35e362016-05-10 13:57:58 +0100265 files = args.files
266 if not files:
Alexandre Ramesb2746622016-07-11 16:12:39 +0100267 retcode, files = GetDefaultFilesToLint()
armvixl0f35e362016-05-10 13:57:58 +0100268 if retcode:
269 sys.exit(retcode)
Alexandre Ramesb2746622016-07-11 16:12:39 +0100270
271 cached = not args.no_cache
272 retcode = RunLinter(files, jobs=args.jobs, cached=cached)
273
armvixl4a102ba2014-07-14 09:02:40 +0100274 sys.exit(retcode)