1 #!/usr/bin/env python 2 # 3 # Copyright (C) 2018 The Android Open Source Project 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 # 17 18 """ 19 Dump new HIDL types that are introduced in each dessert release. 20 """ 21 22 from __future__ import print_function 23 24 import argparse 25 import collections 26 import json 27 import os 28 import re 29 30 class Globals: 31 pass 32 33 class Constants: 34 CURRENT = 'current' 35 HAL_PATH_PATTERN = r'/((?:[a-zA-Z_][a-zA-Z0-9_]*/)*)(\d+\.\d+)/([a-zA-Z_][a-zA-Z0-9_]*).hal' 36 CURRENT_TXT_PATTERN = r'(?:.*/)?([0-9]+|current).txt' 37 38 def trim_trailing_comments(line): 39 idx = line.find('#') 40 if idx < 0: return line 41 return line[0:idx] 42 43 def strip_begin(s, prefix): 44 if s.startswith(prefix): 45 return strip_begin(s[len(prefix):], prefix) 46 return s 47 48 def strip_end(s, suffix): 49 if s.endswith(suffix): 50 return strip_end(s[0:-len(suffix)], suffix) 51 return s 52 53 def get_interfaces(file_name): 54 with open(file_name) as file: 55 for line in file: 56 line_tokens = trim_trailing_comments(line).strip().split() 57 if not line_tokens: 58 continue 59 assert len(line_tokens) == 2, \ 60 "Unrecognized line in file {}:\n{}".format(file_name, line) 61 yield line_tokens[1] 62 63 def api_level_to_int(api_level): 64 try: 65 if api_level == Constants.CURRENT: return float('inf') 66 return int(api_level) 67 except ValueError: 68 return None 69 70 def get_interfaces_from_package_root(package, root): 71 root = strip_end(root, '/') 72 for dirpath, _, filenames in os.walk(root, topdown=False): 73 dirpath = strip_begin(dirpath, root) 74 for filename in filenames: 75 filepath = os.path.join(dirpath, filename) 76 mo = re.match(Constants.HAL_PATH_PATTERN, filepath) 77 if not mo: 78 continue 79 yield '{}.{}@{}::{}'.format( 80 package, mo.group(1).strip('/').replace('/', '.'), mo.group(2), mo.group(3)) 81 82 def filter_out(iterable): 83 return iterable if not Globals.filter_out else filter( 84 lambda s: all(re.search(pattern, s) is None for pattern in Globals.filter_out), 85 iterable) 86 87 def main(): 88 parser = argparse.ArgumentParser(description=__doc__) 89 parser.add_argument('--pretty', help='Print pretty JSON', action='store_true') 90 parser.add_argument('--package-root', metavar='PACKAGE:PATH', nargs='*', 91 help='package root of current directory, e.g. android.hardware:hardware/interfaces') 92 parser.add_argument('--filter-out', metavar='REGEX', nargs='*', 93 help='A regular expression that filters out interfaces.') 94 parser.add_argument('hashes', metavar='FILE', nargs='*', 95 help='Locations of current.txt for each release.') 96 parser.parse_args(namespace=Globals) 97 98 interfaces_for_level = dict() 99 100 for filename in Globals.hashes or tuple(): 101 mo = re.match(Constants.CURRENT_TXT_PATTERN, filename) 102 assert mo is not None, \ 103 'Input hash file names must have the format {} but is {}'.format(Constants.CURRENT_TXT_PATTERN, filename) 104 105 api_level = mo.group(1) 106 assert api_level_to_int(api_level) is not None 107 108 if api_level not in interfaces_for_level: 109 interfaces_for_level[api_level] = set() 110 interfaces_for_level[api_level].update(filter_out(get_interfaces(filename))) 111 112 for package_root in Globals.package_root or tuple(): 113 tup = package_root.split(':') 114 assert len(tup) == 2, \ 115 '--package-root must have the format PACKAGE:PATH, but is {}'.format(package_root) 116 if Constants.CURRENT not in interfaces_for_level: 117 interfaces_for_level[Constants.CURRENT] = set() 118 interfaces_for_level[Constants.CURRENT].update(filter_out(get_interfaces_from_package_root(*tup))) 119 120 seen_interfaces = set() 121 new_interfaces_for_level = collections.OrderedDict() 122 for level, interfaces in sorted(interfaces_for_level.items(), key=lambda tup: api_level_to_int(tup[0])): 123 if level != Constants.CURRENT: 124 removed_interfaces = seen_interfaces - interfaces 125 assert not removed_interfaces, \ 126 "The following interfaces are removed from API level {}:\n{}".format( 127 level, removed_interfaces) 128 new_interfaces_for_level[level] = sorted(interfaces - seen_interfaces) 129 seen_interfaces.update(interfaces) 130 131 print(json.dumps(new_interfaces_for_level, 132 separators=None if Globals.pretty else (',',':'), 133 indent=4 if Globals.pretty else None)) 134 135 if __name__ == '__main__': 136 main() 137