Home | History | Annotate | Download | only in build_tools
      1 #!/usr/bin/env python
      2 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """Helper script for PPAPI's PRESUBMIT.py to detect if additions or removals of
      7 PPAPI interfaces have been propagated to the Native Client libraries (.dsc
      8 files).
      9 
     10 For example, if a user adds "ppapi/c/foo.h", we check that the interface has
     11 been added to "native_client_sdk/src/libraries/ppapi/library.dsc".
     12 """
     13 
     14 import optparse
     15 import os
     16 import sys
     17 
     18 from build_paths import PPAPI_DIR, SRC_DIR, SDK_LIBRARY_DIR
     19 import parse_dsc
     20 
     21 
     22 class VerifyException(Exception):
     23   def __init__(self, lib_path, expected, unexpected):
     24     self.expected = expected
     25     self.unexpected = unexpected
     26 
     27     msg = 'In %s:\n' % lib_path
     28     if expected:
     29       msg += '  these files are missing and should be added:\n'
     30       for filename in sorted(expected):
     31         msg += '    %s\n' % filename
     32     if unexpected:
     33       msg += '  these files no longer exist and should be removed:\n'
     34       for filename in sorted(unexpected):
     35         msg += '    %s\n' % filename
     36 
     37     Exception.__init__(self, msg)
     38 
     39 
     40 def PartitionFiles(filenames):
     41   c_filenames = set()
     42   cpp_filenames = set()
     43   private_filenames = set()
     44 
     45   for filename in filenames:
     46     if os.path.splitext(filename)[1] not in ('.cc', '.h'):
     47       continue
     48 
     49     parts = filename.split(os.sep)
     50     if 'private' in filename:
     51       if 'flash' in filename:
     52         continue
     53       private_filenames.add(filename)
     54     elif parts[0:2] == ['ppapi', 'c']:
     55       if len(parts) >= 2 and parts[2] in ('documentation', 'trusted'):
     56         continue
     57       c_filenames.add(filename)
     58     elif (parts[0:2] == ['ppapi', 'cpp'] or
     59           parts[0:2] == ['ppapi', 'utility']):
     60       if len(parts) >= 2 and parts[2] in ('documentation', 'trusted'):
     61         continue
     62       cpp_filenames.add(filename)
     63     else:
     64       continue
     65 
     66   return {
     67       'ppapi': c_filenames,
     68       'ppapi_cpp': cpp_filenames,
     69       'ppapi_cpp_private': private_filenames
     70   }
     71 
     72 
     73 def GetDirectoryList(directory_path, relative_to):
     74   result = []
     75   for root, _, files in os.walk(directory_path):
     76     rel_root = os.path.relpath(root, relative_to)
     77     if rel_root == '.':
     78       rel_root = ''
     79     for base_name in files:
     80       result.append(os.path.join(rel_root, base_name))
     81   return result
     82 
     83 
     84 def GetDscSourcesAndHeaders(dsc):
     85   result = []
     86   for headers_info in dsc.get('HEADERS', []):
     87     result.extend(headers_info['FILES'])
     88   for targets_info in dsc.get('TARGETS', []):
     89     result.extend(targets_info['SOURCES'])
     90   return result
     91 
     92 
     93 def GetChangedAndRemovedFilenames(modified_filenames, directory_list):
     94   changed = set()
     95   removed = set()
     96   directory_list_set = set(directory_list)
     97   for filename in modified_filenames:
     98     if filename in directory_list_set:
     99       # We can't know if a file was added (that would require knowing the
    100       # previous state of the working directory). Instead, we assume that a
    101       # changed file may have been added, and check it accordingly.
    102       changed.add(filename)
    103     else:
    104       removed.add(filename)
    105   return changed, removed
    106 
    107 
    108 def GetDscFilenameFromLibraryName(lib_name):
    109   return os.path.join(SDK_LIBRARY_DIR, lib_name, 'library.dsc')
    110 
    111 
    112 def Verify(dsc_filename, dsc_sources_and_headers, changed_filenames,
    113            removed_filenames):
    114   expected_filenames = set()
    115   unexpected_filenames = set()
    116 
    117   for filename in changed_filenames:
    118     basename = os.path.basename(filename)
    119     if basename not in dsc_sources_and_headers:
    120       expected_filenames.add(filename)
    121 
    122   for filename in removed_filenames:
    123     basename = os.path.basename(filename)
    124     if basename in dsc_sources_and_headers:
    125       unexpected_filenames.add(filename)
    126 
    127   if expected_filenames or unexpected_filenames:
    128     raise VerifyException(dsc_filename, expected_filenames,
    129                           unexpected_filenames)
    130 
    131 
    132 def VerifyOrPrintError(dsc_filename, dsc_sources_and_headers, changed_filenames,
    133                        removed_filenames, is_private=False):
    134   try:
    135     Verify(dsc_filename, dsc_sources_and_headers, changed_filenames,
    136            removed_filenames)
    137   except VerifyException as e:
    138     should_fail = True
    139     if is_private and e.expected:
    140       # For ppapi_cpp_private, we don't fail if there are expected filenames...
    141       # we may not want to include them. We still want to fail if there are
    142       # unexpected filenames, though.
    143       sys.stderr.write('>>> WARNING: private interface files changed. '
    144           'Should they be added to the Native Client SDK? <<<\n')
    145       if not e.unexpected:
    146         should_fail = False
    147     sys.stderr.write(str(e) + '\n')
    148     if should_fail:
    149       return False
    150   return True
    151 
    152 
    153 def main(args):
    154   usage = '%prog <file>...'
    155   description = __doc__
    156   parser = optparse.OptionParser(usage=usage, description=description)
    157   args = parser.parse_args(args)[1]
    158   if not args:
    159     parser.error('Expected a PPAPI header or source file.')
    160 
    161   retval = 0
    162   lib_files = PartitionFiles(args)
    163   directory_list = GetDirectoryList(PPAPI_DIR, relative_to=SRC_DIR)
    164   for lib_name, filenames in lib_files.iteritems():
    165     if not filenames:
    166       continue
    167 
    168     changed_filenames, removed_filenames = \
    169         GetChangedAndRemovedFilenames(filenames, directory_list)
    170 
    171     dsc_filename = GetDscFilenameFromLibraryName(lib_name)
    172     dsc = parse_dsc.LoadProject(dsc_filename)
    173     dsc_sources_and_headers = GetDscSourcesAndHeaders(dsc)
    174 
    175     # Use the relative path to the .dsc to make the error messages shorter.
    176     rel_dsc_filename = os.path.relpath(dsc_filename, SRC_DIR)
    177     is_private = lib_name == 'ppapi_cpp_private'
    178     if not VerifyOrPrintError(rel_dsc_filename, dsc_sources_and_headers,
    179                               changed_filenames, removed_filenames,
    180                               is_private=is_private):
    181       retval = 1
    182   return retval
    183 
    184 
    185 if __name__ == '__main__':
    186   sys.exit(main(sys.argv[1:]))
    187