xref: /aosp_15_r20/external/cronet/components/cronet/tools/update_api.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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