1#!/usr/bin/env python3 2# Copyright 2016 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""update_api.py - Update committed Cronet API.""" 7 8 9 10import argparse 11import filecmp 12import fileinput 13import hashlib 14import os 15import re 16import shutil 17import sys 18import tempfile 19 20 21REPOSITORY_ROOT = os.path.abspath( 22 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)) 23 24sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'build/android/gyp')) 25from util import build_utils # pylint: disable=wrong-import-position 26 27# Filename of dump of current API. 28API_FILENAME = os.path.abspath(os.path.join( 29 os.path.dirname(__file__), '..', 'android', 'api.txt')) 30# Filename of file containing API version number. 31API_VERSION_FILENAME = os.path.abspath( 32 os.path.join(os.path.dirname(__file__), '..', 'android', 'api_version.txt')) 33 34# Regular expression that catches the beginning of lines that declare classes. 35# The first group returned by a match is the class name. 36CLASS_RE = re.compile(r'.*(class|interface) ([^ ]*) .*\{') 37 38# Regular expression that matches a string containing an unnamed class name, 39# for example 'Foo$1'. 40UNNAMED_CLASS_RE = re.compile(r'.*\$[0-9]') 41 42# javap still prints internal (package private, nested...) classes even though 43# -protected is passed so they need to be filtered out. 44INTERNAL_CLASS_RE = re.compile( 45 r'^(?!public ((final|abstract) )?(class|interface)).*') 46 47JAR_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'jar') 48JAVAP_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'javap') 49 50 51def generate_api(api_jar, output_filename): 52 # Dumps the API in |api_jar| into |outpuf_filename|. 53 54 with open(output_filename, 'w') as output_file: 55 output_file.write( 56 'DO NOT EDIT THIS FILE, USE update_api.py TO UPDATE IT\n\n') 57 58 # Extract API class files from api_jar. 59 temp_dir = tempfile.mkdtemp() 60 old_cwd = os.getcwd() 61 api_jar_path = os.path.abspath(api_jar) 62 jar_cmd = '%s xf %s' % (os.path.relpath(JAR_PATH, temp_dir), api_jar_path) 63 os.chdir(temp_dir) 64 if os.system(jar_cmd): 65 print('ERROR: jar failed on ' + api_jar) 66 return False 67 os.chdir(old_cwd) 68 shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) 69 70 # Collect names of all API class files 71 api_class_files = [] 72 for root, _, filenames in os.walk(temp_dir): 73 api_class_files += [os.path.join(root, f) for f in filenames] 74 api_class_files.sort() 75 76 # Dump API class files into |output_filename| 77 javap_cmd = ( 78 '%s -protected %s >> %s' % ( 79 JAVAP_PATH, ' '.join(api_class_files), output_filename) 80 ).replace('$', '\\$') 81 if os.system(javap_cmd): 82 print('ERROR: javap command failed: ' + javap_cmd) 83 return False 84 shutil.rmtree(temp_dir) 85 86 # Strip out pieces we don't need to compare. 87 output_file = fileinput.FileInput(output_filename, inplace=True) 88 skip_to_next_class = False 89 md5_hash = hashlib.md5() 90 for line in output_file: 91 # Skip 'Compiled from ' lines as they're not part of the API. 92 if line.startswith('Compiled from "'): 93 continue 94 if CLASS_RE.match(line): 95 skip_to_next_class = ( 96 # Skip internal classes, they aren't exposed. 97 UNNAMED_CLASS_RE.match(line)) or (INTERNAL_CLASS_RE.match(line)) 98 if skip_to_next_class: 99 skip_to_next_class = line != '}' 100 continue 101 md5_hash.update(line.encode('utf8')) 102 sys.stdout.write(line) 103 output_file.close() 104 with open(output_filename, 'a') as output_file: 105 output_file.write('Stamp: %s\n' % md5_hash.hexdigest()) 106 return True 107 108 109def check_up_to_date(api_jar): 110 # Returns True if API_FILENAME matches the API exposed by |api_jar|. 111 112 [_, temp_filename] = tempfile.mkstemp() 113 if not generate_api(api_jar, temp_filename): 114 return False 115 ret = filecmp.cmp(API_FILENAME, temp_filename) 116 os.remove(temp_filename) 117 return ret 118 119 120def check_api_update(old_api, new_api): 121 # Enforce that lines are only added when updating API. 122 new_hash = hashlib.md5() 123 old_hash = hashlib.md5() 124 seen_stamp = False 125 with open(old_api, 'r') as old_api_file, open(new_api, 'r') as new_api_file: 126 for old_line in old_api_file: 127 while True: 128 new_line = new_api_file.readline() 129 if seen_stamp: 130 print('ERROR: Stamp is not the last line.') 131 return False 132 if new_line.startswith('Stamp: ') and old_line.startswith('Stamp: '): 133 if old_line != 'Stamp: %s\n' % old_hash.hexdigest(): 134 print('ERROR: Prior api.txt not stamped by update_api.py') 135 return False 136 if new_line != 'Stamp: %s\n' % new_hash.hexdigest(): 137 print('ERROR: New api.txt not stamped by update_api.py') 138 return False 139 seen_stamp = True 140 break 141 new_hash.update(new_line.encode('utf8')) 142 if new_line == old_line: 143 break 144 if not new_line: 145 if old_line.startswith('Stamp: '): 146 print('ERROR: New api.txt not stamped by update_api.py') 147 else: 148 print('ERROR: This API was modified or removed:') 149 print(' ' + old_line) 150 print(' Cronet API methods and classes cannot be modified.') 151 return False 152 old_hash.update(old_line.encode('utf8')) 153 if not seen_stamp: 154 print('ERROR: api.txt not stamped by update_api.py.') 155 return False 156 return True 157 158 159def main(args): 160 parser = argparse.ArgumentParser(description='Update Cronet api.txt.') 161 parser.add_argument('--api_jar', 162 help='Path to API jar (i.e. cronet_api.jar)', 163 required=True, 164 metavar='path/to/cronet_api.jar') 165 parser.add_argument('--ignore_check_errors', 166 help='If true, ignore errors from verification checks', 167 required=False, 168 default=False, 169 action='store_true') 170 opts = parser.parse_args(args) 171 172 if check_up_to_date(opts.api_jar): 173 return True 174 175 [_, temp_filename] = tempfile.mkstemp() 176 if (generate_api(opts.api_jar, temp_filename) 177 and check_api_update(API_FILENAME, temp_filename)): 178 # Update API version number to new version number 179 with open(API_VERSION_FILENAME, 'r+') as f: 180 version = int(f.read()) 181 f.seek(0) 182 f.write(str(version + 1)) 183 # Update API file to new API 184 shutil.move(temp_filename, API_FILENAME) 185 return True 186 os.remove(temp_filename) 187 return False 188 189 190if __name__ == '__main__': 191 sys.exit(0 if main(sys.argv[1:]) else -1) 192