#!/usr/bin/env python3 # # Copyright (C) 2021 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """ Dump new HALs that are introduced in each FCM version in a human-readable format. Example: hals_for_release.py Show changes for each release, including new and deprecated HALs. hals_for_release.py -dua Show changes as well as unchanged HALs for each release. hals_for_release.py -i Show details about instance names and regex patterns as well. hals_for_release.py -p wifi Show changes of Wi-Fi HALs for each release. """ import argparse import collections import enum import logging import os import subprocess import sys logging.basicConfig(format="%(levelname)s: %(message)s") logger = logging.getLogger(__name__) def ParseArgs(): """ Parse arguments. :return: arguments. """ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawTextHelpFormatter) parser.add_argument("--analyze-matrix", help="Location of analyze_matrix") parser.add_argument("input", metavar="INPUT", nargs="?", help="Directory of compatibility matrices.") parser.add_argument("--deprecated", "-d", help="Show deprecated HALs. If none of deprecated, unchanged or introduced " "is specified, default is --deprecated and --introduced", action="store_true") parser.add_argument("--unchanged", "-u", help="Show unchanged HALs. If none of deprecated, unchanged or introduced " "is specified, default is --deprecated and --introduced", action="store_true") parser.add_argument("--introduced", "-a", help="Show deprecated HALs. If none of deprecated, unchanged or introduced " "is specified, default is --deprecated and --introduced", action="store_true") parser.add_argument("--instances", "-i", action="store_true", help="Show instance names and regex patterns as well") parser.add_argument("--packages", "-p", nargs="*", metavar="PACKAGE", help="Only print HALs where package contains the given substring. " "E.g. wifi, usb, health. Recommend to use with --unchanged.") parser.add_argument("--verbose", "-v", action="store_true", help="Verbose mode") args = parser.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) if not args.deprecated and not args.unchanged and not args.introduced: args.deprecated = args.introduced = True host_out = os.environ.get("ANDROID_HOST_OUT") if host_out and not args.analyze_matrix: analyze_matrix = os.path.join(host_out, "bin", "analyze_matrix") if os.path.isfile(analyze_matrix): args.analyze_matrix = analyze_matrix if not args.analyze_matrix: args.analyze_matrix = "analyze_matrix" top = os.environ.get("ANDROID_BUILD_TOP") if top and not args.input: args.input = os.path.join(top, "hardware", "interfaces", "compatibility_matrices") if not args.input: logger.fatal("Unable to determine compatibility matrix dir, lunch or provide one explicitly.") return None logger.debug("Using analyze_matrix at path: %s", args.analyze_matrix) logger.debug("Dumping compatibility matrices at path: %s", args.input) logger.debug("Show deprecated HALs? %s", args.deprecated) logger.debug("Show unchanged HALs? %s", args.unchanged) logger.debug("Show introduced HALs? %s", args.introduced) logger.debug("Only showing packages %s", args.packages) return args def Analyze(analyze_matrix, file, args, ignore_errors=False): """ Run analyze_matrix with :param analyze_matrix: path of analyze_matrix :param file: input file :param arg: argument to analyze_matrix, e.g. "level" :param ignore_errors: Whether errors during execution should be rased :return: output of analyze_matrix """ command = [analyze_matrix, "--input", file] + args proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL if ignore_errors else subprocess.PIPE) if not ignore_errors and proc.returncode != 0: logger.warning("`%s` exits with code %d with the following error: %s", " ".join(command), proc.returncode, proc.stderr) proc.check_returncode() return proc.stdout.decode().strip() def GetLevel(analyze_matrix, file): """ :param analyze_matrix: Path of analyze_matrix :param file: a file, possibly a compatibility matrix :return: If it is a compatibility matrix, return an integer indicating the level. If it is not a compatibility matrix, returns None. For matrices with empty level, return None. """ output = Analyze(analyze_matrix, file, ["--level"], ignore_errors=True) # Ignore empty level matrices and non-matrices if not output: return None try: return int(output) except ValueError: logger.warning("Unknown level '%s' in file: %s", output, file) return None def GetLevelName(analyze_matrix, file): """ :param analyze_matrix: Path of analyze_matrix :param file: a file, possibly a compatibility matrix :return: If it is a compatibility matrix, return the level name. If it is not a compatibility matrix, returns None. For matrices with empty level, return "Level unspecified". """ return Analyze(analyze_matrix, file, ["--level-name"], ignore_errors=True) def ReadMatrices(args): """ :param args: parsed arguments from ParseArgs :return: A dictionary. Key is an integer indicating the matrix level. Value is (level name, a set of instances in that matrix). """ matrices = dict() for child in os.listdir(args.input): file = os.path.join(args.input, child) level, level_name = GetLevel(args.analyze_matrix, file), GetLevelName(args.analyze_matrix, file) if level is None: logger.debug("Ignoring file %s", file) continue action = "--instances" if args.instances else "--interfaces" instances = Analyze(args.analyze_matrix, file, [action, "--requirement"]).split("\n") instances = set(map(str.strip, instances)) - {""} if level in matrices: logger.warning("Found duplicated matrix for level %s, ignoring: %s", level, file) continue matrices[level] = (level_name, instances) return matrices class HalFormat(enum.Enum): HIDL = 0 AIDL = 2 def GetHalFormat(instance): """ Guess the HAL format of instance. :param instance: two formats: android.hardware.health.storage@1.0::IStorage/default optional android.hardware.health.storage.IStorage/default (@1) optional :return: HalFormat.HIDL for the first one, HalFormat.AIDL for the second. >>> str(GetHalFormat("android.hardware.health.storage@1.0::IStorage/default optional")) 'HalFormat.HIDL' >>> str(GetHalFormat("android.hardware.health.storage.IStorage/default (@1) optional")) 'HalFormat.AIDL' """ return HalFormat.HIDL if "::" in instance else HalFormat.AIDL def SplitInstance(instance): """ Split instance into parts. :param instance: :param instance: two formats: android.hardware.health.storage@1.0::IStorage/default optional android.hardware.health.storage.IStorage/default (@1) optional :return: (package, version+interface+instance, requirement) >>> SplitInstance("android.hardware.health.storage@1.0::IStorage/default optional") ('android.hardware.health.storage', '@1.0::IStorage/default', 'optional') >>> SplitInstance("android.hardware.health.storage.IStorage/default (@1) optional") ('android.hardware.health.storage', 'IStorage/default (@1)', 'optional') """ format = GetHalFormat(instance) if format == HalFormat.HIDL: atPos = instance.find("@") spacePos = instance.rfind(" ") return instance[:atPos], instance[atPos:spacePos], instance[spacePos + 1:] elif format == HalFormat.AIDL: dotPos = instance.rfind(".") spacePos = instance.rfind(" ") return instance[:dotPos], instance[dotPos + 1:spacePos], instance[spacePos + 1:] def GetPackage(instance): """ Guess the package of instance. :param instance: two formats: android.hardware.health.storage@1.0::IStorage/default android.hardware.health.storage.IStorage/default (@1) :return: The package. In the above example, return android.hardware.health.storage >>> GetPackage("android.hardware.health.storage@1.0::IStorage/default") 'android.hardware.health.storage' >>> GetPackage("android.hardware.health.storage.IStorage/default (@1)") 'android.hardware.health.storage' """ return SplitInstance(instance)[0] def KeyOnPackage(instances): """ :param instances: A list of instances. :return: A dictionary, where key is the package (see GetPackage), and value is a list of instances in the provided list, where GetPackage(instance) is the corresponding key. """ d = collections.defaultdict(list) for instance in instances: package = GetPackage(instance) d[package].append(instance) return d def GetReport(tuple1, tuple2, args): """ :param tuple1: (level, (level_name, Set of instances from the first matrix)) :param tuple2: (level, (level_name, Set of instances from the second matrix)) :return: A human-readable report of their difference. """ level1, (level_name1, instances1) = tuple1 level2, (level_name2, instances2) = tuple2 instances_by_package1 = KeyOnPackage(instances1) instances_by_package2 = KeyOnPackage(instances2) all_packages = set(instances_by_package1.keys()) | set(instances_by_package2.keys()) if args.packages: package_matches = lambda package: any(pattern in package for pattern in args.packages) all_packages = filter(package_matches, all_packages) packages_report = dict() for package in all_packages: package_instances1 = set(instances_by_package1.get(package, [])) package_instances2 = set(instances_by_package2.get(package, [])) package_report = [] deprecated = sorted(package_instances1 - package_instances2) unchanged = sorted(package_instances1 & package_instances2) introduced = sorted(package_instances2 - package_instances1) desc = lambda fmt, instance: fmt.format(GetHalFormat(instance).name, *SplitInstance(instance)) if args.deprecated: package_report += [desc("- {0} {2} can no longer be used", instance) for instance in deprecated] if args.unchanged: package_report += [desc(" {0} {2} is {3}", instance) for instance in unchanged] if args.introduced: package_report += [desc("+ {0} {2} is {3}", instance) for instance in introduced] if package_report: packages_report[package] = package_report report = ["============", "Level %s (%s) (against Level %s (%s))" % (level2, level_name2, level1, level_name1), "============"] for package, lines in sorted(packages_report.items()): report.append(package) report += [(" " + e) for e in lines] return "\n".join(report) def main(): print("Generated with %s" % " ".join(sys.argv)) args = ParseArgs() if args is None: return 1 matrices = ReadMatrices(args) sorted_matrices = sorted(matrices.items()) if not sorted_matrices: logger.warning("Nothing to show, because no matrices found in '%s'.", args.input) for tuple1, tuple2 in zip(sorted_matrices, sorted_matrices[1:]): print(GetReport(tuple1, tuple2, args)) return 0 if __name__ == "__main__": sys.exit(main())