blob: c648fe8388ae53e170ab98679bda66ee3cb1d205 [file] [log] [blame]
Paul Duffin4dcf6592022-02-28 19:22:12 +00001#!/usr/bin/env -S python -u
2#
3# Copyright (C) 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""Analyze bootclasspath_fragment usage."""
17import argparse
18import dataclasses
19import json
20import logging
21import os
22import re
23import shutil
24import subprocess
25import tempfile
26import textwrap
27import typing
28import sys
29
30_STUB_FLAGS_FILE = "out/soong/hiddenapi/hiddenapi-stub-flags.txt"
31
32_FLAGS_FILE = "out/soong/hiddenapi/hiddenapi-flags.csv"
33
34_INCONSISTENT_FLAGS = "ERROR: Hidden API flags are inconsistent:"
35
36
37class BuildOperation:
38
39 def __init__(self, popen):
40 self.popen = popen
41 self.returncode = None
42
43 def lines(self):
44 """Return an iterator over the lines output by the build operation.
45
46 The lines have had any trailing white space, including the newline
47 stripped.
48 """
49 return newline_stripping_iter(self.popen.stdout.readline)
50
51 def wait(self, *args, **kwargs):
52 self.popen.wait(*args, **kwargs)
53 self.returncode = self.popen.returncode
54
55
56@dataclasses.dataclass()
57class FlagDiffs:
58 """Encapsulates differences in flags reported by the build"""
59
60 # Map from member signature to the (module flags, monolithic flags)
61 diffs: typing.Dict[str, typing.Tuple[str, str]]
62
63
64@dataclasses.dataclass()
65class ModuleInfo:
66 """Provides access to the generated module-info.json file.
67
68 This is used to find the location of the file within which specific modules
69 are defined.
70 """
71
72 modules: typing.Dict[str, typing.Dict[str, typing.Any]]
73
74 @staticmethod
75 def load(filename):
76 with open(filename, "r", encoding="utf8") as f:
77 j = json.load(f)
78 return ModuleInfo(j)
79
80 def _module(self, module_name):
81 """Find module by name in module-info.json file"""
82 if module_name in self.modules:
83 return self.modules[module_name]
84
85 raise Exception(f"Module {module_name} could not be found")
86
87 def module_path(self, module_name):
88 module = self._module(module_name)
89 # The "path" is actually a list of paths, one for each class of module
90 # but as the modules are all created from bp files if a module does
91 # create multiple classes of make modules they should all have the same
92 # path.
93 paths = module["path"]
94 unique_paths = set(paths)
95 if len(unique_paths) != 1:
96 raise Exception(f"Expected module '{module_name}' to have a "
97 f"single unique path but found {unique_paths}")
98 return paths[0]
99
100
101@dataclasses.dataclass
102class FileChange:
103 path: str
104
105 description: str
106
107 def __lt__(self, other):
108 return self.path < other.path
109
110
111@dataclasses.dataclass
112class HiddenApiPropertyChange:
113
114 property_name: str
115
116 values: typing.List[str]
117
118 property_comment: str = ""
119
120 def snippet(self, indent):
121 snippet = "\n"
122 snippet += format_comment_as_text(self.property_comment, indent)
123 snippet += f"{indent}{self.property_name}: ["
124 if self.values:
125 snippet += "\n"
126 for value in self.values:
127 snippet += f'{indent} "{value}",\n'
128 snippet += f"{indent}"
129 snippet += "],\n"
130 return snippet
131
132
133@dataclasses.dataclass()
134class Result:
135 """Encapsulates the result of the analysis."""
136
137 # The diffs in the flags.
138 diffs: typing.Optional[FlagDiffs] = None
139
140 # The bootclasspath_fragment hidden API properties changes.
141 property_changes: typing.List[HiddenApiPropertyChange] = dataclasses.field(
142 default_factory=list)
143
144 # The list of file changes.
145 file_changes: typing.List[FileChange] = dataclasses.field(
146 default_factory=list)
147
148
149@dataclasses.dataclass()
150class BcpfAnalyzer:
151 # Directory pointed to by ANDROID_BUILD_OUT
152 top_dir: str
153
154 # Directory pointed to by OUT_DIR of {top_dir}/out if that is not set.
155 out_dir: str
156
157 # Directory pointed to by ANDROID_PRODUCT_OUT.
158 product_out_dir: str
159
160 # The name of the bootclasspath_fragment module.
161 bcpf: str
162
163 # The name of the apex module containing {bcpf}, only used for
164 # informational purposes.
165 apex: str
166
167 # The name of the sdk module containing {bcpf}, only used for
168 # informational purposes.
169 sdk: str
170
171 # All the signatures, loaded from all-flags.csv, initialized by
172 # load_all_flags().
173 _signatures: typing.Set[str] = dataclasses.field(default_factory=set)
174
175 # All the classes, loaded from all-flags.csv, initialized by
176 # load_all_flags().
177 _classes: typing.Set[str] = dataclasses.field(default_factory=set)
178
179 # Information loaded from module-info.json, initialized by
180 # load_module_info().
181 module_info: ModuleInfo = None
182
183 @staticmethod
184 def reformat_report_test(text):
185 return re.sub(r"(.)\n([^\s])", r"\1 \2", text)
186
187 def report(self, text, **kwargs):
188 # Concatenate lines that are not separated by a blank line together to
189 # eliminate formatting applied to the supplied text to adhere to python
190 # line length limitations.
191 text = self.reformat_report_test(text)
192 logging.info("%s", text, **kwargs)
193
194 def run_command(self, cmd, *args, **kwargs):
195 cmd_line = " ".join(cmd)
196 logging.debug("Running %s", cmd_line)
197 subprocess.run(
198 cmd,
199 *args,
200 check=True,
201 cwd=self.top_dir,
202 stderr=subprocess.STDOUT,
203 stdout=log_stream_for_subprocess(),
204 text=True,
205 **kwargs)
206
207 @property
208 def signatures(self):
209 if not self._signatures:
210 raise Exception("signatures has not been initialized")
211 return self._signatures
212
213 @property
214 def classes(self):
215 if not self._classes:
216 raise Exception("classes has not been initialized")
217 return self._classes
218
219 def load_all_flags(self):
220 all_flags = self.find_bootclasspath_fragment_output_file(
221 "all-flags.csv")
222
223 # Extract the set of signatures and a separate set of classes produced
224 # by the bootclasspath_fragment.
225 with open(all_flags, "r", encoding="utf8") as f:
226 for line in newline_stripping_iter(f.readline):
227 signature = self.line_to_signature(line)
228 self._signatures.add(signature)
229 class_name = self.signature_to_class(signature)
230 self._classes.add(class_name)
231
232 def load_module_info(self):
233 module_info_file = os.path.join(self.product_out_dir,
234 "module-info.json")
235 self.report(f"""
236Making sure that {module_info_file} is up to date.
237""")
238 output = self.build_file_read_output(module_info_file)
239 lines = output.lines()
240 for line in lines:
241 logging.debug("%s", line)
242 output.wait(timeout=10)
243 if output.returncode:
244 raise Exception(f"Error building {module_info_file}")
245 abs_module_info_file = os.path.join(self.top_dir, module_info_file)
246 self.module_info = ModuleInfo.load(abs_module_info_file)
247
248 @staticmethod
249 def line_to_signature(line):
250 return line.split(",")[0]
251
252 @staticmethod
253 def signature_to_class(signature):
254 return signature.split(";->")[0]
255
256 @staticmethod
257 def to_parent_package(pkg_or_class):
258 return pkg_or_class.rsplit("/", 1)[0]
259
260 def module_path(self, module_name):
261 return self.module_info.module_path(module_name)
262
263 def module_out_dir(self, module_name):
264 module_path = self.module_path(module_name)
265 return os.path.join(self.out_dir, "soong/.intermediates", module_path,
266 module_name)
267
268 def find_bootclasspath_fragment_output_file(self, basename):
269 # Find the output file of the bootclasspath_fragment with the specified
270 # base name.
271 found_file = ""
272 bcpf_out_dir = self.module_out_dir(self.bcpf)
273 for (dirpath, _, filenames) in os.walk(bcpf_out_dir):
274 for f in filenames:
275 if f == basename:
276 found_file = os.path.join(dirpath, f)
277 break
278 if not found_file:
279 raise Exception(f"Could not find {basename} in {bcpf_out_dir}")
280 return found_file
281
282 def analyze(self):
283 """Analyze a bootclasspath_fragment module.
284
285 Provides help in resolving any existing issues and provides
286 optimizations that can be applied.
287 """
288 self.report(f"Analyzing bootclasspath_fragment module {self.bcpf}")
289 self.report(f"""
290Run this tool to help initialize a bootclasspath_fragment module. Before you
291start make sure that:
292
2931. The current checkout is up to date.
294
2952. The environment has been initialized using lunch, e.g.
296 lunch aosp_arm64-userdebug
297
2983. You have added a bootclasspath_fragment module to the appropriate Android.bp
299file. Something like this:
300
301 bootclasspath_fragment {{
302 name: "{self.bcpf}",
303 contents: [
304 "...",
305 ],
306
307 // The bootclasspath_fragments that provide APIs on which this depends.
308 fragments: [
309 {{
310 apex: "com.android.art",
311 module: "art-bootclasspath-fragment",
312 }},
313 ],
314 }}
315
3164. You have added it to the platform_bootclasspath module in
317frameworks/base/boot/Android.bp. Something like this:
318
319 platform_bootclasspath {{
320 name: "platform-bootclasspath",
321 fragments: [
322 ...
323 {{
324 apex: "{self.apex}",
325 module: "{self.bcpf}",
326 }},
327 ],
328 }}
329
3305. You have added an sdk module. Something like this:
331
332 sdk {{
333 name: "{self.sdk}",
334 bootclasspath_fragments: ["{self.bcpf}"],
335 }}
336""")
337
338 # Make sure that the module-info.json file is up to date.
339 self.load_module_info()
340
341 self.report("""
342Cleaning potentially stale files.
343""")
344 # Remove the out/soong/hiddenapi files.
345 shutil.rmtree(f"{self.out_dir}/soong/hiddenapi", ignore_errors=True)
346
347 # Remove any bootclasspath_fragment output files.
348 shutil.rmtree(self.module_out_dir(self.bcpf), ignore_errors=True)
349
350 self.build_monolithic_stubs_flags()
351
352 result = Result()
353
354 self.build_monolithic_flags(result)
355
356 # If there were any changes that need to be made to the Android.bp
357 # file then report them.
358 if result.property_changes:
359 bcpf_dir = self.module_info.module_path(self.bcpf)
360 bcpf_bp_file = os.path.join(self.top_dir, bcpf_dir, "Android.bp")
361 hiddenapi_snippet = ""
362 for property_change in result.property_changes:
363 hiddenapi_snippet += property_change.snippet(" ")
364
365 # Remove leading and trailing blank lines.
366 hiddenapi_snippet = hiddenapi_snippet.strip("\n")
367
368 result.file_changes.append(
369 self.new_file_change(
370 bcpf_bp_file, f"""
371Add the following snippet into the {self.bcpf} bootclasspath_fragment module
372in the {bcpf_dir}/Android.bp file. If the hidden_api block already exists then
373merge these properties into it.
374
375 hidden_api: {{
376{hiddenapi_snippet}
377 }},
378"""))
379
380 if result.file_changes:
381 self.report("""
382The following modifications need to be made:""")
383 result.file_changes.sort()
384 for file_change in result.file_changes:
385 self.report(f"""
386 {file_change.path}
387 {file_change.description}
388""".lstrip("\n"))
389
390 def new_file_change(self, file, description):
391 return FileChange(
392 path=os.path.relpath(file, self.top_dir), description=description)
393
394 def check_inconsistent_flag_lines(self, significant, module_line,
395 monolithic_line, separator_line):
396 if not (module_line.startswith("< ") and
397 monolithic_line.startswith("> ") and not separator_line):
398 # Something went wrong.
399 self.report(f"""Invalid build output detected:
400 module_line: "{module_line}"
401 monolithic_line: "{monolithic_line}"
402 separator_line: "{separator_line}"
403""")
404 sys.exit(1)
405
406 if significant:
407 logging.debug("%s", module_line)
408 logging.debug("%s", monolithic_line)
409 logging.debug("%s", separator_line)
410
411 def scan_inconsistent_flags_report(self, lines):
412 """Scans a hidden API flags report
413
414 The hidden API inconsistent flags report which looks something like
415 this.
416
417 < out/soong/.intermediates/.../filtered-stub-flags.csv
418 > out/soong/hiddenapi/hiddenapi-stub-flags.txt
419
420 < Landroid/compat/Compatibility;->clearOverrides()V
421 > Landroid/compat/Compatibility;->clearOverrides()V,core-platform-api
422
423 """
424
425 # The basic format of an entry in the inconsistent flags report is:
426 # <module specific flag>
427 # <monolithic flag>
428 # <separator>
429 #
430 # Wrap the lines iterator in an iterator which returns a tuple
431 # consisting of the three separate lines.
432 triples = zip(lines, lines, lines)
433
434 module_line, monolithic_line, separator_line = next(triples)
435 significant = False
436 bcpf_dir = self.module_info.module_path(self.bcpf)
437 if os.path.join(bcpf_dir, self.bcpf) in module_line:
438 # These errors are related to the bcpf being analyzed so
439 # keep them.
440 significant = True
441 else:
442 self.report(f"Filtering out errors related to {module_line}")
443
444 self.check_inconsistent_flag_lines(significant, module_line,
445 monolithic_line, separator_line)
446
447 diffs = {}
448 for module_line, monolithic_line, separator_line in triples:
449 self.check_inconsistent_flag_lines(significant, module_line,
450 monolithic_line, "")
451
452 module_parts = module_line.removeprefix("< ").split(",")
453 module_signature = module_parts[0]
454 module_flags = module_parts[1:]
455
456 monolithic_parts = monolithic_line.removeprefix("> ").split(",")
457 monolithic_signature = monolithic_parts[0]
458 monolithic_flags = monolithic_parts[1:]
459
460 if module_signature != monolithic_signature:
461 # Something went wrong.
462 self.report(f"""Inconsistent signatures detected:
463 module_signature: "{module_signature}"
464 monolithic_signature: "{monolithic_signature}"
465""")
466 sys.exit(1)
467
468 diffs[module_signature] = (module_flags, monolithic_flags)
469
470 if separator_line:
471 # If the separator line is not blank then it is the end of the
472 # current report, and possibly the start of another.
473 return separator_line, diffs
474
475 return "", diffs
476
477 def build_file_read_output(self, filename):
478 # Make sure the filename is relative to top if possible as the build
479 # may be using relative paths as the target.
480 rel_filename = filename.removeprefix(self.top_dir)
481 cmd = ["build/soong/soong_ui.bash", "--make-mode", rel_filename]
482 cmd_line = " ".join(cmd)
483 logging.debug("%s", cmd_line)
484 # pylint: disable=consider-using-with
485 output = subprocess.Popen(
486 cmd,
487 cwd=self.top_dir,
488 stderr=subprocess.STDOUT,
489 stdout=subprocess.PIPE,
490 text=True,
491 )
492 return BuildOperation(popen=output)
493
494 def build_hiddenapi_flags(self, filename):
495 output = self.build_file_read_output(filename)
496
497 lines = output.lines()
498 diffs = None
499 for line in lines:
500 logging.debug("%s", line)
501 while line == _INCONSISTENT_FLAGS:
502 line, diffs = self.scan_inconsistent_flags_report(lines)
503
504 output.wait(timeout=10)
505 if output.returncode != 0:
506 logging.debug("Command failed with %s", output.returncode)
507 else:
508 logging.debug("Command succeeded")
509
510 return diffs
511
512 def build_monolithic_stubs_flags(self):
513 self.report(f"""
514Attempting to build {_STUB_FLAGS_FILE} to verify that the
515bootclasspath_fragment has the correct API stubs available...
516""")
517
518 # Build the hiddenapi-stubs-flags.txt file.
519 diffs = self.build_hiddenapi_flags(_STUB_FLAGS_FILE)
520 if diffs:
521 self.report(f"""
522There is a discrepancy between the stub API derived flags created by the
523bootclasspath_fragment and the platform_bootclasspath. See preceding error
524messages to see which flags are inconsistent. The inconsistencies can occur for
525a couple of reasons:
526
527If you are building against prebuilts of the Android SDK, e.g. by using
528TARGET_BUILD_APPS then the prebuilt versions of the APIs this
529bootclasspath_fragment depends upon are out of date and need updating. See
530go/update-prebuilts for help.
531
532Otherwise, this is happening because there are some stub APIs that are either
533provided by or used by the contents of the bootclasspath_fragment but which are
534not available to it. There are 4 ways to handle this:
535
5361. A java_sdk_library in the contents property will automatically make its stub
537 APIs available to the bootclasspath_fragment so nothing needs to be done.
538
5392. If the API provided by the bootclasspath_fragment is created by an api_only
540 java_sdk_library (or a java_library that compiles files generated by a
541 separate droidstubs module then it cannot be added to the contents and
542 instead must be added to the api.stubs property, e.g.
543
544 bootclasspath_fragment {{
545 name: "{self.bcpf}",
546 ...
547 api: {{
548 stubs: ["$MODULE-api-only"],"
549 }},
550 }}
551
5523. If the contents use APIs provided by another bootclasspath_fragment then
553 it needs to be added to the fragments property, e.g.
554
555 bootclasspath_fragment {{
556 name: "{self.bcpf}",
557 ...
558 // The bootclasspath_fragments that provide APIs on which this depends.
559 fragments: [
560 ...
561 {{
562 apex: "com.android.other",
563 module: "com.android.other-bootclasspath-fragment",
564 }},
565 ],
566 }}
567
5684. If the contents use APIs from a module that is not part of another
569 bootclasspath_fragment then it must be added to the additional_stubs
570 property, e.g.
571
572 bootclasspath_fragment {{
573 name: "{self.bcpf}",
574 ...
575 additional_stubs: ["android-non-updatable"],
576 }}
577
578 Like the api.stubs property these are typically java_sdk_library modules but
579 can be java_library too.
580
581 Note: The "android-non-updatable" is treated as if it was a java_sdk_library
582 which it is not at the moment but will be in future.
583""")
584
585 return diffs
586
587 def build_monolithic_flags(self, result):
588 self.report(f"""
589Attempting to build {_FLAGS_FILE} to verify that the
590bootclasspath_fragment has the correct hidden API flags...
591""")
592
593 # Build the hiddenapi-flags.csv file and extract any differences in
594 # the flags between this bootclasspath_fragment and the monolithic
595 # files.
596 result.diffs = self.build_hiddenapi_flags(_FLAGS_FILE)
597
598 # Load information from the bootclasspath_fragment's all-flags.csv file.
599 self.load_all_flags()
600
601 if result.diffs:
602 self.report(f"""
603There is a discrepancy between the hidden API flags created by the
604bootclasspath_fragment and the platform_bootclasspath. See preceding error
605messages to see which flags are inconsistent. The inconsistencies can occur for
606a couple of reasons:
607
608If you are building against prebuilts of this bootclasspath_fragment then the
609prebuilt version of the sdk snapshot (specifically the hidden API flag files)
610are inconsistent with the prebuilt version of the apex {self.apex}. Please
611ensure that they are both updated from the same build.
612
6131. There are custom hidden API flags specified in the one of the files in
614 frameworks/base/boot/hiddenapi which apply to the bootclasspath_fragment but
615 which are not supplied to the bootclasspath_fragment module.
616
6172. The bootclasspath_fragment specifies invalid "package_prefixes" or
618 "split_packages" properties that match packages and classes that it does not
619 provide.
620
621""")
622
623 # Check to see if there are any hiddenapi related properties that
624 # need to be added to the
625 self.report("""
626Checking custom hidden API flags....
627""")
628 self.check_frameworks_base_boot_hidden_api_files(result)
629
630 def report_hidden_api_flag_file_changes(self, result, property_name,
631 flags_file, rel_bcpf_flags_file,
632 bcpf_flags_file):
633 matched_signatures = set()
634 # Open the flags file to read the flags from.
635 with open(flags_file, "r", encoding="utf8") as f:
636 for signature in newline_stripping_iter(f.readline):
637 if signature in self.signatures:
638 # The signature is provided by the bootclasspath_fragment so
639 # it will need to be moved to the bootclasspath_fragment
640 # specific file.
641 matched_signatures.add(signature)
642
643 # If the bootclasspath_fragment specific flags file is not empty
644 # then it contains flags. That could either be new flags just moved
645 # from frameworks/base or previous contents of the file. In either
646 # case the file must not be removed.
647 if matched_signatures:
648 insert = textwrap.indent("\n".join(matched_signatures),
649 " ")
650 result.file_changes.append(
651 self.new_file_change(
652 flags_file, f"""Remove the following entries:
653{insert}
654"""))
655
656 result.file_changes.append(
657 self.new_file_change(
658 bcpf_flags_file, f"""Add the following entries:
659{insert}
660"""))
661
662 result.property_changes.append(
663 HiddenApiPropertyChange(
664 property_name=property_name,
665 values=[rel_bcpf_flags_file],
666 ))
667
668 def check_frameworks_base_boot_hidden_api_files(self, result):
669 hiddenapi_dir = os.path.join(self.top_dir,
670 "frameworks/base/boot/hiddenapi")
671 for basename in sorted(os.listdir(hiddenapi_dir)):
672 if not (basename.startswith("hiddenapi-") and
673 basename.endswith(".txt")):
674 continue
675
676 flags_file = os.path.join(hiddenapi_dir, basename)
677
678 logging.debug("Checking %s for flags related to %s", flags_file,
679 self.bcpf)
680
681 # Map the file name in frameworks/base/boot/hiddenapi into a
682 # slightly more meaningful name for use by the
683 # bootclasspath_fragment.
684 if basename == "hiddenapi-max-target-o.txt":
685 basename = "hiddenapi-max-target-o-low-priority.txt"
686 elif basename == "hiddenapi-max-target-r-loprio.txt":
687 basename = "hiddenapi-max-target-r-low-priority.txt"
688
689 property_name = basename.removeprefix("hiddenapi-")
690 property_name = property_name.removesuffix(".txt")
691 property_name = property_name.replace("-", "_")
692
693 rel_bcpf_flags_file = f"hiddenapi/{basename}"
694 bcpf_dir = self.module_info.module_path(self.bcpf)
695 bcpf_flags_file = os.path.join(self.top_dir, bcpf_dir,
696 rel_bcpf_flags_file)
697
698 self.report_hidden_api_flag_file_changes(result, property_name,
699 flags_file,
700 rel_bcpf_flags_file,
701 bcpf_flags_file)
702
703
704def newline_stripping_iter(iterator):
705 """Return an iterator over the iterator that strips trailing white space."""
706 lines = iter(iterator, "")
707 lines = (line.rstrip() for line in lines)
708 return lines
709
710
711def format_comment_as_text(text, indent):
712 return "".join(
713 [f"{line}\n" for line in format_comment_as_lines(text, indent)])
714
715
716def format_comment_as_lines(text, indent):
717 lines = textwrap.wrap(text.strip("\n"), width=77 - len(indent))
718 lines = [f"{indent}// {line}" for line in lines]
719 return lines
720
721
722def log_stream_for_subprocess():
723 stream = subprocess.DEVNULL
724 for handler in logging.root.handlers:
725 if handler.level == logging.DEBUG:
726 if isinstance(handler, logging.StreamHandler):
727 stream = handler.stream
728 return stream
729
730
731def main(argv):
732 args_parser = argparse.ArgumentParser(
733 description="Analyze a bootclasspath_fragment module.")
734 args_parser.add_argument(
735 "--bcpf",
736 help="The bootclasspath_fragment module to analyze",
737 required=True,
738 )
739 args_parser.add_argument(
740 "--apex",
741 help="The apex module to which the bootclasspath_fragment belongs. It "
742 "is not strictly necessary at the moment but providing it will "
743 "allow this script to give more useful messages and it may be"
744 "required in future.",
745 default="SPECIFY-APEX-OPTION")
746 args_parser.add_argument(
747 "--sdk",
748 help="The sdk module to which the bootclasspath_fragment belongs. It "
749 "is not strictly necessary at the moment but providing it will "
750 "allow this script to give more useful messages and it may be"
751 "required in future.",
752 default="SPECIFY-SDK-OPTION")
753 args = args_parser.parse_args(argv[1:])
754 top_dir = os.environ["ANDROID_BUILD_TOP"] + "/"
755 out_dir = os.environ.get("OUT_DIR", os.path.join(top_dir, "out"))
756 product_out_dir = os.environ.get("ANDROID_PRODUCT_OUT", top_dir)
757 # Make product_out_dir relative to the top so it can be used as part of a
758 # build target.
759 product_out_dir = product_out_dir.removeprefix(top_dir)
760 log_fd, abs_log_file = tempfile.mkstemp(
761 suffix="_analyze_bcpf.log", text=True)
762
763 with os.fdopen(log_fd, "w") as log_file:
764 # Set up debug logging to the log file.
765 logging.basicConfig(
766 level=logging.DEBUG,
767 format="%(levelname)-8s %(message)s",
768 stream=log_file)
769
770 # define a Handler which writes INFO messages or higher to the
771 # sys.stdout with just the message.
772 console = logging.StreamHandler()
773 console.setLevel(logging.INFO)
774 console.setFormatter(logging.Formatter("%(message)s"))
775 # add the handler to the root logger
776 logging.getLogger("").addHandler(console)
777
778 print(f"Writing log to {abs_log_file}")
779 try:
780 analyzer = BcpfAnalyzer(
781 top_dir=top_dir,
782 out_dir=out_dir,
783 product_out_dir=product_out_dir,
784 bcpf=args.bcpf,
785 apex=args.apex,
786 sdk=args.sdk,
787 )
788 analyzer.analyze()
789 finally:
790 print(f"Log written to {abs_log_file}")
791
792
793if __name__ == "__main__":
794 main(sys.argv)