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
321 lines
12 KiB
4 months ago
|
#!/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())
|