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