| #!/usr/bin/env python3 |
| # Copyright 2016 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """update_api.py - Update committed Cronet API.""" |
| |
| import argparse |
| import filecmp |
| import hashlib |
| import os |
| import re |
| import shutil |
| import sys |
| import tempfile |
| |
| |
| REPOSITORY_ROOT = os.path.abspath( |
| os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)) |
| |
| sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'build/android/gyp')) |
| from util import build_utils # pylint: disable=wrong-import-position |
| |
| # Filename of dump of current API. |
| API_FILENAME = os.path.abspath(os.path.join( |
| os.path.dirname(__file__), '..', 'android', 'api.txt')) |
| # Filename of file containing API version number. |
| API_VERSION_FILENAME = os.path.abspath( |
| os.path.join(os.path.dirname(__file__), '..', 'android', 'api_version.txt')) |
| |
| # Regular expression that catches the beginning of lines that declare classes. |
| # The first group returned by a match is the class name. |
| CLASS_RE = re.compile(r'.*(class|interface) ([^ ]*) .*\{') |
| |
| # Regular expression that matches a string containing an unnamed class name, |
| # for example 'Foo$1'. |
| UNNAMED_CLASS_RE = re.compile(r'.*\$[0-9]') |
| |
| # javap still prints internal (package private, nested...) classes even though |
| # -protected is passed so they need to be filtered out. |
| INTERNAL_CLASS_RE = re.compile( |
| r'^(?!public ((final|abstract) )?(class|interface)).*') |
| |
| JAR_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'jar') |
| JAVAP_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'javap') |
| |
| |
| def _split_by_class(javap_output): |
| """Splits the combined javap output to separate classes. |
| |
| * Removes unneeded comments like "Compiled from ...". |
| * Sorts the declarations inside the class. |
| |
| Returns an array where each element represents a class. |
| """ |
| current_class_lines = [] |
| all_classes = [] |
| for line in javap_output: |
| # Lines starting with Compiled from are just comments and not part of the |
| # api. |
| if line.startswith('Compiled from'): |
| continue |
| current_class_lines.append(line) |
| if line == '}': |
| # sort only the lines between the {}. |
| current_class_lines = ([current_class_lines[0]] + |
| sorted(current_class_lines[1:-1]) + |
| [current_class_lines[-1]]) |
| all_classes.append(current_class_lines) |
| current_class_lines = [] |
| return all_classes |
| |
| |
| def _generate_api(api_jar, output_filename): |
| """Dumps the API in |api_jar| into |outpuf_filename|.""" |
| # Extract API class files from api_jar. |
| with tempfile.TemporaryDirectory() as temp_dir: |
| api_jar_path = os.path.abspath(api_jar) |
| jar_cmd = [os.path.relpath(JAR_PATH, temp_dir), 'xf', api_jar_path] |
| build_utils.CheckOutput(jar_cmd, cwd=temp_dir) |
| |
| shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) |
| |
| # Collect paths of all API class files |
| api_class_files = [] |
| for root, _, filenames in os.walk(temp_dir): |
| api_class_files += [os.path.join(root, f) for f in filenames] |
| api_class_files.sort() |
| |
| output_lines = ['DO NOT EDIT THIS FILE, USE update_api.py TO UPDATE IT\n'] |
| javap_cmd = [JAVAP_PATH, '-protected'] + api_class_files |
| javap_output = build_utils.CheckOutput(javap_cmd) |
| |
| all_classes = _split_by_class(javap_output.splitlines()) |
| for class_lines in all_classes: |
| first_line = class_lines[0] |
| # Skip classes we do not care about. |
| if UNNAMED_CLASS_RE.match(first_line) or INTERNAL_CLASS_RE.match( |
| first_line): |
| continue |
| output_lines.extend(class_lines) |
| |
| output_string = '\n'.join(output_lines) + '\n' |
| md5_hash = hashlib.md5() |
| md5_hash.update(output_string.encode('utf-8')) |
| output_string += 'Stamp: %s\n' % md5_hash.hexdigest() |
| |
| with open(output_filename, 'w') as output_file: |
| output_file.write(output_string) |
| |
| |
| def check_up_to_date(api_jar): |
| """Returns True if API_FILENAME matches the API exposed by |api_jar|.""" |
| with tempfile.NamedTemporaryFile() as temp: |
| _generate_api(api_jar, temp.name) |
| return filecmp.cmp(API_FILENAME, temp.name) |
| |
| |
| def _check_api_update(old_api, new_api): |
| """Enforce that lines are only added when updating API.""" |
| new_hash = hashlib.md5() |
| old_hash = hashlib.md5() |
| seen_stamp = False |
| with open(old_api, 'r') as old_api_file, open(new_api, 'r') as new_api_file: |
| for old_line in old_api_file: |
| while True: |
| new_line = new_api_file.readline() |
| if seen_stamp: |
| print('ERROR: Stamp is not the last line.') |
| return False |
| if new_line.startswith('Stamp: ') and old_line.startswith('Stamp: '): |
| if old_line != 'Stamp: %s\n' % old_hash.hexdigest(): |
| print('ERROR: Prior api.txt not stamped by update_api.py') |
| return False |
| if new_line != 'Stamp: %s\n' % new_hash.hexdigest(): |
| print('ERROR: New api.txt not stamped by update_api.py') |
| return False |
| seen_stamp = True |
| break |
| new_hash.update(new_line.encode('utf8')) |
| if new_line == old_line: |
| break |
| if not new_line: |
| if old_line.startswith('Stamp: '): |
| print('ERROR: New api.txt not stamped by update_api.py') |
| else: |
| print('ERROR: This API was modified or removed:') |
| print(' ' + old_line) |
| print(' Cronet API methods and classes cannot be modified.') |
| return False |
| old_hash.update(old_line.encode('utf8')) |
| if not seen_stamp: |
| print('ERROR: api.txt not stamped by update_api.py.') |
| return False |
| return True |
| |
| |
| def main(args): |
| parser = argparse.ArgumentParser(description='Update Cronet api.txt.') |
| parser.add_argument('--api_jar', |
| help='Path to API jar (i.e. cronet_api.jar)', |
| required=True, |
| metavar='path/to/cronet_api.jar') |
| parser.add_argument('--ignore_check_errors', |
| help='If true, ignore errors from verification checks', |
| required=False, |
| default=False, |
| action='store_true') |
| opts = parser.parse_args(args) |
| |
| if check_up_to_date(opts.api_jar): |
| return True |
| |
| with tempfile.NamedTemporaryFile() as temp: |
| _generate_api(opts.api_jar, temp.name) |
| if _check_api_update(API_FILENAME, temp.name): |
| # Update API version number to new version number |
| with open(API_VERSION_FILENAME, 'r+') as f: |
| version = int(f.read()) |
| f.seek(0) |
| f.write(str(version + 1)) |
| # Update API file to new API |
| shutil.copyfile(temp.name, API_FILENAME) |
| return True |
| return False |
| |
| |
| if __name__ == '__main__': |
| sys.exit(0 if main(sys.argv[1:]) else -1) |