blob: b5700fb8a079dfc2fdc427047777c59a53a83073 [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
Paul Duffin26f19912022-03-28 16:09:27 +0100101def extract_indent(line):
102 return re.match(r"([ \t]*)", line).group(1)
103
104
105_SPECIAL_PLACEHOLDER: str = "SPECIAL_PLACEHOLDER"
106
107
108@dataclasses.dataclass
109class BpModifyRunner:
110
111 bpmodify_path: str
112
113 def add_values_to_property(self, property_name, values, module_name,
114 bp_file):
115 cmd = [
116 self.bpmodify_path, "-a", values, "-property", property_name, "-m",
117 module_name, "-w", bp_file, bp_file
118 ]
119
120 logging.debug(" ".join(cmd))
121 subprocess.run(
122 cmd,
123 stderr=subprocess.STDOUT,
124 stdout=log_stream_for_subprocess(),
125 check=True)
126
127
Paul Duffin4dcf6592022-02-28 19:22:12 +0000128@dataclasses.dataclass
129class FileChange:
130 path: str
131
132 description: str
133
134 def __lt__(self, other):
135 return self.path < other.path
136
137
138@dataclasses.dataclass
139class HiddenApiPropertyChange:
140
141 property_name: str
142
143 values: typing.List[str]
144
145 property_comment: str = ""
146
147 def snippet(self, indent):
148 snippet = "\n"
149 snippet += format_comment_as_text(self.property_comment, indent)
150 snippet += f"{indent}{self.property_name}: ["
151 if self.values:
152 snippet += "\n"
153 for value in self.values:
154 snippet += f'{indent} "{value}",\n'
155 snippet += f"{indent}"
156 snippet += "],\n"
157 return snippet
158
Paul Duffin26f19912022-03-28 16:09:27 +0100159 def fix_bp_file(self, bcpf_bp_file, bcpf, bpmodify_runner: BpModifyRunner):
160 # Add an additional placeholder value to identify the modification that
161 # bpmodify makes.
162 bpmodify_values = [_SPECIAL_PLACEHOLDER]
163 bpmodify_values.extend(self.values)
164
165 packages = ",".join(bpmodify_values)
166 bpmodify_runner.add_values_to_property(
167 f"hidden_api.{self.property_name}", packages, bcpf, bcpf_bp_file)
168
169 with open(bcpf_bp_file, "r", encoding="utf8") as tio:
170 lines = tio.readlines()
171 lines = [line.rstrip("\n") for line in lines]
172
173 if self.fixup_bpmodify_changes(bcpf_bp_file, lines):
174 with open(bcpf_bp_file, "w", encoding="utf8") as tio:
175 for line in lines:
176 print(line, file=tio)
177
178 def fixup_bpmodify_changes(self, bcpf_bp_file, lines):
179 # Find the line containing the placeholder that has been inserted.
180 place_holder_index = -1
181 for i, line in enumerate(lines):
182 if _SPECIAL_PLACEHOLDER in line:
183 place_holder_index = i
184 break
185 if place_holder_index == -1:
186 logging.debug("Could not find %s in %s", _SPECIAL_PLACEHOLDER,
187 bcpf_bp_file)
188 return False
189
190 # Remove the place holder. Do this before inserting the comment as that
191 # would change the location of the place holder in the list.
192 place_holder_line = lines[place_holder_index]
193 if place_holder_line.endswith("],"):
194 place_holder_line = place_holder_line.replace(
195 f'"{_SPECIAL_PLACEHOLDER}"', "")
196 lines[place_holder_index] = place_holder_line
197 else:
198 del lines[place_holder_index]
199
200 # Scan forward to the end of the property block to remove a blank line
201 # that bpmodify inserts.
202 end_property_array_index = -1
203 for i in range(place_holder_index, len(lines)):
204 line = lines[i]
205 if line.endswith("],"):
206 end_property_array_index = i
207 break
208 if end_property_array_index == -1:
209 logging.debug("Could not find end of property array in %s",
210 bcpf_bp_file)
211 return False
212
213 # If bdmodify inserted a blank line afterwards then remove it.
214 if (not lines[end_property_array_index + 1] and
215 lines[end_property_array_index + 2].endswith("},")):
216 del lines[end_property_array_index + 1]
217
218 # Scan back to find the preceding property line.
219 property_line_index = -1
220 for i in range(place_holder_index, 0, -1):
221 line = lines[i]
222 if line.lstrip().startswith(f"{self.property_name}: ["):
223 property_line_index = i
224 break
225 if property_line_index == -1:
226 logging.debug("Could not find property line in %s", bcpf_bp_file)
227 return False
228
229 # Only insert a comment if the property does not already have a comment.
230 line_preceding_property = lines[(property_line_index - 1)]
231 if (self.property_comment and
232 not re.match("([ \t]+)// ", line_preceding_property)):
233 # Extract the indent from the property line and use it to format the
234 # comment.
235 indent = extract_indent(lines[property_line_index])
236 comment_lines = format_comment_as_lines(self.property_comment,
237 indent)
238
239 # If the line before the comment is not blank then insert an extra
240 # blank line at the beginning of the comment.
241 if line_preceding_property:
242 comment_lines.insert(0, "")
243
244 # Insert the comment before the property.
245 lines[property_line_index:property_line_index] = comment_lines
246 return True
247
Paul Duffin4dcf6592022-02-28 19:22:12 +0000248
249@dataclasses.dataclass()
250class Result:
251 """Encapsulates the result of the analysis."""
252
253 # The diffs in the flags.
254 diffs: typing.Optional[FlagDiffs] = None
255
256 # The bootclasspath_fragment hidden API properties changes.
257 property_changes: typing.List[HiddenApiPropertyChange] = dataclasses.field(
258 default_factory=list)
259
260 # The list of file changes.
261 file_changes: typing.List[FileChange] = dataclasses.field(
262 default_factory=list)
263
264
265@dataclasses.dataclass()
266class BcpfAnalyzer:
Paul Duffin26f19912022-03-28 16:09:27 +0100267 # Path to this tool.
268 tool_path: str
269
Paul Duffin4dcf6592022-02-28 19:22:12 +0000270 # Directory pointed to by ANDROID_BUILD_OUT
271 top_dir: str
272
273 # Directory pointed to by OUT_DIR of {top_dir}/out if that is not set.
274 out_dir: str
275
276 # Directory pointed to by ANDROID_PRODUCT_OUT.
277 product_out_dir: str
278
279 # The name of the bootclasspath_fragment module.
280 bcpf: str
281
282 # The name of the apex module containing {bcpf}, only used for
283 # informational purposes.
284 apex: str
285
286 # The name of the sdk module containing {bcpf}, only used for
287 # informational purposes.
288 sdk: str
289
Paul Duffin26f19912022-03-28 16:09:27 +0100290 # If true then this will attempt to automatically fix any issues that are
291 # found.
292 fix: bool = False
293
Paul Duffin4dcf6592022-02-28 19:22:12 +0000294 # All the signatures, loaded from all-flags.csv, initialized by
295 # load_all_flags().
296 _signatures: typing.Set[str] = dataclasses.field(default_factory=set)
297
298 # All the classes, loaded from all-flags.csv, initialized by
299 # load_all_flags().
300 _classes: typing.Set[str] = dataclasses.field(default_factory=set)
301
302 # Information loaded from module-info.json, initialized by
303 # load_module_info().
304 module_info: ModuleInfo = None
305
306 @staticmethod
307 def reformat_report_test(text):
308 return re.sub(r"(.)\n([^\s])", r"\1 \2", text)
309
310 def report(self, text, **kwargs):
311 # Concatenate lines that are not separated by a blank line together to
312 # eliminate formatting applied to the supplied text to adhere to python
313 # line length limitations.
314 text = self.reformat_report_test(text)
315 logging.info("%s", text, **kwargs)
316
317 def run_command(self, cmd, *args, **kwargs):
318 cmd_line = " ".join(cmd)
319 logging.debug("Running %s", cmd_line)
320 subprocess.run(
321 cmd,
322 *args,
323 check=True,
324 cwd=self.top_dir,
325 stderr=subprocess.STDOUT,
326 stdout=log_stream_for_subprocess(),
327 text=True,
328 **kwargs)
329
330 @property
331 def signatures(self):
332 if not self._signatures:
333 raise Exception("signatures has not been initialized")
334 return self._signatures
335
336 @property
337 def classes(self):
338 if not self._classes:
339 raise Exception("classes has not been initialized")
340 return self._classes
341
342 def load_all_flags(self):
343 all_flags = self.find_bootclasspath_fragment_output_file(
344 "all-flags.csv")
345
346 # Extract the set of signatures and a separate set of classes produced
347 # by the bootclasspath_fragment.
348 with open(all_flags, "r", encoding="utf8") as f:
349 for line in newline_stripping_iter(f.readline):
350 signature = self.line_to_signature(line)
351 self._signatures.add(signature)
352 class_name = self.signature_to_class(signature)
353 self._classes.add(class_name)
354
355 def load_module_info(self):
356 module_info_file = os.path.join(self.product_out_dir,
357 "module-info.json")
358 self.report(f"""
359Making sure that {module_info_file} is up to date.
360""")
361 output = self.build_file_read_output(module_info_file)
362 lines = output.lines()
363 for line in lines:
364 logging.debug("%s", line)
365 output.wait(timeout=10)
366 if output.returncode:
367 raise Exception(f"Error building {module_info_file}")
368 abs_module_info_file = os.path.join(self.top_dir, module_info_file)
369 self.module_info = ModuleInfo.load(abs_module_info_file)
370
371 @staticmethod
372 def line_to_signature(line):
373 return line.split(",")[0]
374
375 @staticmethod
376 def signature_to_class(signature):
377 return signature.split(";->")[0]
378
379 @staticmethod
380 def to_parent_package(pkg_or_class):
381 return pkg_or_class.rsplit("/", 1)[0]
382
383 def module_path(self, module_name):
384 return self.module_info.module_path(module_name)
385
386 def module_out_dir(self, module_name):
387 module_path = self.module_path(module_name)
388 return os.path.join(self.out_dir, "soong/.intermediates", module_path,
389 module_name)
390
391 def find_bootclasspath_fragment_output_file(self, basename):
392 # Find the output file of the bootclasspath_fragment with the specified
393 # base name.
394 found_file = ""
395 bcpf_out_dir = self.module_out_dir(self.bcpf)
396 for (dirpath, _, filenames) in os.walk(bcpf_out_dir):
397 for f in filenames:
398 if f == basename:
399 found_file = os.path.join(dirpath, f)
400 break
401 if not found_file:
402 raise Exception(f"Could not find {basename} in {bcpf_out_dir}")
403 return found_file
404
405 def analyze(self):
406 """Analyze a bootclasspath_fragment module.
407
408 Provides help in resolving any existing issues and provides
409 optimizations that can be applied.
410 """
411 self.report(f"Analyzing bootclasspath_fragment module {self.bcpf}")
412 self.report(f"""
413Run this tool to help initialize a bootclasspath_fragment module. Before you
414start make sure that:
415
4161. The current checkout is up to date.
417
4182. The environment has been initialized using lunch, e.g.
419 lunch aosp_arm64-userdebug
420
4213. You have added a bootclasspath_fragment module to the appropriate Android.bp
422file. Something like this:
423
424 bootclasspath_fragment {{
425 name: "{self.bcpf}",
426 contents: [
427 "...",
428 ],
429
430 // The bootclasspath_fragments that provide APIs on which this depends.
431 fragments: [
432 {{
433 apex: "com.android.art",
434 module: "art-bootclasspath-fragment",
435 }},
436 ],
437 }}
438
4394. You have added it to the platform_bootclasspath module in
440frameworks/base/boot/Android.bp. Something like this:
441
442 platform_bootclasspath {{
443 name: "platform-bootclasspath",
444 fragments: [
445 ...
446 {{
447 apex: "{self.apex}",
448 module: "{self.bcpf}",
449 }},
450 ],
451 }}
452
4535. You have added an sdk module. Something like this:
454
455 sdk {{
456 name: "{self.sdk}",
457 bootclasspath_fragments: ["{self.bcpf}"],
458 }}
459""")
460
461 # Make sure that the module-info.json file is up to date.
462 self.load_module_info()
463
464 self.report("""
465Cleaning potentially stale files.
466""")
467 # Remove the out/soong/hiddenapi files.
468 shutil.rmtree(f"{self.out_dir}/soong/hiddenapi", ignore_errors=True)
469
470 # Remove any bootclasspath_fragment output files.
471 shutil.rmtree(self.module_out_dir(self.bcpf), ignore_errors=True)
472
473 self.build_monolithic_stubs_flags()
474
475 result = Result()
476
477 self.build_monolithic_flags(result)
478
479 # If there were any changes that need to be made to the Android.bp
Paul Duffin26f19912022-03-28 16:09:27 +0100480 # file then either apply or report them.
Paul Duffin4dcf6592022-02-28 19:22:12 +0000481 if result.property_changes:
482 bcpf_dir = self.module_info.module_path(self.bcpf)
483 bcpf_bp_file = os.path.join(self.top_dir, bcpf_dir, "Android.bp")
Paul Duffin26f19912022-03-28 16:09:27 +0100484 if self.fix:
485 tool_dir = os.path.dirname(self.tool_path)
486 bpmodify_path = os.path.join(tool_dir, "bpmodify")
487 bpmodify_runner = BpModifyRunner(bpmodify_path)
488 for property_change in result.property_changes:
489 property_change.fix_bp_file(bcpf_bp_file, self.bcpf,
490 bpmodify_runner)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000491
Paul Duffin26f19912022-03-28 16:09:27 +0100492 result.file_changes.append(
493 self.new_file_change(
494 bcpf_bp_file,
495 f"Updated hidden_api properties of '{self.bcpf}'"))
Paul Duffin4dcf6592022-02-28 19:22:12 +0000496
Paul Duffin26f19912022-03-28 16:09:27 +0100497 else:
498 hiddenapi_snippet = ""
499 for property_change in result.property_changes:
500 hiddenapi_snippet += property_change.snippet(" ")
501
502 # Remove leading and trailing blank lines.
503 hiddenapi_snippet = hiddenapi_snippet.strip("\n")
504
505 result.file_changes.append(
506 self.new_file_change(
507 bcpf_bp_file, f"""
Paul Duffin4dcf6592022-02-28 19:22:12 +0000508Add the following snippet into the {self.bcpf} bootclasspath_fragment module
509in the {bcpf_dir}/Android.bp file. If the hidden_api block already exists then
510merge these properties into it.
511
512 hidden_api: {{
513{hiddenapi_snippet}
514 }},
515"""))
516
517 if result.file_changes:
Paul Duffin26f19912022-03-28 16:09:27 +0100518 if self.fix:
519 file_change_message = """
520The following files were modified by this script:"""
521 else:
522 file_change_message = """
523The following modifications need to be made:"""
524
525 self.report(f"""
526{file_change_message}""")
Paul Duffin4dcf6592022-02-28 19:22:12 +0000527 result.file_changes.sort()
528 for file_change in result.file_changes:
529 self.report(f"""
530 {file_change.path}
531 {file_change.description}
532""".lstrip("\n"))
533
Paul Duffin26f19912022-03-28 16:09:27 +0100534 if not self.fix:
535 self.report("""
536Run the command again with the --fix option to automatically make the above
537changes.
538""".lstrip())
539
Paul Duffin4dcf6592022-02-28 19:22:12 +0000540 def new_file_change(self, file, description):
541 return FileChange(
542 path=os.path.relpath(file, self.top_dir), description=description)
543
544 def check_inconsistent_flag_lines(self, significant, module_line,
545 monolithic_line, separator_line):
546 if not (module_line.startswith("< ") and
547 monolithic_line.startswith("> ") and not separator_line):
548 # Something went wrong.
549 self.report(f"""Invalid build output detected:
550 module_line: "{module_line}"
551 monolithic_line: "{monolithic_line}"
552 separator_line: "{separator_line}"
553""")
554 sys.exit(1)
555
556 if significant:
557 logging.debug("%s", module_line)
558 logging.debug("%s", monolithic_line)
559 logging.debug("%s", separator_line)
560
561 def scan_inconsistent_flags_report(self, lines):
562 """Scans a hidden API flags report
563
564 The hidden API inconsistent flags report which looks something like
565 this.
566
567 < out/soong/.intermediates/.../filtered-stub-flags.csv
568 > out/soong/hiddenapi/hiddenapi-stub-flags.txt
569
570 < Landroid/compat/Compatibility;->clearOverrides()V
571 > Landroid/compat/Compatibility;->clearOverrides()V,core-platform-api
572
573 """
574
575 # The basic format of an entry in the inconsistent flags report is:
576 # <module specific flag>
577 # <monolithic flag>
578 # <separator>
579 #
580 # Wrap the lines iterator in an iterator which returns a tuple
581 # consisting of the three separate lines.
582 triples = zip(lines, lines, lines)
583
584 module_line, monolithic_line, separator_line = next(triples)
585 significant = False
586 bcpf_dir = self.module_info.module_path(self.bcpf)
587 if os.path.join(bcpf_dir, self.bcpf) in module_line:
588 # These errors are related to the bcpf being analyzed so
589 # keep them.
590 significant = True
591 else:
592 self.report(f"Filtering out errors related to {module_line}")
593
594 self.check_inconsistent_flag_lines(significant, module_line,
595 monolithic_line, separator_line)
596
597 diffs = {}
598 for module_line, monolithic_line, separator_line in triples:
599 self.check_inconsistent_flag_lines(significant, module_line,
600 monolithic_line, "")
601
602 module_parts = module_line.removeprefix("< ").split(",")
603 module_signature = module_parts[0]
604 module_flags = module_parts[1:]
605
606 monolithic_parts = monolithic_line.removeprefix("> ").split(",")
607 monolithic_signature = monolithic_parts[0]
608 monolithic_flags = monolithic_parts[1:]
609
610 if module_signature != monolithic_signature:
611 # Something went wrong.
612 self.report(f"""Inconsistent signatures detected:
613 module_signature: "{module_signature}"
614 monolithic_signature: "{monolithic_signature}"
615""")
616 sys.exit(1)
617
618 diffs[module_signature] = (module_flags, monolithic_flags)
619
620 if separator_line:
621 # If the separator line is not blank then it is the end of the
622 # current report, and possibly the start of another.
623 return separator_line, diffs
624
625 return "", diffs
626
627 def build_file_read_output(self, filename):
628 # Make sure the filename is relative to top if possible as the build
629 # may be using relative paths as the target.
630 rel_filename = filename.removeprefix(self.top_dir)
631 cmd = ["build/soong/soong_ui.bash", "--make-mode", rel_filename]
632 cmd_line = " ".join(cmd)
633 logging.debug("%s", cmd_line)
634 # pylint: disable=consider-using-with
635 output = subprocess.Popen(
636 cmd,
637 cwd=self.top_dir,
638 stderr=subprocess.STDOUT,
639 stdout=subprocess.PIPE,
640 text=True,
641 )
642 return BuildOperation(popen=output)
643
644 def build_hiddenapi_flags(self, filename):
645 output = self.build_file_read_output(filename)
646
647 lines = output.lines()
648 diffs = None
649 for line in lines:
650 logging.debug("%s", line)
651 while line == _INCONSISTENT_FLAGS:
652 line, diffs = self.scan_inconsistent_flags_report(lines)
653
654 output.wait(timeout=10)
655 if output.returncode != 0:
656 logging.debug("Command failed with %s", output.returncode)
657 else:
658 logging.debug("Command succeeded")
659
660 return diffs
661
662 def build_monolithic_stubs_flags(self):
663 self.report(f"""
664Attempting to build {_STUB_FLAGS_FILE} to verify that the
665bootclasspath_fragment has the correct API stubs available...
666""")
667
668 # Build the hiddenapi-stubs-flags.txt file.
669 diffs = self.build_hiddenapi_flags(_STUB_FLAGS_FILE)
670 if diffs:
671 self.report(f"""
672There is a discrepancy between the stub API derived flags created by the
673bootclasspath_fragment and the platform_bootclasspath. See preceding error
674messages to see which flags are inconsistent. The inconsistencies can occur for
675a couple of reasons:
676
677If you are building against prebuilts of the Android SDK, e.g. by using
678TARGET_BUILD_APPS then the prebuilt versions of the APIs this
679bootclasspath_fragment depends upon are out of date and need updating. See
680go/update-prebuilts for help.
681
682Otherwise, this is happening because there are some stub APIs that are either
683provided by or used by the contents of the bootclasspath_fragment but which are
684not available to it. There are 4 ways to handle this:
685
6861. A java_sdk_library in the contents property will automatically make its stub
687 APIs available to the bootclasspath_fragment so nothing needs to be done.
688
6892. If the API provided by the bootclasspath_fragment is created by an api_only
690 java_sdk_library (or a java_library that compiles files generated by a
691 separate droidstubs module then it cannot be added to the contents and
692 instead must be added to the api.stubs property, e.g.
693
694 bootclasspath_fragment {{
695 name: "{self.bcpf}",
696 ...
697 api: {{
698 stubs: ["$MODULE-api-only"],"
699 }},
700 }}
701
7023. If the contents use APIs provided by another bootclasspath_fragment then
703 it needs to be added to the fragments property, e.g.
704
705 bootclasspath_fragment {{
706 name: "{self.bcpf}",
707 ...
708 // The bootclasspath_fragments that provide APIs on which this depends.
709 fragments: [
710 ...
711 {{
712 apex: "com.android.other",
713 module: "com.android.other-bootclasspath-fragment",
714 }},
715 ],
716 }}
717
7184. If the contents use APIs from a module that is not part of another
719 bootclasspath_fragment then it must be added to the additional_stubs
720 property, e.g.
721
722 bootclasspath_fragment {{
723 name: "{self.bcpf}",
724 ...
725 additional_stubs: ["android-non-updatable"],
726 }}
727
728 Like the api.stubs property these are typically java_sdk_library modules but
729 can be java_library too.
730
731 Note: The "android-non-updatable" is treated as if it was a java_sdk_library
732 which it is not at the moment but will be in future.
733""")
734
735 return diffs
736
737 def build_monolithic_flags(self, result):
738 self.report(f"""
739Attempting to build {_FLAGS_FILE} to verify that the
740bootclasspath_fragment has the correct hidden API flags...
741""")
742
743 # Build the hiddenapi-flags.csv file and extract any differences in
744 # the flags between this bootclasspath_fragment and the monolithic
745 # files.
746 result.diffs = self.build_hiddenapi_flags(_FLAGS_FILE)
747
748 # Load information from the bootclasspath_fragment's all-flags.csv file.
749 self.load_all_flags()
750
751 if result.diffs:
752 self.report(f"""
753There is a discrepancy between the hidden API flags created by the
754bootclasspath_fragment and the platform_bootclasspath. See preceding error
755messages to see which flags are inconsistent. The inconsistencies can occur for
756a couple of reasons:
757
758If you are building against prebuilts of this bootclasspath_fragment then the
759prebuilt version of the sdk snapshot (specifically the hidden API flag files)
760are inconsistent with the prebuilt version of the apex {self.apex}. Please
761ensure that they are both updated from the same build.
762
7631. There are custom hidden API flags specified in the one of the files in
764 frameworks/base/boot/hiddenapi which apply to the bootclasspath_fragment but
765 which are not supplied to the bootclasspath_fragment module.
766
7672. The bootclasspath_fragment specifies invalid "package_prefixes" or
768 "split_packages" properties that match packages and classes that it does not
769 provide.
770
771""")
772
773 # Check to see if there are any hiddenapi related properties that
774 # need to be added to the
775 self.report("""
776Checking custom hidden API flags....
777""")
778 self.check_frameworks_base_boot_hidden_api_files(result)
779
780 def report_hidden_api_flag_file_changes(self, result, property_name,
781 flags_file, rel_bcpf_flags_file,
782 bcpf_flags_file):
783 matched_signatures = set()
784 # Open the flags file to read the flags from.
785 with open(flags_file, "r", encoding="utf8") as f:
786 for signature in newline_stripping_iter(f.readline):
787 if signature in self.signatures:
788 # The signature is provided by the bootclasspath_fragment so
789 # it will need to be moved to the bootclasspath_fragment
790 # specific file.
791 matched_signatures.add(signature)
792
793 # If the bootclasspath_fragment specific flags file is not empty
794 # then it contains flags. That could either be new flags just moved
795 # from frameworks/base or previous contents of the file. In either
796 # case the file must not be removed.
797 if matched_signatures:
798 insert = textwrap.indent("\n".join(matched_signatures),
799 " ")
800 result.file_changes.append(
801 self.new_file_change(
802 flags_file, f"""Remove the following entries:
803{insert}
804"""))
805
806 result.file_changes.append(
807 self.new_file_change(
808 bcpf_flags_file, f"""Add the following entries:
809{insert}
810"""))
811
812 result.property_changes.append(
813 HiddenApiPropertyChange(
814 property_name=property_name,
815 values=[rel_bcpf_flags_file],
816 ))
817
Paul Duffin26f19912022-03-28 16:09:27 +0100818 def fix_hidden_api_flag_files(self, result, property_name, flags_file,
819 rel_bcpf_flags_file, bcpf_flags_file):
820 # Read the file in frameworks/base/boot/hiddenapi/<file> copy any
821 # flags that relate to the bootclasspath_fragment into a local
822 # file in the hiddenapi subdirectory.
823 tmp_flags_file = flags_file + ".tmp"
824
825 # Make sure the directory containing the bootclasspath_fragment specific
826 # hidden api flags exists.
827 os.makedirs(os.path.dirname(bcpf_flags_file), exist_ok=True)
828
829 bcpf_flags_file_exists = os.path.exists(bcpf_flags_file)
830
831 matched_signatures = set()
832 # Open the flags file to read the flags from.
833 with open(flags_file, "r", encoding="utf8") as f:
834 # Open a temporary file to write the flags (minus any removed
835 # flags).
836 with open(tmp_flags_file, "w", encoding="utf8") as t:
837 # Open the bootclasspath_fragment file for append just in
838 # case it already exists.
839 with open(bcpf_flags_file, "a", encoding="utf8") as b:
840 for line in iter(f.readline, ""):
841 signature = line.rstrip()
842 if signature in self.signatures:
843 # The signature is provided by the
844 # bootclasspath_fragment so write it to the new
845 # bootclasspath_fragment specific file.
846 print(line, file=b, end="")
847 matched_signatures.add(signature)
848 else:
849 # The signature is NOT provided by the
850 # bootclasspath_fragment. Copy it to the new
851 # monolithic file.
852 print(line, file=t, end="")
853
854 # If the bootclasspath_fragment specific flags file is not empty
855 # then it contains flags. That could either be new flags just moved
856 # from frameworks/base or previous contents of the file. In either
857 # case the file must not be removed.
858 if matched_signatures:
859 # There are custom flags related to the bootclasspath_fragment
860 # so replace the frameworks/base/boot/hiddenapi file with the
861 # file that does not contain those flags.
862 shutil.move(tmp_flags_file, flags_file)
863
864 result.file_changes.append(
865 self.new_file_change(flags_file,
866 f"Removed '{self.bcpf}' specific entries"))
867
868 result.property_changes.append(
869 HiddenApiPropertyChange(
870 property_name=property_name,
871 values=[rel_bcpf_flags_file],
872 ))
873
874 # Make sure that the files are sorted.
875 self.run_command([
876 "tools/platform-compat/hiddenapi/sort_api.sh",
877 bcpf_flags_file,
878 ])
879
880 if bcpf_flags_file_exists:
881 desc = f"Added '{self.bcpf}' specific entries"
882 else:
883 desc = f"Created with '{self.bcpf}' specific entries"
884 result.file_changes.append(
885 self.new_file_change(bcpf_flags_file, desc))
886 else:
887 # There are no custom flags related to the
888 # bootclasspath_fragment so clean up the working files.
889 os.remove(tmp_flags_file)
890 if not bcpf_flags_file_exists:
891 os.remove(bcpf_flags_file)
892
Paul Duffin4dcf6592022-02-28 19:22:12 +0000893 def check_frameworks_base_boot_hidden_api_files(self, result):
894 hiddenapi_dir = os.path.join(self.top_dir,
895 "frameworks/base/boot/hiddenapi")
896 for basename in sorted(os.listdir(hiddenapi_dir)):
897 if not (basename.startswith("hiddenapi-") and
898 basename.endswith(".txt")):
899 continue
900
901 flags_file = os.path.join(hiddenapi_dir, basename)
902
903 logging.debug("Checking %s for flags related to %s", flags_file,
904 self.bcpf)
905
906 # Map the file name in frameworks/base/boot/hiddenapi into a
907 # slightly more meaningful name for use by the
908 # bootclasspath_fragment.
909 if basename == "hiddenapi-max-target-o.txt":
910 basename = "hiddenapi-max-target-o-low-priority.txt"
911 elif basename == "hiddenapi-max-target-r-loprio.txt":
912 basename = "hiddenapi-max-target-r-low-priority.txt"
913
914 property_name = basename.removeprefix("hiddenapi-")
915 property_name = property_name.removesuffix(".txt")
916 property_name = property_name.replace("-", "_")
917
918 rel_bcpf_flags_file = f"hiddenapi/{basename}"
919 bcpf_dir = self.module_info.module_path(self.bcpf)
920 bcpf_flags_file = os.path.join(self.top_dir, bcpf_dir,
921 rel_bcpf_flags_file)
922
Paul Duffin26f19912022-03-28 16:09:27 +0100923 if self.fix:
924 self.fix_hidden_api_flag_files(result, property_name,
925 flags_file, rel_bcpf_flags_file,
926 bcpf_flags_file)
927 else:
928 self.report_hidden_api_flag_file_changes(
929 result, property_name, flags_file, rel_bcpf_flags_file,
930 bcpf_flags_file)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000931
932
933def newline_stripping_iter(iterator):
934 """Return an iterator over the iterator that strips trailing white space."""
935 lines = iter(iterator, "")
936 lines = (line.rstrip() for line in lines)
937 return lines
938
939
940def format_comment_as_text(text, indent):
941 return "".join(
942 [f"{line}\n" for line in format_comment_as_lines(text, indent)])
943
944
945def format_comment_as_lines(text, indent):
946 lines = textwrap.wrap(text.strip("\n"), width=77 - len(indent))
947 lines = [f"{indent}// {line}" for line in lines]
948 return lines
949
950
951def log_stream_for_subprocess():
952 stream = subprocess.DEVNULL
953 for handler in logging.root.handlers:
954 if handler.level == logging.DEBUG:
955 if isinstance(handler, logging.StreamHandler):
956 stream = handler.stream
957 return stream
958
959
960def main(argv):
961 args_parser = argparse.ArgumentParser(
962 description="Analyze a bootclasspath_fragment module.")
963 args_parser.add_argument(
964 "--bcpf",
965 help="The bootclasspath_fragment module to analyze",
966 required=True,
967 )
968 args_parser.add_argument(
969 "--apex",
970 help="The apex module to which the bootclasspath_fragment belongs. It "
971 "is not strictly necessary at the moment but providing it will "
972 "allow this script to give more useful messages and it may be"
973 "required in future.",
974 default="SPECIFY-APEX-OPTION")
975 args_parser.add_argument(
976 "--sdk",
977 help="The sdk module to which the bootclasspath_fragment belongs. It "
978 "is not strictly necessary at the moment but providing it will "
979 "allow this script to give more useful messages and it may be"
980 "required in future.",
981 default="SPECIFY-SDK-OPTION")
Paul Duffin26f19912022-03-28 16:09:27 +0100982 args_parser.add_argument(
983 "--fix",
984 help="Attempt to fix any issues found automatically.",
985 action="store_true",
986 default=False)
Paul Duffin4dcf6592022-02-28 19:22:12 +0000987 args = args_parser.parse_args(argv[1:])
988 top_dir = os.environ["ANDROID_BUILD_TOP"] + "/"
989 out_dir = os.environ.get("OUT_DIR", os.path.join(top_dir, "out"))
990 product_out_dir = os.environ.get("ANDROID_PRODUCT_OUT", top_dir)
991 # Make product_out_dir relative to the top so it can be used as part of a
992 # build target.
993 product_out_dir = product_out_dir.removeprefix(top_dir)
994 log_fd, abs_log_file = tempfile.mkstemp(
995 suffix="_analyze_bcpf.log", text=True)
996
997 with os.fdopen(log_fd, "w") as log_file:
998 # Set up debug logging to the log file.
999 logging.basicConfig(
1000 level=logging.DEBUG,
1001 format="%(levelname)-8s %(message)s",
1002 stream=log_file)
1003
1004 # define a Handler which writes INFO messages or higher to the
1005 # sys.stdout with just the message.
1006 console = logging.StreamHandler()
1007 console.setLevel(logging.INFO)
1008 console.setFormatter(logging.Formatter("%(message)s"))
1009 # add the handler to the root logger
1010 logging.getLogger("").addHandler(console)
1011
1012 print(f"Writing log to {abs_log_file}")
1013 try:
1014 analyzer = BcpfAnalyzer(
Paul Duffin26f19912022-03-28 16:09:27 +01001015 tool_path=argv[0],
Paul Duffin4dcf6592022-02-28 19:22:12 +00001016 top_dir=top_dir,
1017 out_dir=out_dir,
1018 product_out_dir=product_out_dir,
1019 bcpf=args.bcpf,
1020 apex=args.apex,
1021 sdk=args.sdk,
Paul Duffin26f19912022-03-28 16:09:27 +01001022 fix=args.fix,
Paul Duffin4dcf6592022-02-28 19:22:12 +00001023 )
1024 analyzer.analyze()
1025 finally:
1026 print(f"Log written to {abs_log_file}")
1027
1028
1029if __name__ == "__main__":
1030 main(sys.argv)