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