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