• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright (C) 2021 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#
17
18"""
19Dump new HALs that are introduced in each FCM version in a human-readable format.
20
21Example:
22hals_for_release.py
23  Show changes for each release, including new and deprecated HALs.
24hals_for_release.py -dua
25  Show changes as well as unchanged HALs for each release.
26hals_for_release.py -i
27  Show details about instance names and regex patterns as well.
28hals_for_release.py -p wifi
29  Show changes of Wi-Fi HALs for each release.
30"""
31
32import argparse
33import collections
34import enum
35import json
36import logging
37import os
38import subprocess
39from collections.abc import Sequence
40from typing import Any
41from typing import Optional
42
43import sys
44
45logging.basicConfig(format="%(levelname)s: %(message)s")
46logger = logging.getLogger(__name__)
47
48
49def ParseArgs() -> argparse.Namespace:
50  """
51  Parse arguments.
52  :return: arguments.
53  """
54  parser = argparse.ArgumentParser(description=__doc__,
55                                   formatter_class=argparse.RawTextHelpFormatter)
56  parser.add_argument("--analyze-matrix", help="Location of analyze_matrix")
57  parser.add_argument("input", metavar="INPUT", nargs="?",
58                      help="Directory of compatibility matrices.")
59  parser.add_argument("--deprecated", "-d",
60                      help="Show deprecated HALs. If none of deprecated, unchanged or introduced "
61                           "is specified, default is --deprecated and --introduced",
62                      action="store_true")
63  parser.add_argument("--unchanged", "-u",
64                      help="Show unchanged HALs. If none of deprecated, unchanged or introduced "
65                           "is specified, default is --deprecated and --introduced",
66                      action="store_true")
67  parser.add_argument("--introduced", "-a",
68                      help="Show deprecated HALs. If none of deprecated, unchanged or introduced "
69                           "is specified, default is --deprecated and --introduced",
70                      action="store_true")
71  parser.add_argument("--instances", "-i", action="store_true",
72                      help="Show instance names and regex patterns as well")
73  parser.add_argument("--packages", "-p", nargs="*", metavar="PACKAGE",
74                      help="Only print HALs where package contains the given substring. "
75                           "E.g. wifi, usb, health. Recommend to use with --unchanged.")
76  parser.add_argument("--verbose", "-v", action="store_true", help="Verbose mode")
77  parser.add_argument("--json", "-j", action="store_true", help="Print JSON")
78  args = parser.parse_args()
79
80  if args.verbose:
81    logger.setLevel(logging.DEBUG)
82
83  if not args.deprecated and not args.unchanged and not args.introduced:
84    args.deprecated = args.introduced = True
85
86  host_out = os.environ.get("ANDROID_HOST_OUT")
87  if host_out and not args.analyze_matrix:
88    analyze_matrix = os.path.join(host_out, "bin", "analyze_matrix")
89    if os.path.isfile(analyze_matrix):
90      args.analyze_matrix = analyze_matrix
91  if not args.analyze_matrix:
92    args.analyze_matrix = "analyze_matrix"
93
94  top = os.environ.get("ANDROID_BUILD_TOP")
95  if top and not args.input:
96    args.input = os.path.join(top, "hardware", "interfaces", "compatibility_matrices")
97  if not args.input:
98    logger.fatal("Unable to determine compatibility matrix dir, lunch or provide one explicitly.")
99    return None
100
101  logger.debug("Using analyze_matrix at path: %s", args.analyze_matrix)
102  logger.debug("Dumping compatibility matrices at path: %s", args.input)
103  logger.debug("Show deprecated HALs? %s", args.deprecated)
104  logger.debug("Show unchanged HALs? %s", args.unchanged)
105  logger.debug("Show introduced HALs? %s", args.introduced)
106  logger.debug("Only showing packages %s", args.packages)
107
108  return args
109
110
111def Analyze(analyze_matrix: str, file: str, args: Sequence[str],
112    ignore_errors=False) -> str:
113  """
114  Run analyze_matrix with
115  :param analyze_matrix: path of analyze_matrix
116  :param file: input file
117  :param arg: argument to analyze_matrix, e.g. "level"
118  :param ignore_errors: Whether errors during execution should be rased
119  :return: output of analyze_matrix
120  """
121  command = [analyze_matrix, "--input", file] + args
122  proc = subprocess.run(command,
123                        stdout=subprocess.PIPE,
124                        stderr=subprocess.DEVNULL if ignore_errors else subprocess.PIPE)
125  if not ignore_errors and proc.returncode != 0:
126    logger.warning("`%s` exits with code %d with the following error: %s", " ".join(command),
127                   proc.returncode, proc.stderr)
128    proc.check_returncode()
129  return proc.stdout.decode().strip()
130
131
132def GetLevel(analyze_matrix: str, file: str) -> Optional[int]:
133  """
134  :param analyze_matrix: Path of analyze_matrix
135  :param file: a file, possibly a compatibility matrix
136  :return: If it is a compatibility matrix, return an integer indicating the level.
137    If it is not a compatibility matrix, returns None.
138    For matrices with empty level, return None.
139  """
140  output = Analyze(analyze_matrix, file, ["--level"], ignore_errors=True)
141  # Ignore empty level matrices and non-matrices
142  if not output:
143    return None
144  try:
145    return int(output)
146  except ValueError:
147    logger.warning("Unknown level '%s' in file: %s", output, file)
148    return None
149
150
151def GetLevelName(analyze_matrix: str, file: str) -> str:
152  """
153  :param analyze_matrix: Path of analyze_matrix
154  :param file: a file, possibly a compatibility matrix
155  :return: If it is a compatibility matrix, return the level name.
156    If it is not a compatibility matrix, returns None.
157    For matrices with empty level, return "Level unspecified".
158  """
159  return Analyze(analyze_matrix, file, ["--level-name"], ignore_errors=True)
160
161
162class MatrixData(object):
163  def __init__(self, level: str, level_name: str, instances: Sequence[str]):
164    self.level = level
165    self.level_name = level_name
166    self.instances = instances
167
168  def GetInstancesKeyedOnPackage(self) -> dict[str, list[str]]:
169    return KeyOnPackage(self.instances)
170
171
172def ReadMatrices(args: argparse.Namespace) -> dict[int, MatrixData]:
173  """
174  :param args: parsed arguments from ParseArgs
175  :return: A dictionary. Key is an integer indicating the matrix level.
176  Value is (level name, a set of instances in that matrix).
177  """
178  matrices = dict()
179  for child in os.listdir(args.input):
180    file = os.path.join(args.input, child)
181    level, level_name = GetLevel(args.analyze_matrix, file), GetLevelName(args.analyze_matrix, file)
182    if level is None:
183      logger.debug("Ignoring file %s", file)
184      continue
185    action = "--instances" if args.instances else "--interfaces"
186    instances = Analyze(args.analyze_matrix, file, [action, "--requirement"]).split("\n")
187    instances = set(map(str.strip, instances)) - {""}
188    if level in matrices:
189      logger.warning("Found duplicated matrix for level %s, ignoring: %s", level, file)
190      continue
191    matrices[level] = MatrixData(level, level_name, instances)
192
193  return matrices
194
195
196class HalFormat(enum.Enum):
197  HIDL = 0
198  AIDL = 2
199
200
201def GetHalFormat(instance: str) -> HalFormat:
202  """
203  Guess the HAL format of instance.
204  :param instance: two formats:
205    android.hardware.health.storage@1.0::IStorage/default optional
206    android.hardware.health.storage.IStorage/default (@1) optional
207  :return: HalFormat.HIDL for the first one, HalFormat.AIDL for the second.
208
209  >>> str(GetHalFormat("android.hardware.health.storage@1.0::IStorage/default optional"))
210  'HalFormat.HIDL'
211  >>> str(GetHalFormat("android.hardware.health.storage.IStorage/default (@1) optional"))
212  'HalFormat.AIDL'
213  """
214  return HalFormat.HIDL if "::" in instance else HalFormat.AIDL
215
216
217def SplitInstance(instance: str) -> tuple[str, str, str]:
218  """
219  Split instance into parts.
220  :param instance:
221  :param instance: two formats:
222    android.hardware.health.storage@1.0::IStorage/default optional
223    android.hardware.health.storage.IStorage/default (@1) optional
224  :return: (package, version+interface+instance, requirement)
225
226  >>> SplitInstance("android.hardware.health.storage@1.0::IStorage/default optional")
227  ('android.hardware.health.storage', '@1.0::IStorage/default', 'optional')
228  >>> SplitInstance("android.hardware.health.storage.IStorage/default (@1) optional")
229  ('android.hardware.health.storage', 'IStorage/default (@1)', 'optional')
230  """
231  format = GetHalFormat(instance)
232  if format == HalFormat.HIDL:
233    atPos = instance.find("@")
234    spacePos = instance.rfind(" ")
235    return instance[:atPos], instance[atPos:spacePos], instance[spacePos + 1:]
236  elif format == HalFormat.AIDL:
237    dotPos = instance.rfind(".")
238    spacePos = instance.rfind(" ")
239    return instance[:dotPos], instance[dotPos + 1:spacePos], instance[spacePos + 1:]
240
241
242def GetPackage(instance: str) -> str:
243  """
244  Guess the package of instance.
245  :param instance: two formats:
246    android.hardware.health.storage@1.0::IStorage/default
247    android.hardware.health.storage.IStorage/default (@1)
248  :return: The package. In the above example, return android.hardware.health.storage
249
250  >>> GetPackage("android.hardware.health.storage@1.0::IStorage/default")
251  'android.hardware.health.storage'
252  >>> GetPackage("android.hardware.health.storage.IStorage/default (@1)")
253  'android.hardware.health.storage'
254  """
255  return SplitInstance(instance)[0]
256
257
258def KeyOnPackage(instances: Sequence[str]) -> dict[str, list[str]]:
259  """
260  :param instances: A list of instances.
261  :return: A dictionary, where key is the package (see GetPackage), and
262    value is a list of instances in the provided list, where
263    GetPackage(instance) is the corresponding key.
264  """
265  d = collections.defaultdict(list)
266  for instance in instances:
267    package = GetPackage(instance)
268    d[package].append(instance)
269  return d
270
271
272class Report(object):
273  """
274  Base class for generating a report.
275  """
276  def __init__(self, matrixData1: MatrixData, matrixData2: MatrixData, args: argparse.Namespace):
277    """
278    Initialize the report with two matrices.
279    :param matrixData1: Data of the old matrix
280    :param matrixData2: Data of the new matrix
281    :param args: command-line arguments
282    """
283    self.args = args
284    self.matrixData1 = matrixData1
285    self.matrixData2 = matrixData2
286    self.instances_by_package1 = matrixData1.GetInstancesKeyedOnPackage()
287    self.instances_by_package2 = matrixData2.GetInstancesKeyedOnPackage()
288    self.all_packages = set(self.instances_by_package1.keys()) | set(
289      self.instances_by_package2.keys())
290
291  def GetReport(self) -> Any:
292    """
293    Generate the report
294    :return: An object representing the report. Type is implementation defined.
295    """
296    packages_report: dict[str, Any] = dict()
297    for package in self.all_packages:
298      package_instances1 = set(self.instances_by_package1.get(package, []))
299      package_instances2 = set(self.instances_by_package2.get(package, []))
300      deprecated = sorted(package_instances1 - package_instances2)
301      unchanged = sorted(package_instances1 & package_instances2)
302      introduced = sorted(package_instances2 - package_instances1)
303      package_report = self.DescribePackage(deprecated=deprecated,
304                                            unchanged=unchanged,
305                                            introduced=introduced)
306      if package_report:
307        packages_report[package] = package_report
308    return self.CombineReport(packages_report)
309
310  def DescribePackage(self, deprecated: Sequence[str], unchanged: Sequence[str],
311      introduced: Sequence[str]) -> Any:
312    """
313    Describe a package in a implementation-defined format, with the given
314    set of changes.
315    :param deprecated: set of deprecated HALs
316    :param unchanged:  set of unchanged HALs
317    :param introduced: set of new HALs
318    :return: An object that will later be passed into the values of the
319      packages_report argument of CombineReport
320    """
321    raise NotImplementedError
322
323  def CombineReport(self, packages_report: dict[str, Any]) -> Any:
324    """
325    Combine a set of reports for a package in an implementation-defined way.
326    :param packages_report: A dictionary, where key is the package
327      name, and value is the object generated by DescribePackage
328    :return: the report object
329    """
330    raise NotImplementedError
331
332
333class HumanReadableReport(Report):
334  def DescribePackage(self, deprecated: Sequence[str], unchanged: Sequence[str],
335      introduced: Sequence[str]) -> Any:
336    package_report = []
337    desc = lambda fmt, instance: fmt.format(GetHalFormat(instance).name,
338                                            *SplitInstance(instance))
339    if self.args.deprecated:
340      package_report += [desc("- {0} {2} can no longer be used", instance)
341                         for instance in deprecated]
342    if self.args.unchanged:
343      package_report += [desc("  {0} {2} is {3}", instance) for instance in
344                         unchanged]
345    if self.args.introduced:
346      package_report += [desc("+ {0} {2} is {3}", instance) for instance in
347                         introduced]
348
349    return package_report
350
351  def CombineReport(self, packages_report: dict[str, Any]) -> str:
352    report = ["============",
353              "Level %s (%s) (against Level %s (%s))" % (
354              self.matrixData2.level, self.matrixData2.level_name,
355              self.matrixData1.level, self.matrixData1.level_name),
356              "============"]
357    for package, lines in sorted(packages_report.items()):
358      report.append(package)
359      report += [("    " + e) for e in lines]
360
361    return "\n".join(report)
362
363
364class JsonReport(Report):
365  def DescribePackage(self, deprecated: Sequence[str], unchanged: Sequence[str],
366      introduced: Sequence[str]) -> Any:
367    package_report = collections.defaultdict(list)
368    if self.args.deprecated and deprecated:
369      package_report["deprecated"] += deprecated
370    if self.args.unchanged and unchanged:
371      package_report["unchanged"] += unchanged
372    if self.args.introduced and introduced:
373      package_report["introduced"] += introduced
374
375    return package_report
376
377  def CombineReport(self, packages_report: dict[str, Any]) -> dict[str, Any]:
378    final = collections.defaultdict(list)
379    for package_report in packages_report.values():
380      for key, lst in package_report.items():
381        final[key] += lst
382    final["__meta__"] = {
383        "old": {"level": self.matrixData1.level,
384                "level_name": self.matrixData1.level_name},
385        "new": {"level": self.matrixData2.level,
386                "level_name": self.matrixData2.level_name},
387    }
388    return final
389
390
391def PrintReport(matrices: dict[int, MatrixData], args: argparse.Namespace):
392  """
393  :param matrixData1: data of first matrix
394  :param matrixData2: data of second matrix
395  :return: A report of their difference.
396  """
397  sorted_matrices = sorted(matrices.items())
398  if not sorted_matrices:
399    logger.warning("Nothing to show, because no matrices found in '%s'.", args.input)
400
401  if args.json:
402    reports = []
403    for (level1, matrixData1), (level2, matrixData2) in zip(sorted_matrices, sorted_matrices[1:]):
404      reports.append(JsonReport(matrixData1, matrixData2, args).GetReport())
405    print(json.dumps(reports))
406    return
407
408  for (level1, matrixData1), (level2, matrixData2) in zip(sorted_matrices, sorted_matrices[1:]):
409    report = HumanReadableReport(matrixData1, matrixData2, args)
410    print(report.GetReport())
411
412
413def main():
414  sys.stderr.write("Generated with %s\n" % " ".join(sys.argv))
415  args = ParseArgs()
416  if args is None:
417    return 1
418  matrices = ReadMatrices(args)
419  PrintReport(matrices, args)
420  return 0
421
422
423if __name__ == "__main__":
424  sys.exit(main())
425