| #!/usr/bin/python |
| # Copyright 2016 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. |
| |
| """update_api.py - Update committed Cronet API.""" |
| |
| import argparse |
| import filecmp |
| import fileinput |
| import md5 |
| import os |
| import re |
| import shutil |
| import sys |
| import tempfile |
| |
| # 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 ([^ ]*) .*\{') |
| |
| # Regular expression that matches a string containing an unnamed class name, |
| # for example 'Foo$1'. |
| UNNAMED_CLASS_RE = re.compile(r'.*\$[0-9]') |
| |
| |
| def generate_api(api_jar, output_filename): |
| # Dumps the API in |api_jar| into |outpuf_filename|. |
| |
| with open(output_filename, 'w') as output_file: |
| output_file.write( |
| 'DO NOT EDIT THIS FILE, USE update_api.py TO UPDATE IT\n\n') |
| |
| # Extract API class files from api_jar. |
| temp_dir = tempfile.mkdtemp() |
| old_cwd = os.getcwd() |
| api_jar_path = os.path.abspath(api_jar) |
| os.chdir(temp_dir) |
| if os.system('jar xf %s' % api_jar_path): |
| print 'ERROR: jar failed on ' + api_jar |
| return False |
| os.chdir(old_cwd) |
| shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) |
| |
| # Collect names 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() |
| |
| # Dump API class files into |output_filename| |
| javap_cmd = ('javap -protected %s >> %s' % (' '.join(api_class_files), |
| output_filename)).replace('$', '\\$') |
| if os.system(javap_cmd): |
| print 'ERROR: javap command failed: ' + javap_cmd |
| return False |
| shutil.rmtree(temp_dir) |
| |
| # Strip out pieces we don't need to compare. |
| output_file = fileinput.FileInput(output_filename, inplace=True) |
| skip_to_next_class = False |
| md5_hash = md5.new() |
| for line in output_file: |
| # Skip 'Compiled from ' lines as they're not part of the API. |
| if line.startswith('Compiled from "'): |
| continue |
| if CLASS_RE.match(line): |
| skip_to_next_class = ( |
| # Skip internal classes, they aren't exposed. |
| UNNAMED_CLASS_RE.match(line) or |
| # Skip experimental classes, they can be modified. |
| 'Experimental' in line |
| ) |
| if skip_to_next_class: |
| skip_to_next_class = line != '}' |
| continue |
| md5_hash.update(line) |
| sys.stdout.write(line) |
| output_file.close() |
| with open(output_filename, 'a') as output_file: |
| output_file.write('Stamp: %s\n' % md5_hash.hexdigest()) |
| return True |
| |
| |
| def check_up_to_date(api_jar): |
| # Returns True if API_FILENAME matches the API exposed by |api_jar|. |
| |
| [_, temp_filename] = tempfile.mkstemp() |
| if not generate_api(api_jar, temp_filename): |
| return False |
| ret = filecmp.cmp(API_FILENAME, temp_filename) |
| os.remove(temp_filename) |
| return ret |
| |
| |
| def check_api_update(old_api, new_api): |
| # Enforce that lines are only added when updating API. |
| new_hash = md5.new() |
| old_hash = md5.new() |
| 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) |
| 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) |
| 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') |
| opts = parser.parse_args(args) |
| |
| if check_up_to_date(opts.api_jar): |
| return True |
| |
| [_, temp_filename] = tempfile.mkstemp() |
| if (generate_api(opts.api_jar, temp_filename) and |
| check_api_update(API_FILENAME, temp_filename)): |
| # 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.move(temp_filename, API_FILENAME) |
| return True |
| os.remove(temp_filename) |
| return False |
| |
| |
| if __name__ == '__main__': |
| sys.exit(0 if main(sys.argv[1:]) else -1) |