liyujie
2025-08-28 867b8b7b729282c7e14e200ca277435329ebe747
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
#!/usr/bin/env python
#
# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
 
"""
Dump new HIDL types that are introduced in each dessert release.
"""
 
from __future__ import print_function
 
import argparse
import collections
import json
import os
import re
 
class Globals:
    pass
 
class Constants:
    CURRENT = 'current'
    HAL_PATH_PATTERN = r'/((?:[a-zA-Z_][a-zA-Z0-9_]*/)*)(\d+\.\d+)/([a-zA-Z_][a-zA-Z0-9_]*).hal'
    CURRENT_TXT_PATTERN = r'(?:.*/)?([0-9]+|current).txt'
 
def trim_trailing_comments(line):
    idx = line.find('#')
    if idx < 0: return line
    return line[0:idx]
 
def strip_begin(s, prefix):
    if s.startswith(prefix):
        return strip_begin(s[len(prefix):], prefix)
    return s
 
def strip_end(s, suffix):
    if s.endswith(suffix):
        return strip_end(s[0:-len(suffix)], suffix)
    return s
 
def get_interfaces(file_name):
    with open(file_name) as file:
        for line in file:
            line_tokens = trim_trailing_comments(line).strip().split()
            if not line_tokens:
                continue
            assert len(line_tokens) == 2, \
                "Unrecognized line in file {}:\n{}".format(file_name, line)
            yield line_tokens[1]
 
def api_level_to_int(api_level):
    try:
        if api_level == Constants.CURRENT: return float('inf')
        return int(api_level)
    except ValueError:
        return None
 
def get_interfaces_from_package_root(package, root):
    root = strip_end(root, '/')
    for dirpath, _, filenames in os.walk(root, topdown=False):
        dirpath = strip_begin(dirpath, root)
        for filename in filenames:
            filepath = os.path.join(dirpath, filename)
            mo = re.match(Constants.HAL_PATH_PATTERN, filepath)
            if not mo:
                continue
            yield '{}.{}@{}::{}'.format(
                package, mo.group(1).strip('/').replace('/', '.'), mo.group(2), mo.group(3))
 
def filter_out(iterable):
    return iterable if not Globals.filter_out else filter(
        lambda s: all(re.search(pattern, s) is None for pattern in Globals.filter_out),
        iterable)
 
def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--pretty', help='Print pretty JSON', action='store_true')
    parser.add_argument('--package-root', metavar='PACKAGE:PATH', nargs='*',
        help='package root of current directory, e.g. android.hardware:hardware/interfaces')
    parser.add_argument('--filter-out', metavar='REGEX', nargs='*',
        help='A regular expression that filters out interfaces.')
    parser.add_argument('hashes', metavar='FILE', nargs='*',
        help='Locations of current.txt for each release.')
    parser.parse_args(namespace=Globals)
 
    interfaces_for_level = dict()
 
    for filename in Globals.hashes or tuple():
        mo = re.match(Constants.CURRENT_TXT_PATTERN, filename)
        assert mo is not None, \
            'Input hash file names must have the format {} but is {}'.format(Constants.CURRENT_TXT_PATTERN, filename)
 
        api_level = mo.group(1)
        assert api_level_to_int(api_level) is not None
 
        if api_level not in interfaces_for_level:
            interfaces_for_level[api_level] = set()
        interfaces_for_level[api_level].update(filter_out(get_interfaces(filename)))
 
    for package_root in Globals.package_root or tuple():
        tup = package_root.split(':')
        assert len(tup) == 2, \
            '--package-root must have the format PACKAGE:PATH, but is {}'.format(package_root)
        if Constants.CURRENT not in interfaces_for_level:
            interfaces_for_level[Constants.CURRENT] = set()
        interfaces_for_level[Constants.CURRENT].update(filter_out(get_interfaces_from_package_root(*tup)))
 
    seen_interfaces = set()
    new_interfaces_for_level = collections.OrderedDict()
    for level, interfaces in sorted(interfaces_for_level.items(), key=lambda tup: api_level_to_int(tup[0])):
        if level != Constants.CURRENT:
            removed_interfaces = seen_interfaces - interfaces
            assert not removed_interfaces, \
                "The following interfaces are removed from API level {}:\n{}".format(
                    level, removed_interfaces)
        new_interfaces_for_level[level] = sorted(interfaces - seen_interfaces)
        seen_interfaces.update(interfaces)
 
    print(json.dumps(new_interfaces_for_level,
        separators=None if Globals.pretty else (',',':'),
        indent=4 if Globals.pretty else None))
 
if __name__ == '__main__':
    main()