• 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 logging
36import os
37import subprocess
38import sys
39
40logging.basicConfig(format="%(levelname)s: %(message)s")
41logger = logging.getLogger(__name__)
42
43
44def ParseArgs():
45  """
46  Parse arguments.
47  :return: arguments.
48  """
49  parser = argparse.ArgumentParser(description=__doc__,
50                                   formatter_class=argparse.RawTextHelpFormatter)
51  parser.add_argument("--analyze-matrix", help="Location of analyze_matrix")
52  parser.add_argument("input", metavar="INPUT", nargs="?",
53                      help="Directory of compatibility matrices.")
54  parser.add_argument("--deprecated", "-d",
55                      help="Show deprecated HALs. If none of deprecated, unchanged or introduced "
56                           "is specified, default is --deprecated and --introduced",
57                      action="store_true")
58  parser.add_argument("--unchanged", "-u",
59                      help="Show unchanged HALs. If none of deprecated, unchanged or introduced "
60                           "is specified, default is --deprecated and --introduced",
61                      action="store_true")
62  parser.add_argument("--introduced", "-a",
63                      help="Show deprecated HALs. If none of deprecated, unchanged or introduced "
64                           "is specified, default is --deprecated and --introduced",
65                      action="store_true")
66  parser.add_argument("--instances", "-i", action="store_true",
67                      help="Show instance names and regex patterns as well")
68  parser.add_argument("--packages", "-p", nargs="*", metavar="PACKAGE",
69                      help="Only print HALs where package contains the given substring. "
70                           "E.g. wifi, usb, health. Recommend to use with --unchanged.")
71  parser.add_argument("--verbose", "-v", action="store_true", help="Verbose mode")
72  args = parser.parse_args()
73
74  if args.verbose:
75    logger.setLevel(logging.DEBUG)
76
77  if not args.deprecated and not args.unchanged and not args.introduced:
78    args.deprecated = args.introduced = True
79
80  host_out = os.environ.get("ANDROID_HOST_OUT")
81  if host_out and not args.analyze_matrix:
82    analyze_matrix = os.path.join(host_out, "bin", "analyze_matrix")
83    if os.path.isfile(analyze_matrix):
84      args.analyze_matrix = analyze_matrix
85  if not args.analyze_matrix:
86    args.analyze_matrix = "analyze_matrix"
87
88  top = os.environ.get("ANDROID_BUILD_TOP")
89  if top and not args.input:
90    args.input = os.path.join(top, "hardware", "interfaces", "compatibility_matrices")
91  if not args.input:
92    logger.fatal("Unable to determine compatibility matrix dir, lunch or provide one explicitly.")
93    return None
94
95  logger.debug("Using analyze_matrix at path: %s", args.analyze_matrix)
96  logger.debug("Dumping compatibility matrices at path: %s", args.input)
97  logger.debug("Show deprecated HALs? %s", args.deprecated)
98  logger.debug("Show unchanged HALs? %s", args.unchanged)
99  logger.debug("Show introduced HALs? %s", args.introduced)
100  logger.debug("Only showing packages %s", args.packages)
101
102  return args
103
104
105def Analyze(analyze_matrix, file, args, ignore_errors=False):
106  """
107  Run analyze_matrix with
108  :param analyze_matrix: path of analyze_matrix
109  :param file: input file
110  :param arg: argument to analyze_matrix, e.g. "level"
111  :param ignore_errors: Whether errors during execution should be rased
112  :return: output of analyze_matrix
113  """
114  command = [analyze_matrix, "--input", file] + args
115  proc = subprocess.run(command,
116                        stdout=subprocess.PIPE,
117                        stderr=subprocess.DEVNULL if ignore_errors else subprocess.PIPE)
118  if not ignore_errors and proc.returncode != 0:
119    logger.warning("`%s` exits with code %d with the following error: %s", " ".join(command),
120                   proc.returncode, proc.stderr)
121    proc.check_returncode()
122  return proc.stdout.decode().strip()
123
124
125def GetLevel(analyze_matrix, file):
126  """
127  :param analyze_matrix: Path of analyze_matrix
128  :param file: a file, possibly a compatibility matrix
129  :return: If it is a compatibility matrix, return an integer indicating the level.
130    If it is not a compatibility matrix, returns None.
131    For matrices with empty level, return None.
132  """
133  output = Analyze(analyze_matrix, file, ["--level"], ignore_errors=True)
134  # Ignore empty level matrices and non-matrices
135  if not output:
136    return None
137  try:
138    return int(output)
139  except ValueError:
140    logger.warning("Unknown level '%s' in file: %s", output, file)
141    return None
142
143
144def GetLevelName(analyze_matrix, file):
145  """
146  :param analyze_matrix: Path of analyze_matrix
147  :param file: a file, possibly a compatibility matrix
148  :return: If it is a compatibility matrix, return the level name.
149    If it is not a compatibility matrix, returns None.
150    For matrices with empty level, return "Level unspecified".
151  """
152  return Analyze(analyze_matrix, file, ["--level-name"], ignore_errors=True)
153
154
155def ReadMatrices(args):
156  """
157  :param args: parsed arguments from ParseArgs
158  :return: A dictionary. Key is an integer indicating the matrix level.
159  Value is (level name, a set of instances in that matrix).
160  """
161  matrices = dict()
162  for child in os.listdir(args.input):
163    file = os.path.join(args.input, child)
164    level, level_name = GetLevel(args.analyze_matrix, file), GetLevelName(args.analyze_matrix, file)
165    if level is None:
166      logger.debug("Ignoring file %s", file)
167      continue
168    action = "--instances" if args.instances else "--interfaces"
169    instances = Analyze(args.analyze_matrix, file, [action, "--requirement"]).split("\n")
170    instances = set(map(str.strip, instances)) - {""}
171    if level in matrices:
172      logger.warning("Found duplicated matrix for level %s, ignoring: %s", level, file)
173      continue
174    matrices[level] = (level_name, instances)
175
176  return matrices
177
178
179class HalFormat(enum.Enum):
180  HIDL = 0
181  AIDL = 2
182
183
184def GetHalFormat(instance):
185  """
186  Guess the HAL format of instance.
187  :param instance: two formats:
188    android.hardware.health.storage@1.0::IStorage/default optional
189    android.hardware.health.storage.IStorage/default (@1) optional
190  :return: HalFormat.HIDL for the first one, HalFormat.AIDL for the second.
191
192  >>> str(GetHalFormat("android.hardware.health.storage@1.0::IStorage/default optional"))
193  'HalFormat.HIDL'
194  >>> str(GetHalFormat("android.hardware.health.storage.IStorage/default (@1) optional"))
195  'HalFormat.AIDL'
196  """
197  return HalFormat.HIDL if "::" in instance else HalFormat.AIDL
198
199
200def SplitInstance(instance):
201  """
202  Split instance into parts.
203  :param 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: (package, version+interface+instance, requirement)
208
209  >>> SplitInstance("android.hardware.health.storage@1.0::IStorage/default optional")
210  ('android.hardware.health.storage', '@1.0::IStorage/default', 'optional')
211  >>> SplitInstance("android.hardware.health.storage.IStorage/default (@1) optional")
212  ('android.hardware.health.storage', 'IStorage/default (@1)', 'optional')
213  """
214  format = GetHalFormat(instance)
215  if format == HalFormat.HIDL:
216    atPos = instance.find("@")
217    spacePos = instance.rfind(" ")
218    return instance[:atPos], instance[atPos:spacePos], instance[spacePos + 1:]
219  elif format == HalFormat.AIDL:
220    dotPos = instance.rfind(".")
221    spacePos = instance.rfind(" ")
222    return instance[:dotPos], instance[dotPos + 1:spacePos], instance[spacePos + 1:]
223
224
225def GetPackage(instance):
226  """
227  Guess the package of instance.
228  :param instance: two formats:
229    android.hardware.health.storage@1.0::IStorage/default
230    android.hardware.health.storage.IStorage/default (@1)
231  :return: The package. In the above example, return android.hardware.health.storage
232
233  >>> GetPackage("android.hardware.health.storage@1.0::IStorage/default")
234  'android.hardware.health.storage'
235  >>> GetPackage("android.hardware.health.storage.IStorage/default (@1)")
236  'android.hardware.health.storage'
237  """
238  return SplitInstance(instance)[0]
239
240
241def KeyOnPackage(instances):
242  """
243  :param instances: A list of instances.
244  :return: A dictionary, where key is the package (see GetPackage), and
245    value is a list of instances in the provided list, where
246    GetPackage(instance) is the corresponding key.
247  """
248  d = collections.defaultdict(list)
249  for instance in instances:
250    package = GetPackage(instance)
251    d[package].append(instance)
252  return d
253
254
255def GetReport(tuple1, tuple2, args):
256  """
257  :param tuple1: (level, (level_name, Set of instances from the first matrix))
258  :param tuple2: (level, (level_name, Set of instances from the second matrix))
259  :return: A human-readable report of their difference.
260  """
261  level1, (level_name1, instances1) = tuple1
262  level2, (level_name2, instances2) = tuple2
263
264  instances_by_package1 = KeyOnPackage(instances1)
265  instances_by_package2 = KeyOnPackage(instances2)
266  all_packages = set(instances_by_package1.keys()) | set(instances_by_package2.keys())
267
268  if args.packages:
269    package_matches = lambda package: any(pattern in package for pattern in args.packages)
270    all_packages = filter(package_matches, all_packages)
271
272  packages_report = dict()
273  for package in all_packages:
274    package_instances1 = set(instances_by_package1.get(package, []))
275    package_instances2 = set(instances_by_package2.get(package, []))
276
277    package_report = []
278    deprecated = sorted(package_instances1 - package_instances2)
279    unchanged = sorted(package_instances1 & package_instances2)
280    introduced = sorted(package_instances2 - package_instances1)
281
282    desc = lambda fmt, instance: fmt.format(GetHalFormat(instance).name, *SplitInstance(instance))
283
284    if args.deprecated:
285      package_report += [desc("- {0} {2} can no longer be used", instance)
286                         for instance in deprecated]
287    if args.unchanged:
288      package_report += [desc("  {0} {2} is {3}", instance) for instance in unchanged]
289    if args.introduced:
290      package_report += [desc("+ {0} {2} is {3}", instance) for instance in introduced]
291
292    if package_report:
293      packages_report[package] = package_report
294
295  report = ["============",
296            "Level %s (%s) (against Level %s (%s))" % (level2, level_name2, level1, level_name1),
297            "============"]
298  for package, lines in sorted(packages_report.items()):
299    report.append(package)
300    report += [("    " + e) for e in lines]
301
302  return "\n".join(report)
303
304
305def main():
306  print("Generated with %s" % " ".join(sys.argv))
307  args = ParseArgs()
308  if args is None:
309    return 1
310  matrices = ReadMatrices(args)
311  sorted_matrices = sorted(matrices.items())
312  if not sorted_matrices:
313    logger.warning("Nothing to show, because no matrices found in '%s'.", args.input)
314  for tuple1, tuple2 in zip(sorted_matrices, sorted_matrices[1:]):
315    print(GetReport(tuple1, tuple2, args))
316  return 0
317
318
319if __name__ == "__main__":
320  sys.exit(main())
321