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