1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
|
#!/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)
|