1#!/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 19import enum 20import json 21import logging 22import os 23import re 24import shutil 25import subprocess 26import tempfile 27import textwrap 28import typing 29from enum import Enum 30 31import sys 32 33from signature_trie import signature_trie 34 35_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 106def 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 133@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 143class 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 153@dataclasses.dataclass 154class HiddenApiPropertyChange: 155 156 property_name: str 157 158 values: typing.List[str] 159 160 property_comment: str = "" 161 162 # The action that indicates how this change is applied. 163 action: PropertyChangeAction = PropertyChangeAction.APPEND 164 165 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 177 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] 181 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}") 204 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): 219 """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 235 # 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 285 # 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 301 # 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 320 321@dataclasses.dataclass() 322class PackagePropertyReason: 323 """Provides the reasons why a package was added to a specific property. 324 325 A split package is one that contains classes from the bootclasspath_fragment 326 and other bootclasspath modules. So, for a split package this contains the 327 corresponding lists of classes. 328 329 A single package is one that contains classes sub-packages from the 330 For a split package this contains a list of classes in that package that are 331 provided by the bootclasspath_fragment and a list of classes 332 """ 333 334 # The list of classes/sub-packages that is provided by the 335 # bootclasspath_fragment. 336 bcpf: typing.List[str] 337 338 # The list of classes/sub-packages that is provided by other modules on the 339 # bootclasspath. 340 other: typing.List[str] 341 342 343@dataclasses.dataclass() 344class Result: 345 """Encapsulates the result of the analysis.""" 346 347 # The diffs in the flags. 348 diffs: typing.Optional[FlagDiffs] = None 349 350 # A map from package name to the reason why it belongs in the 351 # split_packages property. 352 split_packages: typing.Dict[str, PackagePropertyReason] = dataclasses.field( 353 default_factory=dict) 354 355 # A map from package name to the reason why it belongs in the 356 # single_packages property. 357 single_packages: typing.Dict[str, 358 PackagePropertyReason] = dataclasses.field( 359 default_factory=dict) 360 361 # The list of packages to add to the package_prefixes property. 362 package_prefixes: typing.List[str] = dataclasses.field(default_factory=list) 363 364 # The bootclasspath_fragment hidden API properties changes. 365 property_changes: typing.List[HiddenApiPropertyChange] = dataclasses.field( 366 default_factory=list) 367 368 # The list of file changes. 369 file_changes: typing.List[FileChange] = dataclasses.field( 370 default_factory=list) 371 372 373class ClassProvider(enum.Enum): 374 """The source of a class found during the hidden API processing""" 375 BCPF = "bcpf" 376 OTHER = "other" 377 378 379# A fake member to use when using the signature trie to compute the package 380# properties from hidden API flags. This is needed because while that 381# computation only cares about classes the trie expects a class to be an 382# interior node but without a member it makes the class a leaf node. That causes 383# problems when analyzing inner classes as the outer class is a leaf node for 384# its own entry but is used as an interior node for inner classes. 385_FAKE_MEMBER = ";->fake()V" 386 387 388@dataclasses.dataclass() 389class BcpfAnalyzer: 390 # Path to this tool. 391 tool_path: str 392 393 # Directory pointed to by ANDROID_BUILD_OUT 394 top_dir: str 395 396 # Directory pointed to by OUT_DIR of {top_dir}/out if that is not set. 397 out_dir: str 398 399 # Directory pointed to by ANDROID_PRODUCT_OUT. 400 product_out_dir: str 401 402 # The name of the bootclasspath_fragment module. 403 bcpf: str 404 405 # The name of the apex module containing {bcpf}, only used for 406 # informational purposes. 407 apex: str 408 409 # The name of the sdk module containing {bcpf}, only used for 410 # informational purposes. 411 sdk: str 412 413 # If true then this will attempt to automatically fix any issues that are 414 # found. 415 fix: bool = False 416 417 # All the signatures, loaded from all-flags.csv, initialized by 418 # load_all_flags(). 419 _signatures: typing.Set[str] = dataclasses.field(default_factory=set) 420 421 # All the classes, loaded from all-flags.csv, initialized by 422 # load_all_flags(). 423 _classes: typing.Set[str] = dataclasses.field(default_factory=set) 424 425 # Information loaded from module-info.json, initialized by 426 # load_module_info(). 427 module_info: ModuleInfo = None 428 429 @staticmethod 430 def reformat_report_test(text): 431 return re.sub(r"(.)\n([^\s])", r"\1 \2", text) 432 433 def report(self, text="", **kwargs): 434 # Concatenate lines that are not separated by a blank line together to 435 # eliminate formatting applied to the supplied text to adhere to python 436 # line length limitations. 437 text = self.reformat_report_test(text) 438 logging.info("%s", text, **kwargs) 439 440 def report_dedent(self, text, **kwargs): 441 text = textwrap.dedent(text) 442 self.report(text, **kwargs) 443 444 def run_command(self, cmd, *args, **kwargs): 445 cmd_line = " ".join(cmd) 446 logging.debug("Running %s", cmd_line) 447 subprocess.run( 448 cmd, 449 *args, 450 check=True, 451 cwd=self.top_dir, 452 stderr=subprocess.STDOUT, 453 stdout=log_stream_for_subprocess(), 454 text=True, 455 **kwargs) 456 457 @property 458 def signatures(self): 459 if not self._signatures: 460 raise Exception("signatures has not been initialized") 461 return self._signatures 462 463 @property 464 def classes(self): 465 if not self._classes: 466 raise Exception("classes has not been initialized") 467 return self._classes 468 469 def load_all_flags(self): 470 all_flags = self.find_bootclasspath_fragment_output_file( 471 "all-flags.csv") 472 473 # Extract the set of signatures and a separate set of classes produced 474 # by the bootclasspath_fragment. 475 with open(all_flags, "r", encoding="utf8") as f: 476 for line in newline_stripping_iter(f.readline): 477 signature = self.line_to_signature(line) 478 self._signatures.add(signature) 479 class_name = self.signature_to_class(signature) 480 self._classes.add(class_name) 481 482 def load_module_info(self): 483 module_info_file = os.path.join(self.product_out_dir, 484 "module-info.json") 485 self.report(f"\nMaking sure that {module_info_file} is up to date.\n") 486 output = self.build_file_read_output(module_info_file) 487 lines = output.lines() 488 for line in lines: 489 logging.debug("%s", line) 490 output.wait(timeout=10) 491 if output.returncode: 492 raise Exception(f"Error building {module_info_file}") 493 abs_module_info_file = os.path.join(self.top_dir, module_info_file) 494 self.module_info = ModuleInfo.load(abs_module_info_file) 495 496 @staticmethod 497 def line_to_signature(line): 498 return line.split(",")[0] 499 500 @staticmethod 501 def signature_to_class(signature): 502 return signature.split(";->")[0] 503 504 @staticmethod 505 def to_parent_package(pkg_or_class): 506 return pkg_or_class.rsplit("/", 1)[0] 507 508 def module_path(self, module_name): 509 return self.module_info.module_path(module_name) 510 511 def module_out_dir(self, module_name): 512 module_path = self.module_path(module_name) 513 return os.path.join(self.out_dir, "soong/.intermediates", module_path, 514 module_name) 515 516 def find_bootclasspath_fragment_output_file(self, basename, required=True): 517 # Find the output file of the bootclasspath_fragment with the specified 518 # base name. 519 found_file = "" 520 bcpf_out_dir = self.module_out_dir(self.bcpf) 521 for (dirpath, _, filenames) in os.walk(bcpf_out_dir): 522 for f in filenames: 523 if f == basename: 524 found_file = os.path.join(dirpath, f) 525 break 526 if not found_file and required: 527 raise Exception(f"Could not find {basename} in {bcpf_out_dir}") 528 return found_file 529 530 def analyze(self): 531 """Analyze a bootclasspath_fragment module. 532 533 Provides help in resolving any existing issues and provides 534 optimizations that can be applied. 535 """ 536 self.report(f"Analyzing bootclasspath_fragment module {self.bcpf}") 537 self.report_dedent(f""" 538 Run this tool to help initialize a bootclasspath_fragment module. 539 Before you start make sure that: 540 541 1. The current checkout is up to date. 542 543 2. The environment has been initialized using lunch, e.g. 544 lunch aosp_arm64-userdebug 545 546 3. You have added a bootclasspath_fragment module to the appropriate 547 Android.bp file. Something like this: 548 549 bootclasspath_fragment {{ 550 name: "{self.bcpf}", 551 contents: [ 552 "...", 553 ], 554 555 // The bootclasspath_fragments that provide APIs on which this 556 // depends. 557 fragments: [ 558 {{ 559 apex: "com.android.art", 560 module: "art-bootclasspath-fragment", 561 }}, 562 ], 563 }} 564 565 4. You have added it to the platform_bootclasspath module in 566 frameworks/base/boot/Android.bp. Something like this: 567 568 platform_bootclasspath {{ 569 name: "platform-bootclasspath", 570 fragments: [ 571 ... 572 {{ 573 apex: "{self.apex}", 574 module: "{self.bcpf}", 575 }}, 576 ], 577 }} 578 579 5. You have added an sdk module. Something like this: 580 581 sdk {{ 582 name: "{self.sdk}", 583 bootclasspath_fragments: ["{self.bcpf}"], 584 }} 585 """) 586 587 # Make sure that the module-info.json file is up to date. 588 self.load_module_info() 589 590 self.report_dedent(""" 591 Cleaning potentially stale files. 592 """) 593 # Remove the out/soong/hiddenapi files. 594 shutil.rmtree(f"{self.out_dir}/soong/hiddenapi", ignore_errors=True) 595 596 # Remove any bootclasspath_fragment output files. 597 shutil.rmtree(self.module_out_dir(self.bcpf), ignore_errors=True) 598 599 self.build_monolithic_stubs_flags() 600 601 result = Result() 602 603 self.build_monolithic_flags(result) 604 self.analyze_hiddenapi_package_properties(result) 605 self.explain_how_to_check_signature_patterns() 606 607 # If there were any changes that need to be made to the Android.bp 608 # file then either apply or report them. 609 if result.property_changes: 610 bcpf_dir = self.module_info.module_path(self.bcpf) 611 bcpf_bp_file = os.path.join(self.top_dir, bcpf_dir, "Android.bp") 612 if self.fix: 613 tool_dir = os.path.dirname(self.tool_path) 614 bpmodify_path = os.path.join(tool_dir, "bpmodify") 615 bpmodify_runner = BpModifyRunner(bpmodify_path) 616 for property_change in result.property_changes: 617 property_change.fix_bp_file(bcpf_bp_file, self.bcpf, 618 bpmodify_runner) 619 620 result.file_changes.append( 621 self.new_file_change( 622 bcpf_bp_file, 623 f"Updated hidden_api properties of '{self.bcpf}'")) 624 625 else: 626 hiddenapi_snippet = "" 627 for property_change in result.property_changes: 628 hiddenapi_snippet += property_change.snippet(" ") 629 630 # Remove leading and trailing blank lines. 631 hiddenapi_snippet = hiddenapi_snippet.strip("\n") 632 633 result.file_changes.append( 634 self.new_file_change( 635 bcpf_bp_file, f""" 636Add the following snippet into the {self.bcpf} bootclasspath_fragment module 637in the {bcpf_dir}/Android.bp file. If the hidden_api block already exists then 638merge these properties into it. 639 640 hidden_api: {{ 641{hiddenapi_snippet} 642 }}, 643""")) 644 645 if result.file_changes: 646 if self.fix: 647 file_change_message = textwrap.dedent(""" 648 The following files were modified by this script: 649 """) 650 else: 651 file_change_message = textwrap.dedent(""" 652 The following modifications need to be made: 653 """) 654 655 self.report(file_change_message) 656 result.file_changes.sort() 657 for file_change in result.file_changes: 658 self.report(f" {file_change.path}") 659 self.report(f" {file_change.description}") 660 self.report() 661 662 if not self.fix: 663 self.report_dedent(""" 664 Run the command again with the --fix option to automatically 665 make the above changes. 666 """.lstrip("\n")) 667 668 def new_file_change(self, file, description): 669 return FileChange( 670 path=os.path.relpath(file, self.top_dir), description=description) 671 672 def check_inconsistent_flag_lines(self, significant, module_line, 673 monolithic_line, separator_line): 674 if not (module_line.startswith("< ") and 675 monolithic_line.startswith("> ") and not separator_line): 676 # Something went wrong. 677 self.report("Invalid build output detected:") 678 self.report(f" module_line: '{module_line}'") 679 self.report(f" monolithic_line: '{monolithic_line}'") 680 self.report(f" separator_line: '{separator_line}'") 681 sys.exit(1) 682 683 if significant: 684 logging.debug("%s", module_line) 685 logging.debug("%s", monolithic_line) 686 logging.debug("%s", separator_line) 687 688 def scan_inconsistent_flags_report(self, lines): 689 """Scans a hidden API flags report 690 691 The hidden API inconsistent flags report which looks something like 692 this. 693 694 < out/soong/.intermediates/.../filtered-stub-flags.csv 695 > out/soong/hiddenapi/hiddenapi-stub-flags.txt 696 697 < Landroid/compat/Compatibility;->clearOverrides()V 698 > Landroid/compat/Compatibility;->clearOverrides()V,core-platform-api 699 700 """ 701 702 # The basic format of an entry in the inconsistent flags report is: 703 # <module specific flag> 704 # <monolithic flag> 705 # <separator> 706 # 707 # Wrap the lines iterator in an iterator which returns a tuple 708 # consisting of the three separate lines. 709 triples = zip(lines, lines, lines) 710 711 module_line, monolithic_line, separator_line = next(triples) 712 significant = False 713 bcpf_dir = self.module_info.module_path(self.bcpf) 714 if os.path.join(bcpf_dir, self.bcpf) in module_line: 715 # These errors are related to the bcpf being analyzed so 716 # keep them. 717 significant = True 718 else: 719 self.report(f"Filtering out errors related to {module_line}") 720 721 self.check_inconsistent_flag_lines(significant, module_line, 722 monolithic_line, separator_line) 723 724 diffs = {} 725 for module_line, monolithic_line, separator_line in triples: 726 self.check_inconsistent_flag_lines(significant, module_line, 727 monolithic_line, "") 728 729 module_parts = module_line.removeprefix("< ").split(",") 730 module_signature = module_parts[0] 731 module_flags = module_parts[1:] 732 733 monolithic_parts = monolithic_line.removeprefix("> ").split(",") 734 monolithic_signature = monolithic_parts[0] 735 monolithic_flags = monolithic_parts[1:] 736 737 if module_signature != monolithic_signature: 738 # Something went wrong. 739 self.report("Inconsistent signatures detected:") 740 self.report(f" module_signature: '{module_signature}'") 741 self.report(f" monolithic_signature: '{monolithic_signature}'") 742 sys.exit(1) 743 744 diffs[module_signature] = (module_flags, monolithic_flags) 745 746 if separator_line: 747 # If the separator line is not blank then it is the end of the 748 # current report, and possibly the start of another. 749 return separator_line, diffs 750 751 return "", diffs 752 753 def build_file_read_output(self, filename): 754 # Make sure the filename is relative to top if possible as the build 755 # may be using relative paths as the target. 756 rel_filename = filename.removeprefix(self.top_dir) 757 cmd = ["build/soong/soong_ui.bash", "--make-mode", rel_filename] 758 cmd_line = " ".join(cmd) 759 logging.debug("%s", cmd_line) 760 # pylint: disable=consider-using-with 761 output = subprocess.Popen( 762 cmd, 763 cwd=self.top_dir, 764 stderr=subprocess.STDOUT, 765 stdout=subprocess.PIPE, 766 text=True, 767 ) 768 return BuildOperation(popen=output) 769 770 def build_hiddenapi_flags(self, filename): 771 output = self.build_file_read_output(filename) 772 773 lines = output.lines() 774 diffs = None 775 for line in lines: 776 logging.debug("%s", line) 777 while line == _INCONSISTENT_FLAGS: 778 line, diffs = self.scan_inconsistent_flags_report(lines) 779 780 output.wait(timeout=10) 781 if output.returncode != 0: 782 logging.debug("Command failed with %s", output.returncode) 783 else: 784 logging.debug("Command succeeded") 785 786 return diffs 787 788 def build_monolithic_stubs_flags(self): 789 self.report_dedent(f""" 790 Attempting to build {_STUB_FLAGS_FILE} to verify that the 791 bootclasspath_fragment has the correct API stubs available... 792 """) 793 794 # Build the hiddenapi-stubs-flags.txt file. 795 diffs = self.build_hiddenapi_flags(_STUB_FLAGS_FILE) 796 if diffs: 797 self.report_dedent(f""" 798 There is a discrepancy between the stub API derived flags 799 created by the bootclasspath_fragment and the 800 platform_bootclasspath. See preceding error messages to see 801 which flags are inconsistent. The inconsistencies can occur for 802 a couple of reasons: 803 804 If you are building against prebuilts of the Android SDK, e.g. 805 by using TARGET_BUILD_APPS then the prebuilt versions of the 806 APIs this bootclasspath_fragment depends upon are out of date 807 and need updating. See go/update-prebuilts for help. 808 809 Otherwise, this is happening because there are some stub APIs 810 that are either provided by or used by the contents of the 811 bootclasspath_fragment but which are not available to it. There 812 are 4 ways to handle this: 813 814 1. A java_sdk_library in the contents property will 815 automatically make its stub APIs available to the 816 bootclasspath_fragment so nothing needs to be done. 817 818 2. If the API provided by the bootclasspath_fragment is created 819 by an api_only java_sdk_library (or a java_library that compiles 820 files generated by a separate droidstubs module then it cannot 821 be added to the contents and instead must be added to the 822 api.stubs property, e.g. 823 824 bootclasspath_fragment {{ 825 name: "{self.bcpf}", 826 ... 827 api: {{ 828 stubs: ["$MODULE-api-only"]," 829 }}, 830 }} 831 832 3. If the contents use APIs provided by another 833 bootclasspath_fragment then it needs to be added to the 834 fragments property, e.g. 835 836 bootclasspath_fragment {{ 837 name: "{self.bcpf}", 838 ... 839 // The bootclasspath_fragments that provide APIs on which this depends. 840 fragments: [ 841 ... 842 {{ 843 apex: "com.android.other", 844 module: "com.android.other-bootclasspath-fragment", 845 }}, 846 ], 847 }} 848 849 4. If the contents use APIs from a module that is not part of 850 another bootclasspath_fragment then it must be added to the 851 additional_stubs property, e.g. 852 853 bootclasspath_fragment {{ 854 name: "{self.bcpf}", 855 ... 856 additional_stubs: ["android-non-updatable"], 857 }} 858 859 Like the api.stubs property these are typically 860 java_sdk_library modules but can be java_library too. 861 862 Note: The "android-non-updatable" is treated as if it was a 863 java_sdk_library which it is not at the moment but will be in 864 future. 865 """) 866 867 return diffs 868 869 def build_monolithic_flags(self, result): 870 self.report_dedent(f""" 871 Attempting to build {_FLAGS_FILE} to verify that the 872 bootclasspath_fragment has the correct hidden API flags... 873 """) 874 875 # Build the hiddenapi-flags.csv file and extract any differences in 876 # the flags between this bootclasspath_fragment and the monolithic 877 # files. 878 result.diffs = self.build_hiddenapi_flags(_FLAGS_FILE) 879 880 # Load information from the bootclasspath_fragment's all-flags.csv file. 881 self.load_all_flags() 882 883 if result.diffs: 884 self.report_dedent(f""" 885 There is a discrepancy between the hidden API flags created by 886 the bootclasspath_fragment and the platform_bootclasspath. See 887 preceding error messages to see which flags are inconsistent. 888 The inconsistencies can occur for a couple of reasons: 889 890 If you are building against prebuilts of this 891 bootclasspath_fragment then the prebuilt version of the sdk 892 snapshot (specifically the hidden API flag files) are 893 inconsistent with the prebuilt version of the apex {self.apex}. 894 Please ensure that they are both updated from the same build. 895 896 1. There are custom hidden API flags specified in the one of the 897 files in frameworks/base/boot/hiddenapi which apply to the 898 bootclasspath_fragment but which are not supplied to the 899 bootclasspath_fragment module. 900 901 2. The bootclasspath_fragment specifies invalid 902 "split_packages", "single_packages" and/of "package_prefixes" 903 properties that match packages and classes that it does not 904 provide. 905 """) 906 907 # Check to see if there are any hiddenapi related properties that 908 # need to be added to the 909 self.report_dedent(""" 910 Checking custom hidden API flags.... 911 """) 912 self.check_frameworks_base_boot_hidden_api_files(result) 913 914 def report_hidden_api_flag_file_changes(self, result, property_name, 915 flags_file, rel_bcpf_flags_file, 916 bcpf_flags_file): 917 matched_signatures = set() 918 # Open the flags file to read the flags from. 919 with open(flags_file, "r", encoding="utf8") as f: 920 for signature in newline_stripping_iter(f.readline): 921 if signature in self.signatures: 922 # The signature is provided by the bootclasspath_fragment so 923 # it will need to be moved to the bootclasspath_fragment 924 # specific file. 925 matched_signatures.add(signature) 926 927 # If the bootclasspath_fragment specific flags file is not empty 928 # then it contains flags. That could either be new flags just moved 929 # from frameworks/base or previous contents of the file. In either 930 # case the file must not be removed. 931 if matched_signatures: 932 insert = textwrap.indent("\n".join(matched_signatures), 933 " ") 934 result.file_changes.append( 935 self.new_file_change( 936 flags_file, f"""Remove the following entries: 937{insert} 938""")) 939 940 result.file_changes.append( 941 self.new_file_change( 942 bcpf_flags_file, f"""Add the following entries: 943{insert} 944""")) 945 946 result.property_changes.append( 947 HiddenApiPropertyChange( 948 property_name=property_name, 949 values=[rel_bcpf_flags_file], 950 )) 951 952 def fix_hidden_api_flag_files(self, result, property_name, flags_file, 953 rel_bcpf_flags_file, bcpf_flags_file): 954 # Read the file in frameworks/base/boot/hiddenapi/<file> copy any 955 # flags that relate to the bootclasspath_fragment into a local 956 # file in the hiddenapi subdirectory. 957 tmp_flags_file = flags_file + ".tmp" 958 959 # Make sure the directory containing the bootclasspath_fragment specific 960 # hidden api flags exists. 961 os.makedirs(os.path.dirname(bcpf_flags_file), exist_ok=True) 962 963 bcpf_flags_file_exists = os.path.exists(bcpf_flags_file) 964 965 matched_signatures = set() 966 # Open the flags file to read the flags from. 967 with open(flags_file, "r", encoding="utf8") as f: 968 # Open a temporary file to write the flags (minus any removed 969 # flags). 970 with open(tmp_flags_file, "w", encoding="utf8") as t: 971 # Open the bootclasspath_fragment file for append just in 972 # case it already exists. 973 with open(bcpf_flags_file, "a", encoding="utf8") as b: 974 for line in iter(f.readline, ""): 975 signature = line.rstrip() 976 if signature in self.signatures: 977 # The signature is provided by the 978 # bootclasspath_fragment so write it to the new 979 # bootclasspath_fragment specific file. 980 print(line, file=b, end="") 981 matched_signatures.add(signature) 982 else: 983 # The signature is NOT provided by the 984 # bootclasspath_fragment. Copy it to the new 985 # monolithic file. 986 print(line, file=t, end="") 987 988 # If the bootclasspath_fragment specific flags file is not empty 989 # then it contains flags. That could either be new flags just moved 990 # from frameworks/base or previous contents of the file. In either 991 # case the file must not be removed. 992 if matched_signatures: 993 # There are custom flags related to the bootclasspath_fragment 994 # so replace the frameworks/base/boot/hiddenapi file with the 995 # file that does not contain those flags. 996 shutil.move(tmp_flags_file, flags_file) 997 998 result.file_changes.append( 999 self.new_file_change(flags_file, 1000 f"Removed '{self.bcpf}' specific entries")) 1001 1002 result.property_changes.append( 1003 HiddenApiPropertyChange( 1004 property_name=property_name, 1005 values=[rel_bcpf_flags_file], 1006 )) 1007 1008 # Make sure that the files are sorted. 1009 self.run_command([ 1010 "tools/platform-compat/hiddenapi/sort_api.sh", 1011 bcpf_flags_file, 1012 ]) 1013 1014 if bcpf_flags_file_exists: 1015 desc = f"Added '{self.bcpf}' specific entries" 1016 else: 1017 desc = f"Created with '{self.bcpf}' specific entries" 1018 result.file_changes.append( 1019 self.new_file_change(bcpf_flags_file, desc)) 1020 else: 1021 # There are no custom flags related to the 1022 # bootclasspath_fragment so clean up the working files. 1023 os.remove(tmp_flags_file) 1024 if not bcpf_flags_file_exists: 1025 os.remove(bcpf_flags_file) 1026 1027 def check_frameworks_base_boot_hidden_api_files(self, result): 1028 hiddenapi_dir = os.path.join(self.top_dir, 1029 "frameworks/base/boot/hiddenapi") 1030 for basename in sorted(os.listdir(hiddenapi_dir)): 1031 if not (basename.startswith("hiddenapi-") and 1032 basename.endswith(".txt")): 1033 continue 1034 1035 flags_file = os.path.join(hiddenapi_dir, basename) 1036 1037 logging.debug("Checking %s for flags related to %s", flags_file, 1038 self.bcpf) 1039 1040 # Map the file name in frameworks/base/boot/hiddenapi into a 1041 # slightly more meaningful name for use by the 1042 # bootclasspath_fragment. 1043 if basename == "hiddenapi-max-target-o.txt": 1044 basename = "hiddenapi-max-target-o-low-priority.txt" 1045 elif basename == "hiddenapi-max-target-r-loprio.txt": 1046 basename = "hiddenapi-max-target-r-low-priority.txt" 1047 1048 property_name = basename.removeprefix("hiddenapi-") 1049 property_name = property_name.removesuffix(".txt") 1050 property_name = property_name.replace("-", "_") 1051 1052 rel_bcpf_flags_file = f"hiddenapi/{basename}" 1053 bcpf_dir = self.module_info.module_path(self.bcpf) 1054 bcpf_flags_file = os.path.join(self.top_dir, bcpf_dir, 1055 rel_bcpf_flags_file) 1056 1057 if self.fix: 1058 self.fix_hidden_api_flag_files(result, property_name, 1059 flags_file, rel_bcpf_flags_file, 1060 bcpf_flags_file) 1061 else: 1062 self.report_hidden_api_flag_file_changes( 1063 result, property_name, flags_file, rel_bcpf_flags_file, 1064 bcpf_flags_file) 1065 1066 @staticmethod 1067 def split_package_comment(split_packages): 1068 if split_packages: 1069 return textwrap.dedent(""" 1070 The following packages contain classes from other modules on the 1071 bootclasspath. That means that the hidden API flags for this 1072 module has to explicitly list every single class this module 1073 provides in that package to differentiate them from the classes 1074 provided by other modules. That can include private classes that 1075 are not part of the API. 1076 """).strip("\n") 1077 1078 return "This module does not contain any split packages." 1079 1080 @staticmethod 1081 def package_prefixes_comment(): 1082 return textwrap.dedent(""" 1083 The following packages and all their subpackages currently only 1084 contain classes from this bootclasspath_fragment. Listing a package 1085 here won't prevent other bootclasspath modules from adding classes 1086 in any of those packages but it will prevent them from adding those 1087 classes into an API surface, e.g. public, system, etc.. Doing so 1088 will result in a build failure due to inconsistent flags. 1089 """).strip("\n") 1090 1091 def analyze_hiddenapi_package_properties(self, result): 1092 self.compute_hiddenapi_package_properties(result) 1093 1094 def indent_lines(lines): 1095 return "\n".join([f" {cls}" for cls in lines]) 1096 1097 # TODO(b/202154151): Find those classes in split packages that are not 1098 # part of an API, i.e. are an internal implementation class, and so 1099 # can, and should, be safely moved out of the split packages. 1100 1101 split_packages = result.split_packages.keys() 1102 result.property_changes.append( 1103 HiddenApiPropertyChange( 1104 property_name="split_packages", 1105 values=split_packages, 1106 property_comment=self.split_package_comment(split_packages), 1107 action=PropertyChangeAction.REPLACE, 1108 )) 1109 1110 if split_packages: 1111 self.report_dedent(f""" 1112 bootclasspath_fragment {self.bcpf} contains classes in packages 1113 that also contain classes provided by other bootclasspath 1114 modules. Those packages are called split packages. Split 1115 packages should be avoided where possible but are often 1116 unavoidable when modularizing existing code. 1117 1118 The hidden api processing needs to know which packages are split 1119 (and conversely which are not) so that it can optimize the 1120 hidden API flags to remove unnecessary implementation details. 1121 1122 By default (for backwards compatibility) the 1123 bootclasspath_fragment assumes that all packages are split 1124 unless one of the package_prefixes or split_packages properties 1125 are specified. While that is safe it is not optimal and can lead 1126 to unnecessary implementation details leaking into the hidden 1127 API flags. Adding an empty split_packages property allows the 1128 flags to be optimized and remove any unnecessary implementation 1129 details. 1130 """) 1131 1132 for package in split_packages: 1133 reason = result.split_packages[package] 1134 self.report(f""" 1135 Package {package} is split because while this bootclasspath_fragment 1136 provides the following classes: 1137{indent_lines(reason.bcpf)} 1138 1139 Other module(s) on the bootclasspath provides the following classes in 1140 that package: 1141{indent_lines(reason.other)} 1142""") 1143 1144 single_packages = result.single_packages.keys() 1145 if single_packages: 1146 result.property_changes.append( 1147 HiddenApiPropertyChange( 1148 property_name="single_packages", 1149 values=single_packages, 1150 property_comment=textwrap.dedent(""" 1151 The following packages currently only contain classes from 1152 this bootclasspath_fragment but some of their sub-packages 1153 contain classes from other bootclasspath modules. Packages 1154 should only be listed here when necessary for legacy 1155 purposes, new packages should match a package prefix. 1156 """), 1157 action=PropertyChangeAction.REPLACE, 1158 )) 1159 1160 self.report_dedent(f""" 1161 bootclasspath_fragment {self.bcpf} contains classes from 1162 packages that has sub-packages which contain classes provided by 1163 other bootclasspath modules. Those packages are called single 1164 packages. Single packages should be avoided where possible but 1165 are often unavoidable when modularizing existing code. 1166 1167 Because some sub-packages contains classes from other 1168 bootclasspath modules it is not possible to use the package as a 1169 package prefix as that treats the package and all its 1170 sub-packages as being provided by this module. 1171 """) 1172 for package in single_packages: 1173 reason = result.single_packages[package] 1174 self.report(f""" 1175 Package {package} is not a package prefix because while this 1176 bootclasspath_fragment provides the following sub-packages: 1177{indent_lines(reason.bcpf)} 1178 1179 Other module(s) on the bootclasspath provide the following sub-packages: 1180{indent_lines(reason.other)} 1181""") 1182 1183 package_prefixes = result.package_prefixes 1184 if package_prefixes: 1185 result.property_changes.append( 1186 HiddenApiPropertyChange( 1187 property_name="package_prefixes", 1188 values=package_prefixes, 1189 property_comment=self.package_prefixes_comment(), 1190 action=PropertyChangeAction.REPLACE, 1191 )) 1192 1193 def explain_how_to_check_signature_patterns(self): 1194 signature_patterns_files = self.find_bootclasspath_fragment_output_file( 1195 "signature-patterns.csv", required=False) 1196 if signature_patterns_files: 1197 signature_patterns_files = signature_patterns_files.removeprefix( 1198 self.top_dir) 1199 1200 self.report_dedent(f""" 1201 The purpose of the hiddenapi split_packages and package_prefixes 1202 properties is to allow the removal of implementation details 1203 from the hidden API flags to reduce the coupling between sdk 1204 snapshots and the APEX runtime. It cannot eliminate that 1205 coupling completely though. Doing so may require changes to the 1206 code. 1207 1208 This tool provides support for managing those properties but it 1209 cannot decide whether the set of package prefixes suggested is 1210 appropriate that needs the input of the developer. 1211 1212 Please run the following command: 1213 m {signature_patterns_files} 1214 1215 And then check the '{signature_patterns_files}' for any mention 1216 of implementation classes and packages (i.e. those 1217 classes/packages that do not contain any part of an API surface, 1218 including the hidden API). If they are found then the code 1219 should ideally be moved to a package unique to this module that 1220 is contained within a package that is part of an API surface. 1221 1222 The format of the file is a list of patterns: 1223 1224 * Patterns for split packages will list every class in that package. 1225 1226 * Patterns for package prefixes will end with .../**. 1227 1228 * Patterns for packages which are not split but cannot use a 1229 package prefix because there are sub-packages which are provided 1230 by another module will end with .../*. 1231 """) 1232 1233 def compute_hiddenapi_package_properties(self, result): 1234 trie = signature_trie() 1235 # Populate the trie with the classes that are provided by the 1236 # bootclasspath_fragment tagging them to make it clear where they 1237 # are from. 1238 sorted_classes = sorted(self.classes) 1239 for class_name in sorted_classes: 1240 trie.add(class_name + _FAKE_MEMBER, ClassProvider.BCPF) 1241 1242 # Now the same for monolithic classes. 1243 monolithic_classes = set() 1244 abs_flags_file = os.path.join(self.top_dir, _FLAGS_FILE) 1245 with open(abs_flags_file, "r", encoding="utf8") as f: 1246 for line in iter(f.readline, ""): 1247 signature = self.line_to_signature(line) 1248 class_name = self.signature_to_class(signature) 1249 if (class_name not in monolithic_classes and 1250 class_name not in self.classes): 1251 trie.add( 1252 class_name + _FAKE_MEMBER, 1253 ClassProvider.OTHER, 1254 only_if_matches=True) 1255 monolithic_classes.add(class_name) 1256 1257 self.recurse_hiddenapi_packages_trie(trie, result) 1258 1259 @staticmethod 1260 def selector_to_java_reference(node): 1261 return node.selector.replace("/", ".") 1262 1263 @staticmethod 1264 def determine_reason_for_single_package(node): 1265 bcpf_packages = [] 1266 other_packages = [] 1267 1268 def recurse(n): 1269 if n.type != "package": 1270 return 1271 1272 providers = n.get_matching_rows("*") 1273 package_ref = BcpfAnalyzer.selector_to_java_reference(n) 1274 if ClassProvider.BCPF in providers: 1275 bcpf_packages.append(package_ref) 1276 else: 1277 other_packages.append(package_ref) 1278 1279 children = n.child_nodes() 1280 if children: 1281 for child in children: 1282 recurse(child) 1283 1284 recurse(node) 1285 return PackagePropertyReason(bcpf=bcpf_packages, other=other_packages) 1286 1287 @staticmethod 1288 def determine_reason_for_split_package(node): 1289 bcpf_classes = [] 1290 other_classes = [] 1291 for child in node.child_nodes(): 1292 if child.type != "class": 1293 continue 1294 1295 providers = child.values(lambda _: True) 1296 class_ref = BcpfAnalyzer.selector_to_java_reference(child) 1297 if ClassProvider.BCPF in providers: 1298 bcpf_classes.append(class_ref) 1299 else: 1300 other_classes.append(class_ref) 1301 1302 return PackagePropertyReason(bcpf=bcpf_classes, other=other_classes) 1303 1304 def recurse_hiddenapi_packages_trie(self, node, result): 1305 nodes = node.child_nodes() 1306 if nodes: 1307 for child in nodes: 1308 # Ignore any non-package nodes. 1309 if child.type != "package": 1310 continue 1311 1312 package = self.selector_to_java_reference(child) 1313 1314 providers = set(child.get_matching_rows("**")) 1315 if not providers: 1316 # The package and all its sub packages contain no 1317 # classes. This should never happen. 1318 pass 1319 elif providers == {ClassProvider.BCPF}: 1320 # The package and all its sub packages only contain 1321 # classes provided by the bootclasspath_fragment. 1322 logging.debug("Package '%s.**' is not split", package) 1323 result.package_prefixes.append(package) 1324 # There is no point traversing into the sub packages. 1325 continue 1326 elif providers == {ClassProvider.OTHER}: 1327 # The package and all its sub packages contain no 1328 # classes provided by the bootclasspath_fragment. 1329 # There is no point traversing into the sub packages. 1330 logging.debug("Package '%s.**' contains no classes from %s", 1331 package, self.bcpf) 1332 continue 1333 elif ClassProvider.BCPF in providers: 1334 # The package and all its sub packages contain classes 1335 # provided by the bootclasspath_fragment and other 1336 # sources. 1337 logging.debug( 1338 "Package '%s.**' contains classes from " 1339 "%s and other sources", package, self.bcpf) 1340 1341 providers = set(child.get_matching_rows("*")) 1342 if not providers: 1343 # The package contains no classes. 1344 logging.debug("Package: %s contains no classes", package) 1345 elif providers == {ClassProvider.BCPF}: 1346 # The package only contains classes provided by the 1347 # bootclasspath_fragment. 1348 logging.debug( 1349 "Package '%s.*' is not split but does have " 1350 "sub-packages from other modules", package) 1351 1352 # Partition the sub-packages into those that are provided by 1353 # this bootclasspath_fragment and those provided by other 1354 # modules. They can be used to explain the reason for the 1355 # single package to developers. 1356 reason = self.determine_reason_for_single_package(child) 1357 result.single_packages[package] = reason 1358 1359 elif providers == {ClassProvider.OTHER}: 1360 # The package contains no classes provided by the 1361 # bootclasspath_fragment. Child nodes make contain such 1362 # classes. 1363 logging.debug("Package '%s.*' contains no classes from %s", 1364 package, self.bcpf) 1365 elif ClassProvider.BCPF in providers: 1366 # The package contains classes provided by both the 1367 # bootclasspath_fragment and some other source. 1368 logging.debug("Package '%s.*' is split", package) 1369 1370 # Partition the classes in this split package into those 1371 # that come from this bootclasspath_fragment and those that 1372 # come from other modules. That can be used to explain the 1373 # reason for the split package to developers. 1374 reason = self.determine_reason_for_split_package(child) 1375 result.split_packages[package] = reason 1376 1377 self.recurse_hiddenapi_packages_trie(child, result) 1378 1379 1380def newline_stripping_iter(iterator): 1381 """Return an iterator over the iterator that strips trailing white space.""" 1382 lines = iter(iterator, "") 1383 lines = (line.rstrip() for line in lines) 1384 return lines 1385 1386 1387def format_comment_as_text(text, indent): 1388 return "".join( 1389 [f"{line}\n" for line in format_comment_as_lines(text, indent)]) 1390 1391 1392def format_comment_as_lines(text, indent): 1393 lines = textwrap.wrap(text.strip("\n"), width=77 - len(indent)) 1394 lines = [f"{indent}// {line}" for line in lines] 1395 return lines 1396 1397 1398def log_stream_for_subprocess(): 1399 stream = subprocess.DEVNULL 1400 for handler in logging.root.handlers: 1401 if handler.level == logging.DEBUG: 1402 if isinstance(handler, logging.StreamHandler): 1403 stream = handler.stream 1404 return stream 1405 1406 1407def main(argv): 1408 args_parser = argparse.ArgumentParser( 1409 description="Analyze a bootclasspath_fragment module.") 1410 args_parser.add_argument( 1411 "--bcpf", 1412 help="The bootclasspath_fragment module to analyze", 1413 required=True, 1414 ) 1415 args_parser.add_argument( 1416 "--apex", 1417 help="The apex module to which the bootclasspath_fragment belongs. It " 1418 "is not strictly necessary at the moment but providing it will " 1419 "allow this script to give more useful messages and it may be" 1420 "required in future.", 1421 default="SPECIFY-APEX-OPTION") 1422 args_parser.add_argument( 1423 "--sdk", 1424 help="The sdk module to which the bootclasspath_fragment belongs. It " 1425 "is not strictly necessary at the moment but providing it will " 1426 "allow this script to give more useful messages and it may be" 1427 "required in future.", 1428 default="SPECIFY-SDK-OPTION") 1429 args_parser.add_argument( 1430 "--fix", 1431 help="Attempt to fix any issues found automatically.", 1432 action="store_true", 1433 default=False) 1434 args = args_parser.parse_args(argv[1:]) 1435 top_dir = os.environ["ANDROID_BUILD_TOP"] + "/" 1436 out_dir = os.environ.get("OUT_DIR", os.path.join(top_dir, "out")) 1437 product_out_dir = os.environ.get("ANDROID_PRODUCT_OUT", top_dir) 1438 # Make product_out_dir relative to the top so it can be used as part of a 1439 # build target. 1440 product_out_dir = product_out_dir.removeprefix(top_dir) 1441 log_fd, abs_log_file = tempfile.mkstemp( 1442 suffix="_analyze_bcpf.log", text=True) 1443 1444 with os.fdopen(log_fd, "w") as log_file: 1445 # Set up debug logging to the log file. 1446 logging.basicConfig( 1447 level=logging.DEBUG, 1448 format="%(levelname)-8s %(message)s", 1449 stream=log_file) 1450 1451 # define a Handler which writes INFO messages or higher to the 1452 # sys.stdout with just the message. 1453 console = logging.StreamHandler() 1454 console.setLevel(logging.INFO) 1455 console.setFormatter(logging.Formatter("%(message)s")) 1456 # add the handler to the root logger 1457 logging.getLogger("").addHandler(console) 1458 1459 print(f"Writing log to {abs_log_file}") 1460 try: 1461 analyzer = BcpfAnalyzer( 1462 tool_path=argv[0], 1463 top_dir=top_dir, 1464 out_dir=out_dir, 1465 product_out_dir=product_out_dir, 1466 bcpf=args.bcpf, 1467 apex=args.apex, 1468 sdk=args.sdk, 1469 fix=args.fix, 1470 ) 1471 analyzer.analyze() 1472 finally: 1473 print(f"Log written to {abs_log_file}") 1474 1475 1476if __name__ == "__main__": 1477 main(sys.argv) 1478