1#!/usr/bin/env python3
2
3# This tool updates extracts the information from Android.bp and updates the
4# datasets for eligible VNDK libraries.
5
6import argparse
7import collections
8import csv
9import json
10import os.path
11import posixpath
12import re
13import sys
14
15def load_make_vars(path):
16    result = collections.OrderedDict([
17        ('SOONG_LLNDK_LIBRARIES', set()),
18        ('SOONG_VNDK_SAMEPROCESS_LIBRARIES', set()),
19        ('SOONG_VNDK_CORE_LIBRARIES', set()),
20        ('SOONG_VNDK_PRIVATE_LIBRARIES', set()),
21    ])
22
23    assign_len = len(' := ')
24
25    with open(path, 'r') as fp:
26        for line in fp:
27            for key, value in result.items():
28                if line.startswith(key):
29                    mod_names = line[len(key) + assign_len:].strip().split(' ')
30                    value.update(mod_names)
31
32    return result.values()
33
34def load_install_paths(module_info_path):
35    with open(module_info_path, 'r') as fp:
36        data = json.load(fp)
37
38    result = set()
39    name_path_dict = {}
40    patt = re.compile(
41            '.*[\\\\/]target[\\\\/]product[\\\\/][^\\\\/]+([\\\\/].*)$')
42    for name, module in data.items():
43        for path in module['installed']:
44            match = patt.match(path)
45            if not match:
46                continue
47            path = match.group(1)
48            path = path.replace(os.path.sep, '/')
49            path = path.replace('/lib/', '/${LIB}/')
50            path = path.replace('/lib64/', '/${LIB}/')
51            path = re.sub('/vndk-sp(?:-[^/$]*)/', '/vndk-sp${VNDK_VER}/', path)
52            path = re.sub('/vndk(?:-[^/$]*)/', '/vndk${VNDK_VER}/', path)
53            result.add(path)
54
55            if name.endswith('_32'):
56                name = name[0:-3]
57
58            name_path_dict[name] = path
59
60    return (result, name_path_dict)
61
62def _is_stale_module(path, installed_paths):
63    if path in installed_paths:
64        return False
65    # libclang_rt.asan-${arch}-android and
66    # libclang_rt.ubsan_standalone-${arch}-android may vary between different
67    # architectures.
68    if posixpath.basename(path).startswith('libclang_rt'):
69        return False
70    return True
71
72def remove_stale_modules(data, installed_paths):
73    result = {}
74    for path, row in data.items():
75        if not _is_stale_module(path, installed_paths):
76            result[path] = row
77    return result
78
79def _parse_args():
80    parser = argparse.ArgumentParser()
81    parser.add_argument('tag_file')
82    parser.add_argument('-o', '--output', required=True)
83    parser.add_argument('--make-vars', required=True,
84                        help='out/soong/make_vars-$(TARGET).mk')
85    parser.add_argument('--module-info', required=True,
86                        help='out/target/product/$(TARGET)/module-info.json')
87    return parser.parse_args()
88
89def main():
90    args = _parse_args()
91
92    # Load libraries from `out/soong/make_vars-$(TARGET).mk`.
93    llndk, vndk_sp, vndk, vndk_private = load_make_vars(args.make_vars)
94
95    # Load eligible list csv file.
96    with open(args.tag_file, 'r') as fp:
97        reader = csv.reader(fp)
98        header = next(reader)
99        data = dict()
100        regex_patterns = []
101        for path, tag, comments in reader:
102            if path.startswith('[regex]'):
103                regex_patterns.append([path, tag, comments])
104            else:
105                data[path] = [path, tag, comments]
106
107    # Delete non-existing libraries.
108    installed_paths, name_path_dict = load_install_paths(args.module_info)
109    data = remove_stale_modules(data, installed_paths)
110
111    # Reset all /system/${LIB} libraries to FWK-ONLY.
112    for path, row in data.items():
113        if posixpath.dirname(path) == '/system/${LIB}':
114            row[1] = 'FWK-ONLY'
115
116    # Helper function to update the tag and the comments of an entry
117    def update_tag(path, tag, comments=None):
118        try:
119            data[path][1] = tag
120            if comments is not None:
121                data[path][2] = comments
122        except KeyError:
123            data[path] = [path, tag, comments if comments is not None else '']
124
125    # Helper function to find the subdir and the module name
126    def get_subdir_and_name(name, name_path_dict, prefix_core, prefix_vendor):
127        try:
128            path = name_path_dict[name + '.vendor']
129            assert path.startswith(prefix_vendor)
130            name_vendor = path[len(prefix_vendor):]
131        except KeyError:
132            name_vendor = name + '.so'
133
134        try:
135            path = name_path_dict[name]
136            assert path.startswith(prefix_core)
137            name_core = path[len(prefix_core):]
138        except KeyError:
139            name_core = name + '.so'
140
141        assert name_core == name_vendor
142        return name_core
143
144    # Update LL-NDK tags
145    prefix_core = '/system/${LIB}/'
146    for name in llndk:
147        try:
148            path = name_path_dict[name]
149            assert path.startswith(prefix_core)
150            name = path[len(prefix_core):]
151        except KeyError:
152            name = name + '.so'
153        update_tag('/system/${LIB}/' + name, 'LL-NDK')
154
155    # Update VNDK-SP and VNDK-SP-Private tags
156    prefix_core = '/system/${LIB}/'
157    prefix_vendor = '/system/${LIB}/vndk-sp${VNDK_VER}/'
158
159    for name in (vndk_sp - vndk_private):
160        name = get_subdir_and_name(name, name_path_dict, prefix_core,
161                                   prefix_vendor)
162        update_tag(prefix_core + name, 'VNDK-SP')
163        update_tag(prefix_vendor + name, 'VNDK-SP')
164
165    for name in (vndk_sp & vndk_private):
166        name = get_subdir_and_name(name, name_path_dict, prefix_core,
167                                   prefix_vendor)
168        update_tag(prefix_core + name, 'VNDK-SP-Private')
169        update_tag(prefix_vendor + name, 'VNDK-SP-Private')
170
171    # Update VNDK and VNDK-Private tags
172    prefix_core = '/system/${LIB}/'
173    prefix_vendor = '/system/${LIB}/vndk${VNDK_VER}/'
174
175    for name in (vndk - vndk_private):
176        name = get_subdir_and_name(name, name_path_dict, prefix_core,
177                                   prefix_vendor)
178        update_tag(prefix_core + name, 'VNDK')
179        update_tag(prefix_vendor + name, 'VNDK')
180
181    for name in (vndk & vndk_private):
182        name = get_subdir_and_name(name, name_path_dict, prefix_core,
183                                   prefix_vendor)
184        update_tag(prefix_core + name, 'VNDK-Private')
185        update_tag(prefix_vendor + name, 'VNDK-Private')
186
187    # Workaround for FWK-ONLY-RS
188    libs = [
189        'libft2',
190        'libmediandk',
191    ]
192    for name in libs:
193        update_tag('/system/${LIB}/' + name + '.so', 'FWK-ONLY-RS')
194
195    # Workaround for LL-NDK APEX bionic
196    libs = [
197        'libc',
198        'libdl',
199        'libm',
200    ]
201    for name in libs:
202        update_tag('/apex/com.android.runtime/${LIB}/bionic/' + name + '.so',
203                   'LL-NDK')
204
205    # Workaround for LL-NDK-Private
206    libs = [
207        'ld-android',
208        'libc_malloc_debug',
209        'libdl_android',
210        'libnetd_client',
211        'libtextclassifier_hash',
212    ]
213    for name in libs:
214        update_tag('/system/${LIB}/' + name + '.so', 'LL-NDK-Private')
215
216    # Workaround for libclang_rt.*.so
217    lib_sets = {
218        'LL-NDK': llndk,
219        'VNDK': vndk,
220    }
221    prefixes = {
222        'libclang_rt.asan': 'LL-NDK',
223        'libclang_rt.hwasan': 'LL-NDK',
224        'libclang_rt.scudo': 'VNDK',
225        'libclang_rt.ubsan_standalone': 'VNDK',
226    }
227    for prefix, tag in prefixes.items():
228        if any(name.startswith(prefix) for name in lib_sets[tag]):
229            for path in list(data.keys()):
230                if os.path.basename(path).startswith(prefix):
231                    update_tag(path, tag)
232
233    # Merge regular expression patterns into final dataset
234    for regex in regex_patterns:
235        data[regex[0]] = regex
236
237    # Write updated eligible list file
238    with open(args.output, 'w') as fp:
239        writer = csv.writer(fp, lineterminator='\n')
240        writer.writerow(header)
241        writer.writerows(sorted(data.values()))
242
243if __name__ == '__main__':
244    sys.exit(main())
245