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