#!/usr/bin/env python # Copyright (c) 2013 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Helper script for PPAPI's PRESUBMIT.py to detect if additions or removals of PPAPI interfaces have been propagated to the Native Client libraries (.dsc files). For example, if a user adds "ppapi/c/foo.h", we check that the interface has been added to "native_client_sdk/src/libraries/ppapi/library.dsc". """ import optparse import os import sys from build_paths import PPAPI_DIR, SRC_DIR, SDK_LIBRARY_DIR import parse_dsc class VerifyException(Exception): def __init__(self, lib_path, expected, unexpected): self.expected = expected self.unexpected = unexpected msg = 'In %s:\n' % lib_path if expected: msg += ' these files are missing and should be added:\n' for filename in sorted(expected): msg += ' %s\n' % filename if unexpected: msg += ' these files no longer exist and should be removed:\n' for filename in sorted(unexpected): msg += ' %s\n' % filename Exception.__init__(self, msg) def PartitionFiles(filenames): c_filenames = set() cpp_filenames = set() private_filenames = set() for filename in filenames: if os.path.splitext(filename)[1] not in ('.cc', '.h'): continue parts = filename.split(os.sep) if 'private' in filename: if 'flash' in filename: continue private_filenames.add(filename) elif parts[0:2] == ['ppapi', 'c']: if len(parts) >= 2 and parts[2] in ('documentation', 'trusted'): continue c_filenames.add(filename) elif (parts[0:2] == ['ppapi', 'cpp'] or parts[0:2] == ['ppapi', 'utility']): if len(parts) >= 2 and parts[2] in ('documentation', 'trusted'): continue cpp_filenames.add(filename) else: continue return { 'ppapi': c_filenames, 'ppapi_cpp': cpp_filenames, 'ppapi_cpp_private': private_filenames } def GetDirectoryList(directory_path, relative_to): result = [] for root, _, files in os.walk(directory_path): rel_root = os.path.relpath(root, relative_to) if rel_root == '.': rel_root = '' for base_name in files: result.append(os.path.join(rel_root, base_name)) return result def GetDscSourcesAndHeaders(dsc): result = [] for headers_info in dsc.get('HEADERS', []): result.extend(headers_info['FILES']) for targets_info in dsc.get('TARGETS', []): result.extend(targets_info['SOURCES']) return result def GetChangedAndRemovedFilenames(modified_filenames, directory_list): changed = set() removed = set() directory_list_set = set(directory_list) for filename in modified_filenames: if filename in directory_list_set: # We can't know if a file was added (that would require knowing the # previous state of the working directory). Instead, we assume that a # changed file may have been added, and check it accordingly. changed.add(filename) else: removed.add(filename) return changed, removed def GetDscFilenameFromLibraryName(lib_name): return os.path.join(SDK_LIBRARY_DIR, lib_name, 'library.dsc') def Verify(dsc_filename, dsc_sources_and_headers, changed_filenames, removed_filenames): expected_filenames = set() unexpected_filenames = set() for filename in changed_filenames: basename = os.path.basename(filename) if basename not in dsc_sources_and_headers: expected_filenames.add(filename) for filename in removed_filenames: basename = os.path.basename(filename) if basename in dsc_sources_and_headers: unexpected_filenames.add(filename) if expected_filenames or unexpected_filenames: raise VerifyException(dsc_filename, expected_filenames, unexpected_filenames) def VerifyOrPrintError(dsc_filename, dsc_sources_and_headers, changed_filenames, removed_filenames, is_private=False): try: Verify(dsc_filename, dsc_sources_and_headers, changed_filenames, removed_filenames) except VerifyException as e: should_fail = True if is_private and e.expected: # For ppapi_cpp_private, we don't fail if there are expected filenames... # we may not want to include them. We still want to fail if there are # unexpected filenames, though. sys.stderr.write('>>> WARNING: private interface files changed. ' 'Should they be added to the Native Client SDK? <<<\n') if not e.unexpected: should_fail = False sys.stderr.write(str(e) + '\n') if should_fail: return False return True def main(args): usage = '%prog ...' description = __doc__ parser = optparse.OptionParser(usage=usage, description=description) args = parser.parse_args(args)[1] if not args: parser.error('Expected a PPAPI header or source file.') retval = 0 lib_files = PartitionFiles(args) directory_list = GetDirectoryList(PPAPI_DIR, relative_to=SRC_DIR) for lib_name, filenames in lib_files.iteritems(): if not filenames: continue changed_filenames, removed_filenames = \ GetChangedAndRemovedFilenames(filenames, directory_list) dsc_filename = GetDscFilenameFromLibraryName(lib_name) dsc = parse_dsc.LoadProject(dsc_filename) dsc_sources_and_headers = GetDscSourcesAndHeaders(dsc) # Use the relative path to the .dsc to make the error messages shorter. rel_dsc_filename = os.path.relpath(dsc_filename, SRC_DIR) is_private = lib_name == 'ppapi_cpp_private' if not VerifyOrPrintError(rel_dsc_filename, dsc_sources_and_headers, changed_filenames, removed_filenames, is_private=is_private): retval = 1 return retval if __name__ == '__main__': sys.exit(main(sys.argv[1:]))