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