Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PojavLauncherTeam
GitHub Repository: PojavLauncherTeam/angle
Path: blob/main_old/tools/android/modularization/convenience/lookup_dep.py
1695 views
1
#!/usr/bin/env python3
2
# Copyright 2021 The Chromium Authors. All rights reserved.
3
# Use of this source code is governed by a BSD-style license that can be
4
# found in the LICENSE file.
5
r'''Finds which build target(s) contain a particular Java class.
6
7
This is a utility script for finding out which build target dependency needs to
8
be added to import a given Java class.
9
10
It is a best-effort script.
11
12
Example:
13
14
Find build target with class FooUtil:
15
tools/android/modularization/convenience/lookup_dep.py FooUtil
16
'''
17
18
import argparse
19
import collections
20
import dataclasses
21
import json
22
import logging
23
import os
24
import pathlib
25
import subprocess
26
import sys
27
import zipfile
28
from typing import Dict, List, Set
29
30
_SRC_DIR = pathlib.Path(__file__).parents[4].resolve()
31
32
sys.path.append(str(_SRC_DIR / 'build' / 'android'))
33
from pylib import constants
34
35
# Import list_java_targets so that the dependency is found by print_python_deps.
36
import list_java_targets
37
38
39
def main():
40
arg_parser = argparse.ArgumentParser(
41
description='Finds which build target contains a particular Java class.')
42
43
arg_parser.add_argument('-C', '--output-directory', help='Build output directory.')
44
arg_parser.add_argument('--build', action='store_true', help='Build all .build_config files.')
45
arg_parser.add_argument('classes', nargs='+', help='Java classes to search for')
46
arg_parser.add_argument('-v', '--verbose', action='store_true', help='Verbose logging.')
47
48
arguments = arg_parser.parse_args()
49
50
logging.basicConfig(
51
level=logging.DEBUG if arguments.verbose else logging.WARNING,
52
format='%(asctime)s.%(msecs)03d %(levelname).1s %(message)s',
53
datefmt='%H:%M:%S')
54
55
if arguments.output_directory:
56
constants.SetOutputDirectory(arguments.output_directory)
57
constants.CheckOutputDirectory()
58
abs_out_dir: pathlib.Path = pathlib.Path(constants.GetOutDirectory()).resolve()
59
60
index = ClassLookupIndex(abs_out_dir, arguments.build)
61
matches = {c: index.match(c) for c in arguments.classes}
62
63
if not arguments.build:
64
# Try finding match without building because it is faster.
65
for class_name, match_list in matches.items():
66
if len(match_list) == 0:
67
arguments.build = True
68
break
69
if arguments.build:
70
index = ClassLookupIndex(abs_out_dir, True)
71
matches = {c: index.match(c) for c in arguments.classes}
72
73
if not arguments.build:
74
print('Showing potentially stale results. Run lookup.dep.py with --build '
75
'(slower) to build any unbuilt GN targets and get full results.')
76
print()
77
78
for (class_name, class_entries) in matches.items():
79
if not class_entries:
80
print(f'Could not find build target for class "{class_name}"')
81
elif len(class_entries) == 1:
82
class_entry = class_entries[0]
83
print(f'Class {class_entry.full_class_name} found:')
84
print(f' "{class_entry.target}"')
85
else:
86
print(f'Multiple targets with classes that match "{class_name}":')
87
print()
88
for class_entry in class_entries:
89
print(f' "{class_entry.target}"')
90
print(f' contains {class_entry.full_class_name}')
91
print()
92
93
94
@dataclasses.dataclass(frozen=True)
95
class ClassEntry:
96
"""An assignment of a Java class to a build target."""
97
full_class_name: str
98
target: str
99
100
101
class ClassLookupIndex:
102
"""A map from full Java class to its build targets.
103
104
A class might be in multiple targets if it's bytecode rewritten."""
105
106
def __init__(self, abs_build_output_dir: pathlib.Path, should_build: bool):
107
self._abs_build_output_dir = abs_build_output_dir
108
self._should_build = should_build
109
self._class_index = self._index_root()
110
111
def match(self, search_string: str) -> List[ClassEntry]:
112
"""Get class/target entries where the class matches search_string"""
113
# Priority 1: Exact full matches
114
if search_string in self._class_index:
115
return self._entries_for(search_string)
116
117
# Priority 2: Match full class name (any case), if it's a class name
118
matches = []
119
lower_search_string = search_string.lower()
120
if '.' not in lower_search_string:
121
for full_class_name in self._class_index:
122
package_and_class = full_class_name.rsplit('.', 1)
123
if len(package_and_class) < 2:
124
continue
125
class_name = package_and_class[1]
126
class_lower = class_name.lower()
127
if class_lower == lower_search_string:
128
matches.extend(self._entries_for(full_class_name))
129
if matches:
130
return matches
131
132
# Priority 3: Match anything
133
for full_class_name in self._class_index:
134
if lower_search_string in full_class_name.lower():
135
matches.extend(self._entries_for(full_class_name))
136
137
return matches
138
139
def _entries_for(self, class_name) -> List[ClassEntry]:
140
return [ClassEntry(class_name, target) for target in self._class_index.get(class_name)]
141
142
def _index_root(self) -> Dict[str, List[str]]:
143
"""Create the class to target index."""
144
logging.debug('Running list_java_targets.py...')
145
list_java_targets_command = [
146
'build/android/list_java_targets.py', '--gn-labels', '--print-build-config-paths',
147
f'--output-directory={self._abs_build_output_dir}'
148
]
149
if self._should_build:
150
list_java_targets_command += ['--build']
151
152
list_java_targets_run = subprocess.run(
153
list_java_targets_command, cwd=_SRC_DIR, capture_output=True, text=True, check=True)
154
logging.debug('... done.')
155
156
# Parse output of list_java_targets.py with mapping of build_target to
157
# build_config
158
root_build_targets = list_java_targets_run.stdout.split('\n')
159
class_index = collections.defaultdict(list)
160
for target_line in root_build_targets:
161
# Skip empty lines
162
if not target_line:
163
continue
164
165
target_line_parts = target_line.split(': ')
166
assert len(target_line_parts) == 2, target_line_parts
167
target, build_config_path = target_line_parts
168
169
if not os.path.exists(build_config_path):
170
assert not self._should_build
171
continue
172
173
with open(build_config_path) as build_config_contents:
174
build_config: Dict = json.load(build_config_contents)
175
deps_info = build_config['deps_info']
176
# Checking the library type here instead of in list_java_targets.py avoids
177
# reading each .build_config file twice.
178
if deps_info['type'] != 'java_library':
179
continue
180
181
target = self._compute_toplevel_target(target)
182
full_class_names = self._compute_full_class_names_for_build_config(deps_info)
183
for full_class_name in full_class_names:
184
class_index[full_class_name].append(target)
185
186
return class_index
187
188
@staticmethod
189
def _compute_toplevel_target(target: str) -> str:
190
"""Computes top level target from the passed-in sub-target."""
191
if target.endswith('_java'):
192
return target
193
194
# Handle android_aar_prebuilt() sub targets.
195
index = target.find('_java__subjar')
196
if index >= 0:
197
return target[0:index + 5]
198
index = target.find('_java__classes')
199
if index >= 0:
200
return target[0:index + 5]
201
202
return target
203
204
def _compute_full_class_names_for_build_config(self, deps_info: Dict) -> Set[str]:
205
"""Returns set of fully qualified class names for build config."""
206
207
full_class_names = set()
208
209
# Read the location of the java_sources_file from the build_config
210
sources_path = deps_info.get('java_sources_file')
211
if sources_path:
212
# Read the java_sources_file, indexing the classes found
213
with open(self._abs_build_output_dir / sources_path) as sources_contents:
214
for source_line in sources_contents:
215
source_path = pathlib.Path(source_line.strip())
216
java_class = self._parse_full_java_class(source_path)
217
if java_class:
218
full_class_names.add(java_class)
219
220
# |unprocessed_jar_path| is set for prebuilt targets. (ex:
221
# android_aar_prebuilt())
222
# |unprocessed_jar_path| might be set but not exist if not all targets have
223
# been built.
224
unprocessed_jar_path = deps_info.get('unprocessed_jar_path')
225
if unprocessed_jar_path:
226
abs_unprocessed_jar_path = (self._abs_build_output_dir / unprocessed_jar_path)
227
if abs_unprocessed_jar_path.exists():
228
# Normalize path but do not follow symlink if .jar is symlink.
229
abs_unprocessed_jar_path = (
230
abs_unprocessed_jar_path.parent.resolve() / abs_unprocessed_jar_path.name)
231
232
full_class_names.update(
233
self._extract_full_class_names_from_jar(self._abs_build_output_dir,
234
abs_unprocessed_jar_path))
235
236
return full_class_names
237
238
@staticmethod
239
def _extract_full_class_names_from_jar(abs_build_output_dir: pathlib.Path,
240
abs_jar_path: pathlib.Path) -> Set[str]:
241
"""Returns set of fully qualified class names in passed-in jar."""
242
out = set()
243
jar_namelist = ClassLookupIndex._read_jar_namelist(abs_build_output_dir, abs_jar_path)
244
for zip_entry_name in jar_namelist:
245
if not zip_entry_name.endswith('.class'):
246
continue
247
# Remove .class suffix
248
full_java_class = zip_entry_name[:-6]
249
250
full_java_class = full_java_class.replace('/', '.')
251
dollar_index = full_java_class.find('$')
252
if dollar_index >= 0:
253
full_java_class[0:dollar_index]
254
255
out.add(full_java_class)
256
return out
257
258
@staticmethod
259
def _read_jar_namelist(abs_build_output_dir: pathlib.Path,
260
abs_jar_path: pathlib.Path) -> List[str]:
261
"""Returns list of jar members by name."""
262
263
# Caching namelist speeds up lookup_dep.py runtime by 1.5s.
264
cache_path = abs_jar_path.with_suffix(abs_jar_path.suffix + '.namelist_cache')
265
if (not ClassLookupIndex._is_path_relative_to(abs_jar_path, abs_build_output_dir)):
266
cache_path = (abs_build_output_dir / 'gen' / cache_path.relative_to(_SRC_DIR))
267
if (cache_path.exists() and os.path.getmtime(cache_path) > os.path.getmtime(abs_jar_path)):
268
with open(cache_path) as f:
269
return [s.strip() for s in f.readlines()]
270
271
with zipfile.ZipFile(abs_jar_path) as z:
272
namelist = z.namelist()
273
274
cache_path.parent.mkdir(parents=True, exist_ok=True)
275
with open(cache_path, 'w') as f:
276
f.write('\n'.join(namelist))
277
278
return namelist
279
280
@staticmethod
281
def _is_path_relative_to(path: pathlib.Path, other: pathlib.Path) -> bool:
282
# PurePath.is_relative_to() was introduced in Python 3.9
283
resolved_path = path.resolve()
284
resolved_other = other.resolve()
285
return str(resolved_path).startswith(str(resolved_other))
286
287
@staticmethod
288
def _parse_full_java_class(source_path: pathlib.Path) -> str:
289
"""Guess the fully qualified class name from the path to the source file."""
290
if source_path.suffix != '.java':
291
logging.warning(f'"{source_path}" does not have the .java suffix')
292
return None
293
294
directory_path: pathlib.Path = source_path.parent
295
package_list_reversed = []
296
for part in reversed(directory_path.parts):
297
if part == 'java':
298
break
299
package_list_reversed.append(part)
300
if part in ('com', 'org'):
301
break
302
else:
303
logging.debug(f'File {source_path} not in a subdir of "org" or "com", '
304
'cannot detect package heuristically.')
305
return None
306
307
package = '.'.join(reversed(package_list_reversed))
308
class_name = source_path.stem
309
return f'{package}.{class_name}'
310
311
312
if __name__ == '__main__':
313
main()
314
315