• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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