1#!/usr/bin/env python2.7
2
3# Copyright 2016, 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 multiprocessing
32import os
33import re
34import signal
35import subprocess
36import sys
37import tempfile
38
39import config
40import git
41import printer
42import util
43
44CLANG_FORMAT_VERSION_MAJOR = 3
45CLANG_FORMAT_VERSION_MINOR = 8
46
47DEFAULT_CLANG_FORMAT = \
48    'clang-format-{}.{}'.format(CLANG_FORMAT_VERSION_MAJOR,
49                                CLANG_FORMAT_VERSION_MINOR)
50
51is_output_redirected = not sys.stdout.isatty()
52
53# Catch SIGINT to gracefully exit when ctrl+C is pressed.
54def sigint_handler(signal, frame):
55  sys.exit(1)
56signal.signal(signal.SIGINT, sigint_handler)
57
58def BuildOptions():
59  parser = argparse.ArgumentParser(
60    description = '''This tool runs `clang-format` on C++ files.
61    If no files are provided on the command-line, all C++ source files in `src`,
62    `sample`, and `benchmarks` are processed.
63    When available, `colordiff` is automatically used to clour the output.''',
64    # Print default values.
65    formatter_class = argparse.ArgumentDefaultsHelpFormatter)
66  parser.add_argument('files', nargs = '*')
67  parser.add_argument('--clang-format', default=DEFAULT_CLANG_FORMAT,
68                      help='Path to clang-format.')
69  parser.add_argument('--in-place', '-i',
70                      action = 'store_true', default = False,
71                      help = 'Edit files in place.')
72  parser.add_argument('--jobs', '-j', metavar = 'N', type = int, nargs = '?',
73                      default = multiprocessing.cpu_count(),
74                      const = multiprocessing.cpu_count(),
75                      help = '''Runs the tests using N jobs. If the option is set
76                      but no value is provided, the script will use as many jobs
77                      as it thinks useful.''')
78  return parser.parse_args()
79
80
81def ClangFormatIsAvailable(clang_format):
82  if not util.IsCommandAvailable(clang_format):
83    return False
84  version = subprocess.check_output([clang_format, '-version'])
85  m = re.search("^clang-format version (\d)\.(\d)\.\d.*$",
86                version.decode(), re.M)
87  major, minor = m.groups()
88  return int(major) == CLANG_FORMAT_VERSION_MAJOR and \
89      int(minor) == CLANG_FORMAT_VERSION_MINOR
90
91
92# Returns 0 if the file is correctly formatted, or 1 otherwise.
93def ClangFormat(filename, clang_format, in_place = False, progress_prefix = ''):
94  rc = 0
95  printer.PrintOverwritableLine('Processing %s' % filename,
96                                type = printer.LINE_TYPE_LINTER)
97
98  cmd_format = [clang_format, filename]
99  temp_file, temp_file_name = tempfile.mkstemp(prefix = 'clang_format_')
100  cmd_format_string = '$ ' + ' '.join(cmd_format) + ' > %s' % temp_file_name
101  p_format = subprocess.Popen(cmd_format,
102                              stdout = temp_file, stderr = subprocess.STDOUT)
103
104  rc += p_format.wait()
105
106  cmd_diff = ['diff', '--unified', filename, temp_file_name]
107  cmd_diff_string = '$ ' + ' '.join(cmd_diff)
108  p_diff = subprocess.Popen(cmd_diff,
109                            stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
110
111  if util.IsCommandAvailable('colordiff') and not is_output_redirected:
112    p_colordiff = subprocess.Popen(
113            ['colordiff', '--unified'],
114            stdin = p_diff.stdout,
115            stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
116    out, unused = p_colordiff.communicate()
117  else:
118    out, unused = p_diff.communicate()
119
120  rc += p_diff.wait()
121
122  if in_place:
123      cmd_format = [clang_format, '-i', filename]
124      p_format = subprocess.Popen(cmd_format,
125                                  stdout=temp_file, stderr=subprocess.STDOUT)
126
127  if rc != 0:
128    printer.Print('Incorrectly formatted file: ' + filename + '\n' + \
129                  cmd_format_string + '\n' + \
130                  cmd_diff_string + '\n' + \
131                  out)
132
133  os.remove(temp_file_name)
134
135  return 0 if rc == 0 else 1
136
137
138# The multiprocessing map_async function does not allow passing multiple
139# arguments directly, so use a wrapper.
140def ClangFormatWrapper(args):
141  # Run under a try-catch  to avoid flooding the output when the script is
142  # interrupted from the keyboard with ctrl+C.
143  try:
144    return ClangFormat(*args)
145  except:
146    sys.exit(1)
147
148
149# Returns the total number of files incorrectly formatted.
150def ClangFormatFiles(files, clang_format, in_place = False, jobs = 1,
151                     progress_prefix = ''):
152  if not ClangFormatIsAvailable(clang_format):
153    error_message = "`{}` version {}.{} not found. Please ensure it " \
154                    "is installed, in your PATH and the correct version." \
155                    .format(clang_format,
156                            CLANG_FORMAT_VERSION_MAJOR,
157                            CLANG_FORMAT_VERSION_MINOR)
158    print(printer.COLOUR_RED + error_message + printer.NO_COLOUR)
159    return -1
160
161  pool = multiprocessing.Pool(jobs)
162  # The '.get(9999999)' is workaround to allow killing the test script with
163  # ctrl+C from the shell. This bug is documented at
164  # http://bugs.python.org/issue8296.
165  tasks = [(f, clang_format, in_place, progress_prefix) for f in files]
166  # Run under a try-catch  to avoid flooding the output when the script is
167  # interrupted from the keyboard with ctrl+C.
168  try:
169    results = pool.map_async(ClangFormatWrapper, tasks).get(9999999)
170    pool.close()
171    pool.join()
172  except KeyboardInterrupt:
173    pool.terminate()
174    sys.exit(1)
175  rc = sum(results)
176
177  printer.PrintOverwritableLine(
178      progress_prefix + '%d files are incorrectly formatted.' % rc,
179      type = printer.LINE_TYPE_LINTER)
180  printer.EnsureNewLine()
181  return rc
182
183
184def Find(path, filters = ['*'], excluded_dir = ""):
185  files_found = []
186
187  def NameMatchesAnyFilter(name, ff):
188    for f in ff:
189      if fnmatch.fnmatch(name, f):
190        return True
191    return False
192
193  for root, dirs, files in os.walk(path):
194    files_found += [
195        os.path.join(root, fn)
196        for fn in files
197        # Include files which names match "filters".
198        # Exclude files for which the base directory is "excluded_dir".
199        if NameMatchesAnyFilter(os.path.relpath(fn), filters) and \
200            not os.path.dirname(os.path.join(root, fn)).endswith(excluded_dir)
201    ]
202  return files_found
203
204
205def GetCppSourceFilesToFormat():
206  sources = []
207  source_dirs = [config.dir_aarch32_benchmarks,
208                 config.dir_aarch32_examples,
209                 config.dir_aarch64_benchmarks,
210                 config.dir_aarch64_examples,
211                 config.dir_tests,
212                 config.dir_src_vixl ]
213  for directory in source_dirs:
214    sources += Find(directory, ['*.h', '*.cc'], 'traces')
215  return sources
216
217
218if __name__ == '__main__':
219  # Parse the arguments.
220  args = BuildOptions()
221  files = args.files
222  if not files:
223    files = GetCppSourceFilesToFormat()
224
225  rc = ClangFormatFiles(files, clang_format = args.clang_format,
226                        in_place = args.in_place, jobs = args.jobs)
227
228  sys.exit(rc)
229