File: update_api.py

package info (click to toggle)
chromium 139.0.7258.127-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,122,156 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (189 lines) | stat: -rwxr-xr-x 6,761 bytes parent folder | download | duplicates (6)
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)