You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

321 lines
12 KiB

#!/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())