blob: 2265ec69ad330ac446732a294554aecf59376581 [file] [log] [blame]
#!/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)