blob: 9e8807eaf6f4a0824d6a1e95ade201c434bdb98f [file] [log] [blame]
Paul Duffin4dcf6592022-02-28 19:22:12 +00001#!/usr/bin/env -S python -u
2#
3# Copyright (C) 2022 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"""Analyze bootclasspath_fragment usage."""
17import argparse
18import dataclasses
Paul Duffindd97fd22022-02-28 19:22:12 +000019import enum
Paul Duffin4dcf6592022-02-28 19:22:12 +000020import json
21import logging
22import os
23import re
24import shutil
25import subprocess
26import tempfile
27import textwrap
28import typing
Paul Duffindd97fd22022-02-28 19:22:12 +000029from enum import Enum
30
Paul Duffin4dcf6592022-02-28 19:22:12 +000031import sys
32
Paul Duffindd97fd22022-02-28 19:22:12 +000033from signature_trie import signature_trie
34
Paul Duffin4dcf6592022-02-28 19:22:12 +000035_STUB_FLAGS_FILE = "out/soong/hiddenapi/hiddenapi-stub-flags.txt"
36
37_FLAGS_FILE = "out/soong/hiddenapi/hiddenapi-flags.csv"
38
39_INCONSISTENT_FLAGS = "ERROR: Hidden API flags are inconsistent:"
40
41
42class BuildOperation:
43
44 def __init__(self, popen):
45 self.popen = popen
46 self.returncode = None
47
48 def lines(self):
49 """Return an iterator over the lines output by the build operation.
50
51 The lines have had any trailing white space, including the newline
52 stripped.
53 """
54 return newline_stripping_iter(self.popen.stdout.readline)
55
56 def wait(self, *args, **kwargs):
57 self.popen.wait(*args, **kwargs)
58 self.returncode = self.popen.returncode
59
60
61@dataclasses.dataclass()
62class FlagDiffs:
63 """Encapsulates differences in flags reported by the build"""
64
65 # Map from member signature to the (module flags, monolithic flags)
66 diffs: typing.Dict[str, typing.Tuple[str, str]]
67
68
69@dataclasses.dataclass()
70class ModuleInfo:
71 """Provides access to the generated module-info.json file.
72
73 This is used to find the location of the file within which specific modules
74 are defined.
75 """
76
77 modules: typing.Dict[str, typing.Dict[str, typing.Any]]
78
79 @staticmethod
80 def load(filename):
81 with open(filename, "r", encoding="utf8") as f:
82 j = json.load(f)
83 return ModuleInfo(j)
84
85 def _module(self, module_name):
86 """Find module by name in module-info.json file"""
87 if module_name in self.modules:
88 return self.modules[module_name]
89
90 raise Exception(f"Module {module_name} could not be found")
91
92 def module_path(self, module_name):
93 module = self._module(module_name)
94 # The "path" is actually a list of paths, one for each class of module
95 # but as the modules are all created from bp files if a module does
96 # create multiple classes of make modules they should all have the same
97 # path.
98 paths = module["path"]
99 unique_paths = set(paths)
100 if len(unique_paths) != 1:
101 raise Exception(f"Expected module '{module_name}' to have a "
102 f"single unique path but found {unique_paths}")
103 return paths[0]
104
105
Paul Duffin26f19912022-03-28 16:09:27 +0100106def extract_indent(line):
107 return re.match(r"([ \t]*)", line).group(1)
108
109
110_SPECIAL_PLACEHOLDER: str = "SPECIAL_PLACEHOLDER"
111
112
113@dataclasses.dataclass
114class BpModifyRunner:
115
116 bpmodify_path: str
117
118 def add_values_to_property(self, property_name, values, module_name,
119 bp_file):
120 cmd = [
121 self.bpmodify_path, "-a", values, "-property", property_name, "-m",
122 module_name, "-w", bp_file, bp_file
123 ]
124
125 logging.debug(" ".join(cmd))
126 subprocess.run(
127 cmd,
128 stderr=subprocess.STDOUT,
129 stdout=log_stream_for_subprocess(),
130 check=True)
131
132
Paul Duffin4dcf6592022-02-28 19:22:12 +0000133@dataclasses.dataclass
134class FileChange:
135 path: str
136
137 description: str
138
139 def __lt__(self, other):
140 return self.path < other.path
141
142
Paul Duffindd97fd22022-02-28 19:22:12 +0000143class PropertyChangeAction(Enum):
144 """Allowable actions that are supported by HiddenApiPropertyChange."""
145
146 # New values are appended to any existing values.
147 APPEND = 1
148
149 # New values replace any existing values.
150 REPLACE = 2
151
152
Paul Duffin4dcf6592022-02-28 19:22:12 +0000153@dataclasses.dataclass
154class HiddenApiPropertyChange:
155
156 property_name: str
157
158 values: typing.List[str]
159
160 property_comment: str = ""
161
Paul Duffindd97fd22022-02-28 19:22:12 +0000162 # The action that indicates how this change is applied.
163 action: PropertyChangeAction = PropertyChangeAction.APPEND
164
Paul Duffin4dcf6592022-02-28 19:22:12 +0000165 def snippet(self, indent):
166 snippet = "\n"
167 snippet += format_comment_as_text(self.property_comment, indent)
168 snippet += f"{indent}{self.property_name}: ["
169 if self.values:
170 snippet += "\n"
171 for value in self.values:
172 snippet += f'{indent} "{value}",\n'
173 snippet += f"{indent}"
174 snippet += "],\n"
175 return snippet
176
Paul Duffin26f19912022-03-28 16:09:27 +0100177 def fix_bp_file(self, bcpf_bp_file, bcpf, bpmodify_runner: BpModifyRunner):
178 # Add an additional placeholder value to identify the modification that
179 # bpmodify makes.
180 bpmodify_values = [_SPECIAL_PLACEHOLDER]
Paul Duffindd97fd22022-02-28 19:22:12 +0000181
182 if self.action == PropertyChangeAction.APPEND:
183 # If adding the values to the existing values then pass the new
184 # values to bpmodify.
185 bpmodify_values.extend(self.values)
186 elif self.action == PropertyChangeAction.REPLACE:
187 # If replacing the existing values then it is not possible to use
188 # bpmodify for that directly. It could be used twice to remove the
189 # existing property and then add a new one but that does not remove
190 # any related comments and loses the position of the existing
191 # property as the new property is always added to the end of the
192 # containing block.
193 #
194 # So, instead of passing the new values to bpmodify this this just
195 # adds an extra placeholder to force bpmodify to format the list
196 # across multiple lines to ensure a consistent structure for the
197 # code that removes all the existing values and adds the new ones.
198 #
199 # This placeholder has to be different to the other placeholder as
200 # bpmodify dedups values.
201 bpmodify_values.append(_SPECIAL_PLACEHOLDER + "_REPLACE")
202 else:
203 raise ValueError(f"unknown action {self.action}")
Paul Duffin26f19912022-03-28 16:09:27 +0100204
205 packages = ",".join(bpmodify_values)
206 bpmodify_runner.add_values_to_property(
207 f"hidden_api.{self.property_name}", packages, bcpf, bcpf_bp_file)
208
209 with open(bcpf_bp_file, "r", encoding="utf8") as tio:
210 lines = tio.readlines()
211 lines = [line.rstrip("\n") for line in lines]
212
213 if self.fixup_bpmodify_changes(bcpf_bp_file, lines):
214 with open(bcpf_bp_file, "w", encoding="utf8") as tio:
215 for line in lines:
216 print(line, file=tio)
217
218 def fixup_bpmodify_changes(self, bcpf_bp_file, lines):
Paul Duffindd97fd22022-02-28 19:22:12 +0000219 """Fixup the output of bpmodify.
220
221 The bpmodify tool does not support all the capabilities that this needs
222 so it is used to do what it can, including marking the place in the
223 Android.bp file where it makes its changes and then this gets passed a
224 list of lines from that file which it then modifies to complete the
225 change.
226
227 This analyzes the list of lines to find the indices of the significant
228 lines and then applies some changes. As those changes can insert and
229 delete lines (changing the indices of following lines) the changes are
230 generally done in reverse order starting from the end and working
231 towards the beginning. That ensures that the changes do not invalidate
232 the indices of following lines.
233 """
234
Paul Duffin26f19912022-03-28 16:09:27 +0100235 # Find the line containing the placeholder that has been inserted.
236 place_holder_index = -1
237 for i, line in enumerate(lines):
238 if _SPECIAL_PLACEHOLDER in line:
239 place_holder_index = i
240 break
241 if place_holder_index == -1:
242 logging.debug("Could not find %s in %s", _SPECIAL_PLACEHOLDER,
243 bcpf_bp_file)
244 return False
245
246 # Remove the place holder. Do this before inserting the comment as that
247 # would change the location of the place holder in the list.
248 place_holder_line = lines[place_holder_index]
249 if place_holder_line.endswith("],"):
250 place_holder_line = place_holder_line.replace(
251 f'"{_SPECIAL_PLACEHOLDER}"', "")
252 lines[place_holder_index] = place_holder_line
253 else:
254 del lines[place_holder_index]
255
256 # Scan forward to the end of the property block to remove a blank line
257 # that bpmodify inserts.
258 end_property_array_index = -1
259 for i in range(place_holder_index, len(lines)):
260 line = lines[i]
261 if line.endswith("],"):
262 end_property_array_index = i
263 break
264 if end_property_array_index == -1:
265 logging.debug("Could not find end of property array in %s",
266 bcpf_bp_file)
267 return False
268
269 # If bdmodify inserted a blank line afterwards then remove it.
270 if (not lines[end_property_array_index + 1] and
271 lines[end_property_array_index + 2].endswith("},")):
272 del lines[end_property_array_index + 1]
273
274 # Scan back to find the preceding property line.
275 property_line_index = -1
276 for i in range(place_holder_index, 0, -1):
277 line = lines[i]
278 if line.lstrip().startswith(f"{self.property_name}: ["):
279 property_line_index = i
280 break
281 if property_line_index == -1:
282 logging.debug("Could not find property line in %s", bcpf_bp_file)
283 return False
284
Paul Duffindd97fd22022-02-28 19:22:12 +0000285 # If this change is replacing the existing values then they need to be
286 # removed and replaced with the new values. That will change the lines
287 # after the property but it is necessary to do here as the following
288 # code operates on earlier lines.
289 if self.action == PropertyChangeAction.REPLACE:
290 # This removes the existing values and replaces them with the new
291 # values.
292 indent = extract_indent(lines[property_line_index + 1])
293 insert = [f'{indent}"{x}",' for x in self.values]
294 lines[property_line_index + 1:end_property_array_index] = insert
295 if not self.values:
296 # If the property has no values then merge the ], onto the
297 # same line as the property name.
298 del lines[property_line_index + 1]
299 lines[property_line_index] = lines[property_line_index] + "],"
300
Paul Duffin26f19912022-03-28 16:09:27 +0100301 # Only insert a comment if the property does not already have a comment.
302 line_preceding_property = lines[(property_line_index - 1)]
303 if (self.property_comment and
304 not re.match("([ \t]+)// ", line_preceding_property)):
305 # Extract the indent from the property line and use it to format the
306 # comment.
307 indent = extract_indent(lines[property_line_index])
308 comment_lines = format_comment_as_lines(self.property_comment,
309 indent)
310
311 # If the line before the comment is not blank then insert an extra
312 # blank line at the beginning of the comment.
313 if line_preceding_property:
314 comment_lines.insert(0, "")
315
316 # Insert the comment before the property.
317 lines[property_line_index:property_line_index] = comment_lines
318 return True
319
Paul Duffin4dcf6592022-02-28 19:22:12 +0000320
321@dataclasses.dataclass()
322class Result:
323 """Encapsulates the result of the analysis."""
324
325 # The diffs in the flags.
326 diffs: typing.Optional[FlagDiffs] = None
327
328 # The bootclasspath_fragment hidden API properties changes.
329 property_changes: typing.List[HiddenApiPropertyChange] = dataclasses.field(
330 default_factory=list)
331
332 # The list of file changes.
333 file_changes: typing.List[FileChange] = dataclasses.field(
334 default_factory=list)
335
336
Paul Duffindd97fd22022-02-28 19:22:12 +0000337class ClassProvider(enum.Enum):
338 """The source of a class found during the hidden API processing"""
339 BCPF = "bcpf"
340 OTHER = "other"
341
342
343# A fake member to use when using the signature trie to compute the package
344# properties from hidden API flags. This is needed because while that
345# computation only cares about classes the trie expects a class to be an
346# interior node but without a member it makes the class a leaf node. That causes
347# problems when analyzing inner classes as the outer class is a leaf node for
348# its own entry but is used as an interior node for inner classes.
349_FAKE_MEMBER = ";->fake()V"
350
351
Paul Duffin4dcf6592022-02-28 19:22:12 +0000352@dataclasses.dataclass()
353class BcpfAnalyzer:
Paul Duffin26f19912022-03-28 16:09:27 +0100354 # Path to this tool.
355 tool_path: str
356
Paul Duffin4dcf6592022-02-28 19:22:12 +0000357 # Directory pointed to by ANDROID_BUILD_OUT
358 top_dir: str
359
360 # Directory pointed to by OUT_DIR of {top_dir}/out if that is not set.
361 out_dir: str
362
363 # Directory pointed to by ANDROID_PRODUCT_OUT.
364 product_out_dir: str
365
366 # The name of the bootclasspath_fragment module.
367 bcpf: str
368
369 # The name of the apex module containing {bcpf}, only used for
370 # informational purposes.
371 apex: str
372
373 # The name of the sdk module containing {bcpf}, only used for
374 # informational purposes.
375 sdk: str
376
Paul Duffin26f19912022-03-28 16:09:27 +0100377 # If true then this will attempt to automatically fix any issues that are
378 # found.
379 fix: bool = False
380
Paul Duffin4dcf6592022-02-28 19:22:12 +0000381 # All the signatures, loaded from all-flags.csv, initialized by
382 # load_all_flags().
383 _signatures: typing.Set[str] = dataclasses.field(default_factory=set)
384
385 # All the classes, loaded from all-flags.csv, initialized by
386 # load_all_flags().
387 _classes: typing.Set[str] = dataclasses.field(default_factory=set)
388
389 # Information loaded from module-info.json, initialized by
390 # load_module_info().
391 module_info: ModuleInfo = None
392
393 @staticmethod
394 def reformat_report_test(text):
395 return re.sub(r"(.)\n([^\s])", r"\1 \2", text)
396
Paul Duffinea836c22022-04-04 16:59:36 +0100397 def report(self, text="", **kwargs):
Paul Duffin4dcf6592022-02-28 19:22:12 +0000398 # Concatenate lines that are not separated by a blank line together to
399 # eliminate formatting applied to the supplied text to adhere to python
400 # line length limitations.
401 text = self.reformat_report_test(text)
402 logging.info("%s", text, **kwargs)
403
Paul Duffinea836c22022-04-04 16:59:36 +0100404 def report_dedent(self, text, **kwargs):
405 text = textwrap.dedent(text)
406 self.report(text, **kwargs)
407
Paul Duffin4dcf6592022-02-28 19:22:12 +0000408 def run_command(self, cmd, *args, **kwargs):
409 cmd_line = " ".join(cmd)
410 logging.debug("Running %s", cmd_line)
411 subprocess.run(
412 cmd,
413 *args,
414 check=True,
415 cwd=self.top_dir,
416 stderr=subprocess.STDOUT,
417 stdout=log_stream_for_subprocess(),
418 text=True,
419 **kwargs)
420
421 @property
422 def signatures(self):
423 if not self._signatures:
424 raise Exception("signatures has not been initialized")
425 return self._signatures
426
427 @property
428 def classes(self):
429 if not self._classes:
430 raise Exception("classes has not been initialized")
431 return self._classes
432
433 def load_all_flags(self):
434 all_flags = self.find_bootclasspath_fragment_output_file(
435 "all-flags.csv")
436
437 # Extract the set of signatures and a separate set of classes produced
438 # by the bootclasspath_fragment.
439 with open(all_flags, "r", encoding="utf8") as f:
440 for line in newline_stripping_iter(f.readline):
441 signature = self.line_to_signature(line)
442 self._signatures.add(signature)
443 class_name = self.signature_to_class(signature)
444 self._classes.add(class_name)
445
446 def load_module_info(self):
447 module_info_file = os.path.join(self.product_out_dir,
448 "module-info.json")
Paul Duffinea836c22022-04-04 16:59:36 +0100449 self.report(f"\nMaking sure that {module_info_file} is up to date.\n")
Paul Duffin4dcf6592022-02-28 19:22:12 +0000450 output = self.build_file_read_output(module_info_file)
451 lines = output.lines()
452 for line in lines:
453 logging.debug("%s", line)
454 output.wait(timeout=10)
455 if output.returncode:
456 raise Exception(f"Error building {module_info_file}")
457 abs_module_info_file = os.path.join(self.top_dir, module_info_file)
458 self.module_info = ModuleInfo.load(abs_module_info_file)
459
460 @staticmethod
461 def line_to_signature(line):
462 return line.split(",")[0]
463
464 @staticmethod
465 def signature_to_class(signature):
466 return signature.split(";->")[0]
467
468 @staticmethod
469 def to_parent_package(pkg_or_class):
470 return pkg_or_class.rsplit("/", 1)[0]
471
472 def module_path(self, module_name):
473 return self.module_info.module_path(module_name)
474
475 def module_out_dir(self, module_name):
476 module_path = self.module_path(module_name)
477 return os.path.join(self.out_dir, "soong/.intermediates", module_path,
478 module_name)
479
Paul Duffindd97fd22022-02-28 19:22:12 +0000480 def find_bootclasspath_fragment_output_file(self, basename, required=True):
Paul Duffin4dcf6592022-02-28 19:22:12 +0000481 # Find the output file of the bootclasspath_fragment with the specified
482 # base name.
483 found_file = ""
484 bcpf_out_dir = self.module_out_dir(self.bcpf)
485 for (dirpath, _, filenames) in os.walk(bcpf_out_dir):
486 for f in filenames:
487 if f == basename:
488 found_file = os.path.join(dirpath, f)
489 break
Paul Duffindd97fd22022-02-28 19:22:12 +0000490 if not found_file and required:
Paul Duffin4dcf6592022-02-28 19:22:12 +0000491 raise Exception(f"Could not find {basename} in {bcpf_out_dir}")
492 return found_file
493
494 def analyze(self):
495 """Analyze a bootclasspath_fragment module.
496
497 Provides help in resolving any existing issues and provides
498 optimizations that can be applied.
499 """
500 self.report(f"Analyzing bootclasspath_fragment module {self.bcpf}")
Paul Duffinea836c22022-04-04 16:59:36 +0100501 self.report_dedent(f"""
502 Run this tool to help initialize a bootclasspath_fragment module.
503 Before you start make sure that:
Paul Duffin4dcf6592022-02-28 19:22:12 +0000504
Paul Duffinea836c22022-04-04 16:59:36 +0100505 1. The current checkout is up to date.
Paul Duffin4dcf6592022-02-28 19:22:12 +0000506
Paul Duffinea836c22022-04-04 16:59:36 +0100507 2. The environment has been initialized using lunch, e.g.
508 lunch aosp_arm64-userdebug
Paul Duffin4dcf6592022-02-28 19:22:12 +0000509
Paul Duffinea836c22022-04-04 16:59:36 +0100510 3. You have added a bootclasspath_fragment module to the appropriate
511 Android.bp file. Something like this:
Paul Duffin4dcf6592022-02-28 19:22:12 +0000512
Paul Duffinea836c22022-04-04 16:59:36 +0100513 bootclasspath_fragment {{
514 name: "{self.bcpf}",
515 contents: [
516 "...",
517 ],
518
519 // The bootclasspath_fragments that provide APIs on which this
520 // depends.
521 fragments: [
522 {{
523 apex: "com.android.art",
524 module: "art-bootclasspath-fragment",
525 }},
526 ],
527 }}
528
529 4. You have added it to the platform_bootclasspath module in
530 frameworks/base/boot/Android.bp. Something like this:
Paul Duffin4dcf6592022-02-28 19:22:12 +0000531
Paul Duffinea836c22022-04-04 16:59:36 +0100532 platform_bootclasspath {{
533 name: "platform-bootclasspath",
534 fragments: [
535 ...
536 {{
537 apex: "{self.apex}",
538 module: "{self.bcpf}",
539 }},
540 ],
541 }}
Paul Duffin4dcf6592022-02-28 19:22:12 +0000542
Paul Duffinea836c22022-04-04 16:59:36 +0100543 5. You have added an sdk module. Something like this:
Paul Duffin4dcf6592022-02-28 19:22:12 +0000544
Paul Duffinea836c22022-04-04 16:59:36 +0100545 sdk {{
546 name: "{self.sdk}",
547 bootclasspath_fragments: ["{self.bcpf}"],
548 }}
549 """)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000550
551 # Make sure that the module-info.json file is up to date.
552 self.load_module_info()
553
Paul Duffinea836c22022-04-04 16:59:36 +0100554 self.report_dedent("""
555 Cleaning potentially stale files.
556 """)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000557 # Remove the out/soong/hiddenapi files.
558 shutil.rmtree(f"{self.out_dir}/soong/hiddenapi", ignore_errors=True)
559
560 # Remove any bootclasspath_fragment output files.
561 shutil.rmtree(self.module_out_dir(self.bcpf), ignore_errors=True)
562
563 self.build_monolithic_stubs_flags()
564
565 result = Result()
566
567 self.build_monolithic_flags(result)
Paul Duffindd97fd22022-02-28 19:22:12 +0000568 self.analyze_hiddenapi_package_properties(result)
569 self.explain_how_to_check_signature_patterns()
Paul Duffin4dcf6592022-02-28 19:22:12 +0000570
571 # If there were any changes that need to be made to the Android.bp
Paul Duffin26f19912022-03-28 16:09:27 +0100572 # file then either apply or report them.
Paul Duffin4dcf6592022-02-28 19:22:12 +0000573 if result.property_changes:
574 bcpf_dir = self.module_info.module_path(self.bcpf)
575 bcpf_bp_file = os.path.join(self.top_dir, bcpf_dir, "Android.bp")
Paul Duffin26f19912022-03-28 16:09:27 +0100576 if self.fix:
577 tool_dir = os.path.dirname(self.tool_path)
578 bpmodify_path = os.path.join(tool_dir, "bpmodify")
579 bpmodify_runner = BpModifyRunner(bpmodify_path)
580 for property_change in result.property_changes:
581 property_change.fix_bp_file(bcpf_bp_file, self.bcpf,
582 bpmodify_runner)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000583
Paul Duffin26f19912022-03-28 16:09:27 +0100584 result.file_changes.append(
585 self.new_file_change(
586 bcpf_bp_file,
587 f"Updated hidden_api properties of '{self.bcpf}'"))
Paul Duffin4dcf6592022-02-28 19:22:12 +0000588
Paul Duffin26f19912022-03-28 16:09:27 +0100589 else:
590 hiddenapi_snippet = ""
591 for property_change in result.property_changes:
592 hiddenapi_snippet += property_change.snippet(" ")
593
594 # Remove leading and trailing blank lines.
595 hiddenapi_snippet = hiddenapi_snippet.strip("\n")
596
597 result.file_changes.append(
598 self.new_file_change(
599 bcpf_bp_file, f"""
Paul Duffin4dcf6592022-02-28 19:22:12 +0000600Add the following snippet into the {self.bcpf} bootclasspath_fragment module
601in the {bcpf_dir}/Android.bp file. If the hidden_api block already exists then
602merge these properties into it.
603
604 hidden_api: {{
605{hiddenapi_snippet}
606 }},
607"""))
608
609 if result.file_changes:
Paul Duffin26f19912022-03-28 16:09:27 +0100610 if self.fix:
Paul Duffinea836c22022-04-04 16:59:36 +0100611 file_change_message = textwrap.dedent("""
612 The following files were modified by this script:
613 """)
Paul Duffin26f19912022-03-28 16:09:27 +0100614 else:
Paul Duffinea836c22022-04-04 16:59:36 +0100615 file_change_message = textwrap.dedent("""
616 The following modifications need to be made:
617 """)
Paul Duffin26f19912022-03-28 16:09:27 +0100618
Paul Duffinea836c22022-04-04 16:59:36 +0100619 self.report(file_change_message)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000620 result.file_changes.sort()
621 for file_change in result.file_changes:
Paul Duffinea836c22022-04-04 16:59:36 +0100622 self.report(f" {file_change.path}")
623 self.report(f" {file_change.description}")
624 self.report()
Paul Duffin4dcf6592022-02-28 19:22:12 +0000625
Paul Duffin26f19912022-03-28 16:09:27 +0100626 if not self.fix:
Paul Duffinea836c22022-04-04 16:59:36 +0100627 self.report_dedent("""
628 Run the command again with the --fix option to automatically
629 make the above changes.
630 """.lstrip("\n"))
Paul Duffin26f19912022-03-28 16:09:27 +0100631
Paul Duffin4dcf6592022-02-28 19:22:12 +0000632 def new_file_change(self, file, description):
633 return FileChange(
634 path=os.path.relpath(file, self.top_dir), description=description)
635
636 def check_inconsistent_flag_lines(self, significant, module_line,
637 monolithic_line, separator_line):
638 if not (module_line.startswith("< ") and
639 monolithic_line.startswith("> ") and not separator_line):
640 # Something went wrong.
Paul Duffinea836c22022-04-04 16:59:36 +0100641 self.report("Invalid build output detected:")
642 self.report(f" module_line: '{module_line}'")
643 self.report(f" monolithic_line: '{monolithic_line}'")
644 self.report(f" separator_line: '{separator_line}'")
Paul Duffin4dcf6592022-02-28 19:22:12 +0000645 sys.exit(1)
646
647 if significant:
648 logging.debug("%s", module_line)
649 logging.debug("%s", monolithic_line)
650 logging.debug("%s", separator_line)
651
652 def scan_inconsistent_flags_report(self, lines):
653 """Scans a hidden API flags report
654
655 The hidden API inconsistent flags report which looks something like
656 this.
657
658 < out/soong/.intermediates/.../filtered-stub-flags.csv
659 > out/soong/hiddenapi/hiddenapi-stub-flags.txt
660
661 < Landroid/compat/Compatibility;->clearOverrides()V
662 > Landroid/compat/Compatibility;->clearOverrides()V,core-platform-api
663
664 """
665
666 # The basic format of an entry in the inconsistent flags report is:
667 # <module specific flag>
668 # <monolithic flag>
669 # <separator>
670 #
671 # Wrap the lines iterator in an iterator which returns a tuple
672 # consisting of the three separate lines.
673 triples = zip(lines, lines, lines)
674
675 module_line, monolithic_line, separator_line = next(triples)
676 significant = False
677 bcpf_dir = self.module_info.module_path(self.bcpf)
678 if os.path.join(bcpf_dir, self.bcpf) in module_line:
679 # These errors are related to the bcpf being analyzed so
680 # keep them.
681 significant = True
682 else:
683 self.report(f"Filtering out errors related to {module_line}")
684
685 self.check_inconsistent_flag_lines(significant, module_line,
686 monolithic_line, separator_line)
687
688 diffs = {}
689 for module_line, monolithic_line, separator_line in triples:
690 self.check_inconsistent_flag_lines(significant, module_line,
691 monolithic_line, "")
692
693 module_parts = module_line.removeprefix("< ").split(",")
694 module_signature = module_parts[0]
695 module_flags = module_parts[1:]
696
697 monolithic_parts = monolithic_line.removeprefix("> ").split(",")
698 monolithic_signature = monolithic_parts[0]
699 monolithic_flags = monolithic_parts[1:]
700
701 if module_signature != monolithic_signature:
702 # Something went wrong.
Paul Duffinea836c22022-04-04 16:59:36 +0100703 self.report("Inconsistent signatures detected:")
704 self.report(f" module_signature: '{module_signature}'")
705 self.report(f" monolithic_signature: '{monolithic_signature}'")
Paul Duffin4dcf6592022-02-28 19:22:12 +0000706 sys.exit(1)
707
708 diffs[module_signature] = (module_flags, monolithic_flags)
709
710 if separator_line:
711 # If the separator line is not blank then it is the end of the
712 # current report, and possibly the start of another.
713 return separator_line, diffs
714
715 return "", diffs
716
717 def build_file_read_output(self, filename):
718 # Make sure the filename is relative to top if possible as the build
719 # may be using relative paths as the target.
720 rel_filename = filename.removeprefix(self.top_dir)
721 cmd = ["build/soong/soong_ui.bash", "--make-mode", rel_filename]
722 cmd_line = " ".join(cmd)
723 logging.debug("%s", cmd_line)
724 # pylint: disable=consider-using-with
725 output = subprocess.Popen(
726 cmd,
727 cwd=self.top_dir,
728 stderr=subprocess.STDOUT,
729 stdout=subprocess.PIPE,
730 text=True,
731 )
732 return BuildOperation(popen=output)
733
734 def build_hiddenapi_flags(self, filename):
735 output = self.build_file_read_output(filename)
736
737 lines = output.lines()
738 diffs = None
739 for line in lines:
740 logging.debug("%s", line)
741 while line == _INCONSISTENT_FLAGS:
742 line, diffs = self.scan_inconsistent_flags_report(lines)
743
744 output.wait(timeout=10)
745 if output.returncode != 0:
746 logging.debug("Command failed with %s", output.returncode)
747 else:
748 logging.debug("Command succeeded")
749
750 return diffs
751
752 def build_monolithic_stubs_flags(self):
Paul Duffinea836c22022-04-04 16:59:36 +0100753 self.report_dedent(f"""
754 Attempting to build {_STUB_FLAGS_FILE} to verify that the
755 bootclasspath_fragment has the correct API stubs available...
756 """)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000757
758 # Build the hiddenapi-stubs-flags.txt file.
759 diffs = self.build_hiddenapi_flags(_STUB_FLAGS_FILE)
760 if diffs:
Paul Duffinea836c22022-04-04 16:59:36 +0100761 self.report_dedent(f"""
762 There is a discrepancy between the stub API derived flags
763 created by the bootclasspath_fragment and the
764 platform_bootclasspath. See preceding error messages to see
765 which flags are inconsistent. The inconsistencies can occur for
766 a couple of reasons:
Paul Duffin4dcf6592022-02-28 19:22:12 +0000767
Paul Duffinea836c22022-04-04 16:59:36 +0100768 If you are building against prebuilts of the Android SDK, e.g.
769 by using TARGET_BUILD_APPS then the prebuilt versions of the
770 APIs this bootclasspath_fragment depends upon are out of date
771 and need updating. See go/update-prebuilts for help.
Paul Duffin4dcf6592022-02-28 19:22:12 +0000772
Paul Duffinea836c22022-04-04 16:59:36 +0100773 Otherwise, this is happening because there are some stub APIs
774 that are either provided by or used by the contents of the
775 bootclasspath_fragment but which are not available to it. There
776 are 4 ways to handle this:
Paul Duffin4dcf6592022-02-28 19:22:12 +0000777
Paul Duffinea836c22022-04-04 16:59:36 +0100778 1. A java_sdk_library in the contents property will
779 automatically make its stub APIs available to the
780 bootclasspath_fragment so nothing needs to be done.
Paul Duffin4dcf6592022-02-28 19:22:12 +0000781
Paul Duffinea836c22022-04-04 16:59:36 +0100782 2. If the API provided by the bootclasspath_fragment is created
783 by an api_only java_sdk_library (or a java_library that compiles
784 files generated by a separate droidstubs module then it cannot
785 be added to the contents and instead must be added to the
786 api.stubs property, e.g.
Paul Duffin4dcf6592022-02-28 19:22:12 +0000787
Paul Duffinea836c22022-04-04 16:59:36 +0100788 bootclasspath_fragment {{
789 name: "{self.bcpf}",
790 ...
791 api: {{
792 stubs: ["$MODULE-api-only"],"
793 }},
794 }}
Paul Duffin4dcf6592022-02-28 19:22:12 +0000795
Paul Duffinea836c22022-04-04 16:59:36 +0100796 3. If the contents use APIs provided by another
797 bootclasspath_fragment then it needs to be added to the
798 fragments property, e.g.
799
800 bootclasspath_fragment {{
801 name: "{self.bcpf}",
802 ...
803 // The bootclasspath_fragments that provide APIs on which this depends.
804 fragments: [
805 ...
806 {{
807 apex: "com.android.other",
808 module: "com.android.other-bootclasspath-fragment",
809 }},
810 ],
811 }}
812
813 4. If the contents use APIs from a module that is not part of
814 another bootclasspath_fragment then it must be added to the
815 additional_stubs property, e.g.
Paul Duffin4dcf6592022-02-28 19:22:12 +0000816
Paul Duffinea836c22022-04-04 16:59:36 +0100817 bootclasspath_fragment {{
818 name: "{self.bcpf}",
819 ...
820 additional_stubs: ["android-non-updatable"],
821 }}
Paul Duffin4dcf6592022-02-28 19:22:12 +0000822
Paul Duffinea836c22022-04-04 16:59:36 +0100823 Like the api.stubs property these are typically
824 java_sdk_library modules but can be java_library too.
Paul Duffin4dcf6592022-02-28 19:22:12 +0000825
Paul Duffinea836c22022-04-04 16:59:36 +0100826 Note: The "android-non-updatable" is treated as if it was a
827 java_sdk_library which it is not at the moment but will be in
828 future.
829 """)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000830
831 return diffs
832
833 def build_monolithic_flags(self, result):
Paul Duffinea836c22022-04-04 16:59:36 +0100834 self.report_dedent(f"""
835 Attempting to build {_FLAGS_FILE} to verify that the
836 bootclasspath_fragment has the correct hidden API flags...
837 """)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000838
839 # Build the hiddenapi-flags.csv file and extract any differences in
840 # the flags between this bootclasspath_fragment and the monolithic
841 # files.
842 result.diffs = self.build_hiddenapi_flags(_FLAGS_FILE)
843
844 # Load information from the bootclasspath_fragment's all-flags.csv file.
845 self.load_all_flags()
846
847 if result.diffs:
Paul Duffinea836c22022-04-04 16:59:36 +0100848 self.report_dedent(f"""
849 There is a discrepancy between the hidden API flags created by
850 the bootclasspath_fragment and the platform_bootclasspath. See
851 preceding error messages to see which flags are inconsistent.
852 The inconsistencies can occur for a couple of reasons:
Paul Duffin4dcf6592022-02-28 19:22:12 +0000853
Paul Duffinea836c22022-04-04 16:59:36 +0100854 If you are building against prebuilts of this
855 bootclasspath_fragment then the prebuilt version of the sdk
856 snapshot (specifically the hidden API flag files) are
857 inconsistent with the prebuilt version of the apex {self.apex}.
858 Please ensure that they are both updated from the same build.
Paul Duffin4dcf6592022-02-28 19:22:12 +0000859
Paul Duffinea836c22022-04-04 16:59:36 +0100860 1. There are custom hidden API flags specified in the one of the
861 files in frameworks/base/boot/hiddenapi which apply to the
862 bootclasspath_fragment but which are not supplied to the
863 bootclasspath_fragment module.
Paul Duffin4dcf6592022-02-28 19:22:12 +0000864
Paul Duffinea836c22022-04-04 16:59:36 +0100865 2. The bootclasspath_fragment specifies invalid
866 "split_packages", "single_packages" and/of "package_prefixes"
867 properties that match packages and classes that it does not
868 provide.
869 """)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000870
871 # Check to see if there are any hiddenapi related properties that
872 # need to be added to the
Paul Duffinea836c22022-04-04 16:59:36 +0100873 self.report_dedent("""
874 Checking custom hidden API flags....
875 """)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000876 self.check_frameworks_base_boot_hidden_api_files(result)
877
878 def report_hidden_api_flag_file_changes(self, result, property_name,
879 flags_file, rel_bcpf_flags_file,
880 bcpf_flags_file):
881 matched_signatures = set()
882 # Open the flags file to read the flags from.
883 with open(flags_file, "r", encoding="utf8") as f:
884 for signature in newline_stripping_iter(f.readline):
885 if signature in self.signatures:
886 # The signature is provided by the bootclasspath_fragment so
887 # it will need to be moved to the bootclasspath_fragment
888 # specific file.
889 matched_signatures.add(signature)
890
891 # If the bootclasspath_fragment specific flags file is not empty
892 # then it contains flags. That could either be new flags just moved
893 # from frameworks/base or previous contents of the file. In either
894 # case the file must not be removed.
895 if matched_signatures:
896 insert = textwrap.indent("\n".join(matched_signatures),
897 " ")
898 result.file_changes.append(
899 self.new_file_change(
900 flags_file, f"""Remove the following entries:
901{insert}
902"""))
903
904 result.file_changes.append(
905 self.new_file_change(
906 bcpf_flags_file, f"""Add the following entries:
907{insert}
908"""))
909
910 result.property_changes.append(
911 HiddenApiPropertyChange(
912 property_name=property_name,
913 values=[rel_bcpf_flags_file],
914 ))
915
Paul Duffin26f19912022-03-28 16:09:27 +0100916 def fix_hidden_api_flag_files(self, result, property_name, flags_file,
917 rel_bcpf_flags_file, bcpf_flags_file):
918 # Read the file in frameworks/base/boot/hiddenapi/<file> copy any
919 # flags that relate to the bootclasspath_fragment into a local
920 # file in the hiddenapi subdirectory.
921 tmp_flags_file = flags_file + ".tmp"
922
923 # Make sure the directory containing the bootclasspath_fragment specific
924 # hidden api flags exists.
925 os.makedirs(os.path.dirname(bcpf_flags_file), exist_ok=True)
926
927 bcpf_flags_file_exists = os.path.exists(bcpf_flags_file)
928
929 matched_signatures = set()
930 # Open the flags file to read the flags from.
931 with open(flags_file, "r", encoding="utf8") as f:
932 # Open a temporary file to write the flags (minus any removed
933 # flags).
934 with open(tmp_flags_file, "w", encoding="utf8") as t:
935 # Open the bootclasspath_fragment file for append just in
936 # case it already exists.
937 with open(bcpf_flags_file, "a", encoding="utf8") as b:
938 for line in iter(f.readline, ""):
939 signature = line.rstrip()
940 if signature in self.signatures:
941 # The signature is provided by the
942 # bootclasspath_fragment so write it to the new
943 # bootclasspath_fragment specific file.
944 print(line, file=b, end="")
945 matched_signatures.add(signature)
946 else:
947 # The signature is NOT provided by the
948 # bootclasspath_fragment. Copy it to the new
949 # monolithic file.
950 print(line, file=t, end="")
951
952 # If the bootclasspath_fragment specific flags file is not empty
953 # then it contains flags. That could either be new flags just moved
954 # from frameworks/base or previous contents of the file. In either
955 # case the file must not be removed.
956 if matched_signatures:
957 # There are custom flags related to the bootclasspath_fragment
958 # so replace the frameworks/base/boot/hiddenapi file with the
959 # file that does not contain those flags.
960 shutil.move(tmp_flags_file, flags_file)
961
962 result.file_changes.append(
963 self.new_file_change(flags_file,
964 f"Removed '{self.bcpf}' specific entries"))
965
966 result.property_changes.append(
967 HiddenApiPropertyChange(
968 property_name=property_name,
969 values=[rel_bcpf_flags_file],
970 ))
971
972 # Make sure that the files are sorted.
973 self.run_command([
974 "tools/platform-compat/hiddenapi/sort_api.sh",
975 bcpf_flags_file,
976 ])
977
978 if bcpf_flags_file_exists:
979 desc = f"Added '{self.bcpf}' specific entries"
980 else:
981 desc = f"Created with '{self.bcpf}' specific entries"
982 result.file_changes.append(
983 self.new_file_change(bcpf_flags_file, desc))
984 else:
985 # There are no custom flags related to the
986 # bootclasspath_fragment so clean up the working files.
987 os.remove(tmp_flags_file)
988 if not bcpf_flags_file_exists:
989 os.remove(bcpf_flags_file)
990
Paul Duffin4dcf6592022-02-28 19:22:12 +0000991 def check_frameworks_base_boot_hidden_api_files(self, result):
992 hiddenapi_dir = os.path.join(self.top_dir,
993 "frameworks/base/boot/hiddenapi")
994 for basename in sorted(os.listdir(hiddenapi_dir)):
995 if not (basename.startswith("hiddenapi-") and
996 basename.endswith(".txt")):
997 continue
998
999 flags_file = os.path.join(hiddenapi_dir, basename)
1000
1001 logging.debug("Checking %s for flags related to %s", flags_file,
1002 self.bcpf)
1003
1004 # Map the file name in frameworks/base/boot/hiddenapi into a
1005 # slightly more meaningful name for use by the
1006 # bootclasspath_fragment.
1007 if basename == "hiddenapi-max-target-o.txt":
1008 basename = "hiddenapi-max-target-o-low-priority.txt"
1009 elif basename == "hiddenapi-max-target-r-loprio.txt":
1010 basename = "hiddenapi-max-target-r-low-priority.txt"
1011
1012 property_name = basename.removeprefix("hiddenapi-")
1013 property_name = property_name.removesuffix(".txt")
1014 property_name = property_name.replace("-", "_")
1015
1016 rel_bcpf_flags_file = f"hiddenapi/{basename}"
1017 bcpf_dir = self.module_info.module_path(self.bcpf)
1018 bcpf_flags_file = os.path.join(self.top_dir, bcpf_dir,
1019 rel_bcpf_flags_file)
1020
Paul Duffin26f19912022-03-28 16:09:27 +01001021 if self.fix:
1022 self.fix_hidden_api_flag_files(result, property_name,
1023 flags_file, rel_bcpf_flags_file,
1024 bcpf_flags_file)
1025 else:
1026 self.report_hidden_api_flag_file_changes(
1027 result, property_name, flags_file, rel_bcpf_flags_file,
1028 bcpf_flags_file)
Paul Duffin4dcf6592022-02-28 19:22:12 +00001029
Paul Duffindd97fd22022-02-28 19:22:12 +00001030 @staticmethod
1031 def split_package_comment(split_packages):
1032 if split_packages:
1033 return textwrap.dedent("""
1034 The following packages contain classes from other modules on the
1035 bootclasspath. That means that the hidden API flags for this
1036 module has to explicitly list every single class this module
1037 provides in that package to differentiate them from the classes
1038 provided by other modules. That can include private classes that
1039 are not part of the API.
1040 """).strip("\n")
1041
1042 return "This module does not contain any split packages."
1043
1044 @staticmethod
1045 def package_prefixes_comment():
1046 return textwrap.dedent("""
1047 The following packages and all their subpackages currently only
1048 contain classes from this bootclasspath_fragment. Listing a package
1049 here won't prevent other bootclasspath modules from adding classes
1050 in any of those packages but it will prevent them from adding those
1051 classes into an API surface, e.g. public, system, etc.. Doing so
1052 will result in a build failure due to inconsistent flags.
1053 """).strip("\n")
1054
1055 def analyze_hiddenapi_package_properties(self, result):
1056 split_packages, single_packages, package_prefixes = \
1057 self.compute_hiddenapi_package_properties()
1058
1059 # TODO(b/202154151): Find those classes in split packages that are not
1060 # part of an API, i.e. are an internal implementation class, and so
1061 # can, and should, be safely moved out of the split packages.
1062
1063 result.property_changes.append(
1064 HiddenApiPropertyChange(
1065 property_name="split_packages",
1066 values=split_packages,
1067 property_comment=self.split_package_comment(split_packages),
1068 action=PropertyChangeAction.REPLACE,
1069 ))
1070
1071 if split_packages:
Paul Duffinea836c22022-04-04 16:59:36 +01001072 self.report_dedent(f"""
1073 bootclasspath_fragment {self.bcpf} contains classes in packages
1074 that also contain classes provided by other bootclasspath
1075 modules. Those packages are called split packages. Split
1076 packages should be avoided where possible but are often
1077 unavoidable when modularizing existing code.
Paul Duffindd97fd22022-02-28 19:22:12 +00001078
Paul Duffinea836c22022-04-04 16:59:36 +01001079 The hidden api processing needs to know which packages are split
1080 (and conversely which are not) so that it can optimize the
1081 hidden API flags to remove unnecessary implementation details.
Paul Duffindd97fd22022-02-28 19:22:12 +00001082
Paul Duffinea836c22022-04-04 16:59:36 +01001083 By default (for backwards compatibility) the
1084 bootclasspath_fragment assumes that all packages are split
1085 unless one of the package_prefixes or split_packages properties
1086 are specified. While that is safe it is not optimal and can lead
1087 to unnecessary implementation details leaking into the hidden
1088 API flags. Adding an empty split_packages property allows the
1089 flags to be optimized and remove any unnecessary implementation
1090 details.
1091 """)
Paul Duffindd97fd22022-02-28 19:22:12 +00001092
1093 if single_packages:
1094 result.property_changes.append(
1095 HiddenApiPropertyChange(
1096 property_name="single_packages",
1097 values=single_packages,
1098 property_comment=textwrap.dedent("""
1099 The following packages currently only contain classes from
1100 this bootclasspath_fragment but some of their sub-packages
1101 contain classes from other bootclasspath modules. Packages
1102 should only be listed here when necessary for legacy
1103 purposes, new packages should match a package prefix.
Paul Duffinea836c22022-04-04 16:59:36 +01001104 """),
Paul Duffindd97fd22022-02-28 19:22:12 +00001105 action=PropertyChangeAction.REPLACE,
1106 ))
1107
1108 if package_prefixes:
1109 result.property_changes.append(
1110 HiddenApiPropertyChange(
1111 property_name="package_prefixes",
1112 values=package_prefixes,
1113 property_comment=self.package_prefixes_comment(),
1114 action=PropertyChangeAction.REPLACE,
1115 ))
1116
1117 def explain_how_to_check_signature_patterns(self):
1118 signature_patterns_files = self.find_bootclasspath_fragment_output_file(
1119 "signature-patterns.csv", required=False)
1120 if signature_patterns_files:
1121 signature_patterns_files = signature_patterns_files.removeprefix(
1122 self.top_dir)
1123
Paul Duffinea836c22022-04-04 16:59:36 +01001124 self.report_dedent(f"""
1125 The purpose of the hiddenapi split_packages and package_prefixes
1126 properties is to allow the removal of implementation details
1127 from the hidden API flags to reduce the coupling between sdk
1128 snapshots and the APEX runtime. It cannot eliminate that
1129 coupling completely though. Doing so may require changes to the
1130 code.
Paul Duffindd97fd22022-02-28 19:22:12 +00001131
Paul Duffinea836c22022-04-04 16:59:36 +01001132 This tool provides support for managing those properties but it
1133 cannot decide whether the set of package prefixes suggested is
1134 appropriate that needs the input of the developer.
Paul Duffindd97fd22022-02-28 19:22:12 +00001135
Paul Duffinea836c22022-04-04 16:59:36 +01001136 Please run the following command:
1137 m {signature_patterns_files}
Paul Duffindd97fd22022-02-28 19:22:12 +00001138
Paul Duffinea836c22022-04-04 16:59:36 +01001139 And then check the '{signature_patterns_files}' for any mention
1140 of implementation classes and packages (i.e. those
1141 classes/packages that do not contain any part of an API surface,
1142 including the hidden API). If they are found then the code
1143 should ideally be moved to a package unique to this module that
1144 is contained within a package that is part of an API surface.
Paul Duffindd97fd22022-02-28 19:22:12 +00001145
Paul Duffinea836c22022-04-04 16:59:36 +01001146 The format of the file is a list of patterns:
Paul Duffindd97fd22022-02-28 19:22:12 +00001147
Paul Duffinea836c22022-04-04 16:59:36 +01001148 * Patterns for split packages will list every class in that package.
Paul Duffindd97fd22022-02-28 19:22:12 +00001149
Paul Duffinea836c22022-04-04 16:59:36 +01001150 * Patterns for package prefixes will end with .../**.
Paul Duffindd97fd22022-02-28 19:22:12 +00001151
Paul Duffinea836c22022-04-04 16:59:36 +01001152 * Patterns for packages which are not split but cannot use a
1153 package prefix because there are sub-packages which are provided
1154 by another module will end with .../*.
1155 """)
Paul Duffindd97fd22022-02-28 19:22:12 +00001156
1157 def compute_hiddenapi_package_properties(self):
1158 trie = signature_trie()
1159 # Populate the trie with the classes that are provided by the
1160 # bootclasspath_fragment tagging them to make it clear where they
1161 # are from.
1162 sorted_classes = sorted(self.classes)
1163 for class_name in sorted_classes:
1164 trie.add(class_name + _FAKE_MEMBER, ClassProvider.BCPF)
1165
1166 monolithic_classes = set()
1167 abs_flags_file = os.path.join(self.top_dir, _FLAGS_FILE)
1168 with open(abs_flags_file, "r", encoding="utf8") as f:
1169 for line in iter(f.readline, ""):
1170 signature = self.line_to_signature(line)
1171 class_name = self.signature_to_class(signature)
1172 if (class_name not in monolithic_classes and
1173 class_name not in self.classes):
1174 trie.add(
1175 class_name + _FAKE_MEMBER,
1176 ClassProvider.OTHER,
1177 only_if_matches=True)
1178 monolithic_classes.add(class_name)
1179
1180 split_packages = []
1181 single_packages = []
1182 package_prefixes = []
1183 self.recurse_hiddenapi_packages_trie(trie, split_packages,
1184 single_packages, package_prefixes)
1185 return split_packages, single_packages, package_prefixes
1186
1187 def recurse_hiddenapi_packages_trie(self, node, split_packages,
1188 single_packages, package_prefixes):
1189 nodes = node.child_nodes()
1190 if nodes:
1191 for child in nodes:
1192 # Ignore any non-package nodes.
1193 if child.type != "package":
1194 continue
1195
1196 package = child.selector.replace("/", ".")
1197
1198 providers = set(child.get_matching_rows("**"))
1199 if not providers:
1200 # The package and all its sub packages contain no
1201 # classes. This should never happen.
1202 pass
1203 elif providers == {ClassProvider.BCPF}:
1204 # The package and all its sub packages only contain
1205 # classes provided by the bootclasspath_fragment.
1206 logging.debug("Package '%s.**' is not split", package)
1207 package_prefixes.append(package)
1208 # There is no point traversing into the sub packages.
1209 continue
1210 elif providers == {ClassProvider.OTHER}:
1211 # The package and all its sub packages contain no
1212 # classes provided by the bootclasspath_fragment.
1213 # There is no point traversing into the sub packages.
1214 logging.debug("Package '%s.**' contains no classes from %s",
1215 package, self.bcpf)
1216 continue
1217 elif ClassProvider.BCPF in providers:
1218 # The package and all its sub packages contain classes
1219 # provided by the bootclasspath_fragment and other
1220 # sources.
1221 logging.debug(
1222 "Package '%s.**' contains classes from "
1223 "%s and other sources", package, self.bcpf)
1224
1225 providers = set(child.get_matching_rows("*"))
1226 if not providers:
1227 # The package contains no classes.
1228 logging.debug("Package: %s contains no classes", package)
1229 elif providers == {ClassProvider.BCPF}:
1230 # The package only contains classes provided by the
1231 # bootclasspath_fragment.
1232 logging.debug("Package '%s.*' is not split", package)
1233 single_packages.append(package)
1234 elif providers == {ClassProvider.OTHER}:
1235 # The package contains no classes provided by the
1236 # bootclasspath_fragment. Child nodes make contain such
1237 # classes.
1238 logging.debug("Package '%s.*' contains no classes from %s",
1239 package, self.bcpf)
1240 elif ClassProvider.BCPF in providers:
1241 # The package contains classes provided by both the
1242 # bootclasspath_fragment and some other source.
1243 logging.debug("Package '%s.*' is split", package)
1244 split_packages.append(package)
1245
1246 self.recurse_hiddenapi_packages_trie(child, split_packages,
1247 single_packages,
1248 package_prefixes)
1249
Paul Duffin4dcf6592022-02-28 19:22:12 +00001250
1251def newline_stripping_iter(iterator):
1252 """Return an iterator over the iterator that strips trailing white space."""
1253 lines = iter(iterator, "")
1254 lines = (line.rstrip() for line in lines)
1255 return lines
1256
1257
1258def format_comment_as_text(text, indent):
1259 return "".join(
1260 [f"{line}\n" for line in format_comment_as_lines(text, indent)])
1261
1262
1263def format_comment_as_lines(text, indent):
1264 lines = textwrap.wrap(text.strip("\n"), width=77 - len(indent))
1265 lines = [f"{indent}// {line}" for line in lines]
1266 return lines
1267
1268
1269def log_stream_for_subprocess():
1270 stream = subprocess.DEVNULL
1271 for handler in logging.root.handlers:
1272 if handler.level == logging.DEBUG:
1273 if isinstance(handler, logging.StreamHandler):
1274 stream = handler.stream
1275 return stream
1276
1277
1278def main(argv):
1279 args_parser = argparse.ArgumentParser(
1280 description="Analyze a bootclasspath_fragment module.")
1281 args_parser.add_argument(
1282 "--bcpf",
1283 help="The bootclasspath_fragment module to analyze",
1284 required=True,
1285 )
1286 args_parser.add_argument(
1287 "--apex",
1288 help="The apex module to which the bootclasspath_fragment belongs. It "
1289 "is not strictly necessary at the moment but providing it will "
1290 "allow this script to give more useful messages and it may be"
1291 "required in future.",
1292 default="SPECIFY-APEX-OPTION")
1293 args_parser.add_argument(
1294 "--sdk",
1295 help="The sdk module to which the bootclasspath_fragment belongs. It "
1296 "is not strictly necessary at the moment but providing it will "
1297 "allow this script to give more useful messages and it may be"
1298 "required in future.",
1299 default="SPECIFY-SDK-OPTION")
Paul Duffin26f19912022-03-28 16:09:27 +01001300 args_parser.add_argument(
1301 "--fix",
1302 help="Attempt to fix any issues found automatically.",
1303 action="store_true",
1304 default=False)
Paul Duffin4dcf6592022-02-28 19:22:12 +00001305 args = args_parser.parse_args(argv[1:])
1306 top_dir = os.environ["ANDROID_BUILD_TOP"] + "/"
1307 out_dir = os.environ.get("OUT_DIR", os.path.join(top_dir, "out"))
1308 product_out_dir = os.environ.get("ANDROID_PRODUCT_OUT", top_dir)
1309 # Make product_out_dir relative to the top so it can be used as part of a
1310 # build target.
1311 product_out_dir = product_out_dir.removeprefix(top_dir)
1312 log_fd, abs_log_file = tempfile.mkstemp(
1313 suffix="_analyze_bcpf.log", text=True)
1314
1315 with os.fdopen(log_fd, "w") as log_file:
1316 # Set up debug logging to the log file.
1317 logging.basicConfig(
1318 level=logging.DEBUG,
1319 format="%(levelname)-8s %(message)s",
1320 stream=log_file)
1321
1322 # define a Handler which writes INFO messages or higher to the
1323 # sys.stdout with just the message.
1324 console = logging.StreamHandler()
1325 console.setLevel(logging.INFO)
1326 console.setFormatter(logging.Formatter("%(message)s"))
1327 # add the handler to the root logger
1328 logging.getLogger("").addHandler(console)
1329
1330 print(f"Writing log to {abs_log_file}")
1331 try:
1332 analyzer = BcpfAnalyzer(
Paul Duffin26f19912022-03-28 16:09:27 +01001333 tool_path=argv[0],
Paul Duffin4dcf6592022-02-28 19:22:12 +00001334 top_dir=top_dir,
1335 out_dir=out_dir,
1336 product_out_dir=product_out_dir,
1337 bcpf=args.bcpf,
1338 apex=args.apex,
1339 sdk=args.sdk,
Paul Duffin26f19912022-03-28 16:09:27 +01001340 fix=args.fix,
Paul Duffin4dcf6592022-02-28 19:22:12 +00001341 )
1342 analyzer.analyze()
1343 finally:
1344 print(f"Log written to {abs_log_file}")
1345
1346
1347if __name__ == "__main__":
1348 main(sys.argv)