| #!/usr/bin/env python3 |
| # |
| # Copyright (C) 2022 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. |
| """Utility functions to produce module or module type dependency graphs using json-module-graph or queryview.""" |
| |
| import collections |
| import dataclasses |
| import json |
| import os |
| import os.path |
| import subprocess |
| import sys |
| from typing import Dict, Optional, Set |
| import xml.etree.ElementTree |
| from bp2build_metrics_proto.bp2build_metrics_pb2 import Bp2BuildMetrics |
| |
| |
| @dataclasses.dataclass(frozen=True, order=True) |
| class TargetProduct: |
| product: Optional[str] = None |
| banchan_mode: bool = False |
| |
| |
| @dataclasses.dataclass(frozen=True, order=True) |
| class _ModuleKey: |
| """_ModuleKey uniquely identifies a module by name nad variations.""" |
| |
| name: str |
| variations: list |
| |
| def __str__(self): |
| return f"{self.name}, {self.variations}" |
| |
| def __hash__(self): |
| return (self.name + str(self.variations)).__hash__() |
| |
| |
| # This list of module types are omitted from the report and graph |
| # for brevity and simplicity. Presence in this list doesn't mean |
| # that they shouldn't be converted, but that they are not that useful |
| # to be recorded in the graph or report currently. |
| IGNORED_KINDS = set([ |
| "cc_defaults", |
| "cpython3_python_stdlib", |
| "hidl_package_root", # not being converted, contents converted as part of hidl_interface |
| "java_defaults", |
| "license", |
| "license_kind", |
| ]) |
| |
| # queryview doesn't have information on the type of deps, so we explicitly skip |
| # prebuilt types |
| _QUERYVIEW_IGNORE_KINDS = set([ |
| "android_app_import", |
| "android_library_import", |
| "cc_prebuilt_library", |
| "cc_prebuilt_library_headers", |
| "cc_prebuilt_library_shared", |
| "cc_prebuilt_library_static", |
| "cc_prebuilt_library_static", |
| "cc_prebuilt_object", |
| "java_import", |
| "java_import_host", |
| "java_sdk_library_import", |
| "cpython3_python_stdlib", |
| "cpython2_python_stdlib", |
| ]) |
| |
| |
| # Soong adds some dependencies that are handled by Bazel as part of the |
| # toolchain |
| _TOOLCHAIN_DEP_TYPES = frozenset([ |
| "python.dependencyTag {BaseDependencyTag:{} name:hostLauncher}", |
| "python.dependencyTag {BaseDependencyTag:{} name:hostLauncherSharedLib}", |
| "python.dependencyTag {BaseDependencyTag:{} name:hostStdLib}", |
| "python.dependencyTag {BaseDependencyTag:{} name:launcher}", |
| ( |
| "python.installDependencyTag {BaseDependencyTag:{}" |
| " InstallAlwaysNeededDependencyTag:{} name:launcherSharedLib}" |
| ), |
| ]) |
| |
| def get_src_root_dir() -> str: |
| # Search up the directory tree until we find soong_ui.bash as a regular file, not a symlink. |
| # This is so that we find the real source tree root, and not the bazel execroot which symlimks in |
| # soong_ui.bash. |
| def soong_ui(path): |
| return os.path.join(path, 'build/soong/soong_ui.bash') |
| |
| path = '.' |
| while not os.path.isfile(soong_ui(path)) or os.path.islink(soong_ui(path)): |
| if os.path.abspath(path) == '/': |
| sys.exit('Could not find android source tree root.') |
| path = os.path.join(path, '..') |
| return os.path.abspath(path) |
| |
| SRC_ROOT_DIR = get_src_root_dir() |
| |
| LUNCH_ENV = { |
| # Use aosp_arm as the canonical target product. |
| "TARGET_PRODUCT": "aosp_arm", |
| "TARGET_BUILD_VARIANT": "userdebug", |
| } |
| |
| BANCHAN_ENV = { |
| # Use module_arm64 as the canonical banchan target product. |
| "TARGET_PRODUCT": "module_arm64", |
| "TARGET_BUILD_VARIANT": "eng", |
| # just needs to be non-empty, not the specific module for Soong |
| # analysis purposes |
| "TARGET_BUILD_APPS": "all", |
| } |
| |
| _REQUIRED_PROPERTIES = [ |
| "Required", |
| "Host_required", |
| "Target_required", |
| ] |
| |
| |
| def _build_with_soong(target, target_product): |
| env = BANCHAN_ENV if target_product.banchan_mode else LUNCH_ENV |
| if target_product.product: |
| env["TARGET_PRODUCT"] = target_product.product |
| |
| subprocess.check_output( |
| [ |
| "build/soong/soong_ui.bash", |
| "--make-mode", |
| "--skip-soong-tests", |
| target, |
| ], |
| cwd=SRC_ROOT_DIR, |
| env=env, |
| ) |
| |
| |
| def get_properties(json_module): |
| set_properties = {} |
| if "Module" not in json_module: |
| return set_properties |
| if "Android" not in json_module["Module"]: |
| return set_properties |
| if "SetProperties" not in json_module["Module"]["Android"]: |
| return set_properties |
| if json_module["Module"]["Android"]["SetProperties"] is None: |
| return set_properties |
| |
| for prop in json_module["Module"]["Android"]["SetProperties"]: |
| if prop["Values"]: |
| value = prop["Values"] |
| else: |
| value = prop["Value"] |
| set_properties[prop["Name"]] = value |
| |
| return set_properties |
| |
| |
| def get_property_names(json_module): |
| return get_properties(json_module).keys() |
| |
| |
| def get_queryview_module_info_by_type(types, target_product): |
| """Returns the list of transitive dependencies of input module as built by queryview.""" |
| _build_with_soong("queryview", target_product) |
| |
| queryview_xml = subprocess.check_output( |
| [ |
| "build/bazel/bin/bazel", |
| "query", |
| "--config=ci", |
| "--config=queryview", |
| "--output=xml", |
| # union of queries to get the deps of all Soong modules with the give names |
| " + ".join( |
| f'deps(attr("soong_module_type", "^{t}$", //...))' for t in types |
| ), |
| ], |
| cwd=SRC_ROOT_DIR, |
| ) |
| try: |
| return xml.etree.ElementTree.fromstring(queryview_xml) |
| except xml.etree.ElementTree.ParseError as err: |
| sys.exit(f"""Could not parse XML: |
| {queryview_xml} |
| ParseError: {err}""") |
| |
| |
| def get_queryview_module_info(modules, target_product): |
| """Returns the list of transitive dependencies of input module as built by queryview.""" |
| _build_with_soong("queryview", target_product) |
| |
| queryview_xml = subprocess.check_output( |
| [ |
| "build/bazel/bin/bazel", |
| "query", |
| "--config=ci", |
| "--config=queryview", |
| "--output=xml", |
| # union of queries to get the deps of all Soong modules with the give names |
| " + ".join( |
| f'deps(attr("soong_module_name", "^{m}$", //...))' |
| for m in modules |
| ), |
| ], |
| cwd=SRC_ROOT_DIR, |
| ) |
| try: |
| return xml.etree.ElementTree.fromstring(queryview_xml) |
| except xml.etree.ElementTree.ParseError as err: |
| sys.exit(f"""Could not parse XML: |
| {queryview_xml} |
| ParseError: {err}""") |
| |
| |
| def get_json_module_info(target_product=None): |
| """Returns the list of transitive dependencies of input module as provided by Soong's json module graph.""" |
| _build_with_soong("json-module-graph", target_product) |
| try: |
| with open(os.path.join(SRC_ROOT_DIR, "out/soong/module-graph.json")) as f: |
| return json.load(f) |
| except json.JSONDecodeError as err: |
| sys.exit(f"""Could not decode json: |
| out/soong/module-graph.json |
| JSONDecodeError: {err}""") |
| |
| |
| def ignore_json_module(json_module, ignore_by_name): |
| # windows is not a priority currently |
| if is_windows_variation(json_module): |
| return True |
| if ignore_kind(json_module["Type"]): |
| return True |
| if json_module["Name"] in ignore_by_name: |
| return True |
| # for filegroups with a name the same as the source, we are not migrating the |
| # filegroup and instead just rely on the filename being exported |
| if json_module["Type"] == "filegroup": |
| set_properties = get_properties(json_module) |
| srcs = set_properties.get("Srcs", []) |
| if len(srcs) == 1: |
| return json_module["Name"] in srcs |
| return False |
| |
| |
| def visit_json_module_graph_post_order( |
| module_graph, ignore_by_name, ignore_java_auto_deps, filter_predicate, visit |
| ): |
| # The set of ignored modules. These modules (and their dependencies) are not shown |
| # in the graph or report. |
| ignored = set() |
| |
| # name to all module variants |
| module_graph_map = {} |
| root_module_keys = [] |
| name_to_keys = collections.defaultdict(list) |
| |
| # Do a single pass to find all top-level modules to be ignored |
| for module in module_graph: |
| name = module["Name"] |
| key = _ModuleKey(name, module["Variations"]) |
| if ignore_json_module(module, ignore_by_name): |
| ignored.add(key) |
| continue |
| name_to_keys[name].append(key) |
| module_graph_map[key] = module |
| if filter_predicate(module): |
| root_module_keys.append(key) |
| |
| visited = set() |
| |
| def json_module_graph_post_traversal(module_key): |
| if module_key in ignored or module_key in visited: |
| return |
| visited.add(module_key) |
| |
| deps = set() |
| module = module_graph_map[module_key] |
| created_by = module["CreatedBy"] |
| |
| extra_deps = [] |
| if created_by: |
| extra_deps.append(created_by) |
| |
| set_properties = get_properties(module) |
| for prop in set_properties.keys(): |
| for req in _REQUIRED_PROPERTIES: |
| if prop.endswith(req): |
| modules = set_properties.get(prop, []) |
| extra_deps.extend(modules) |
| |
| for m in extra_deps: |
| for key in name_to_keys.get(m, []): |
| if key in ignored: |
| continue |
| # treat created by as a dep so it appears as a blocker, otherwise the |
| # module will be disconnected from the traversal graph despite having a |
| # direct relationship to a module and must addressed in the migration |
| deps.add(m) |
| json_module_graph_post_traversal(key) |
| |
| # collect all variants and dependencies from those variants |
| # we want to visit all deps before other variants |
| all_variants = {} |
| all_deps = [] |
| for k in name_to_keys[module["Name"]]: |
| visited.add(k) |
| m = module_graph_map[k] |
| all_variants[k] = m |
| all_deps.extend(m["Deps"]) |
| |
| deps_visited = set() |
| for dep in all_deps: |
| dep_name = dep["Name"] |
| dep_key = _ModuleKey(dep_name, dep["Variations"]) |
| # only check if we need to ignore or visit each dep once but it might |
| # appear multiple times due to different variants |
| if dep_key in deps_visited: |
| continue |
| deps_visited.add(dep_key) |
| |
| if ignore_json_dep(dep, module["Name"], ignored, ignore_java_auto_deps): |
| continue |
| |
| deps.add(dep_name) |
| json_module_graph_post_traversal(dep_key) |
| |
| for k, m in all_variants.items(): |
| visit(m, deps) |
| |
| for module_key in root_module_keys: |
| json_module_graph_post_traversal(module_key) |
| |
| |
| QueryviewModule = collections.namedtuple( |
| "QueryviewModule", |
| [ |
| "name", |
| "kind", |
| "variant", |
| "dirname", |
| "deps", |
| "srcs", |
| ], |
| ) |
| |
| |
| def _bazel_target_to_dir(full_target): |
| dirname, _ = full_target.split(":") |
| return dirname[len("//") :] # discard prefix |
| |
| |
| def _get_queryview_module(name_with_variant, module, kind): |
| name = None |
| variant = "" |
| deps = [] |
| srcs = [] |
| for attr in module: |
| attr_name = attr.attrib["name"] |
| if attr.tag == "rule-input": |
| deps.append(attr_name) |
| elif attr_name == "soong_module_name": |
| name = attr.attrib["value"] |
| elif attr_name == "soong_module_variant": |
| variant = attr.attrib["value"] |
| elif attr_name == "soong_module_type" and kind == "generic_soong_module": |
| kind = attr.attrib["value"] |
| elif attr_name == "srcs": |
| for item in attr: |
| srcs.append(item.attrib["value"]) |
| |
| return QueryviewModule( |
| name=name, |
| kind=kind, |
| variant=variant, |
| dirname=_bazel_target_to_dir(name_with_variant), |
| deps=deps, |
| srcs=srcs, |
| ) |
| |
| |
| def _ignore_queryview_module(module, ignore_by_name): |
| if module.name in ignore_by_name: |
| return True |
| if ignore_kind(module.kind, queryview=True): |
| return True |
| # special handling for filegroup srcs, if a source has the same name as |
| # the filegroup module, we don't convert it |
| if module.kind == "filegroup" and module.name in module.srcs: |
| return True |
| return module.variant.startswith("windows") |
| |
| |
| def visit_queryview_xml_module_graph_post_order( |
| module_graph, ignored_by_name, filter_predicate, visit |
| ): |
| # The set of ignored modules. These modules (and their dependencies) are |
| # not shown in the graph or report. |
| ignored = set() |
| |
| # queryview embeds variant in long name, keep a map of the name with vaiarnt |
| # to just name |
| name_with_variant_to_name = dict() |
| |
| module_graph_map = dict() |
| to_visit = [] |
| |
| for module in module_graph: |
| ignore = False |
| if module.tag != "rule": |
| continue |
| kind = module.attrib["class"] |
| name_with_variant = module.attrib["name"] |
| |
| qv_module = _get_queryview_module(name_with_variant, module, kind) |
| |
| if _ignore_queryview_module(qv_module, ignored_by_name): |
| ignored.add(name_with_variant) |
| continue |
| |
| if filter_predicate(qv_module): |
| to_visit.append(name_with_variant) |
| |
| name_with_variant_to_name.setdefault(name_with_variant, qv_module.name) |
| module_graph_map[name_with_variant] = qv_module |
| |
| visited = set() |
| |
| def queryview_module_graph_post_traversal(name_with_variant): |
| module = module_graph_map[name_with_variant] |
| if name_with_variant in ignored or name_with_variant in visited: |
| return |
| visited.add(name_with_variant) |
| |
| name = name_with_variant_to_name[name_with_variant] |
| |
| deps = set() |
| for dep_name_with_variant in module.deps: |
| if dep_name_with_variant in ignored: |
| continue |
| dep_name = name_with_variant_to_name[dep_name_with_variant] |
| if dep_name == "prebuilt_" + name: |
| continue |
| if dep_name_with_variant not in visited: |
| queryview_module_graph_post_traversal(dep_name_with_variant) |
| |
| if name != dep_name: |
| deps.add(dep_name) |
| |
| visit(module, deps) |
| |
| for name_with_variant in to_visit: |
| queryview_module_graph_post_traversal(name_with_variant) |
| |
| |
| def get_bp2build_converted_modules(target_product) -> Dict[str, Set[str]]: |
| """Returns the list of modules that bp2build can currently convert.""" |
| _build_with_soong("bp2build", target_product) |
| # Parse the list of converted module names from bp2build |
| with open( |
| os.path.join( |
| SRC_ROOT_DIR, |
| "out/soong/soong_injection/metrics/converted_modules.json", |
| ), |
| "r", |
| ) as f: |
| converted_mods = json.loads(f.read()) |
| ret = collections.defaultdict(set) |
| for m in converted_mods: |
| ret[m["name"]].add(m["type"]) |
| return ret |
| |
| |
| def get_bp2build_metrics(bp2build_metrics_location): |
| """Returns the bp2build metrics""" |
| bp2build_metrics = Bp2BuildMetrics() |
| with open( |
| os.path.join(bp2build_metrics_location, "bp2build_metrics.pb"), "rb" |
| ) as f: |
| bp2build_metrics.ParseFromString(f.read()) |
| f.close() |
| return bp2build_metrics |
| |
| |
| def get_json_module_type_info(module_type, target_product=None): |
| """Returns the combined transitive dependency closures of all modules of module_type.""" |
| if target_product is None: |
| target_product = TargetProduct(banchan_mode=False) |
| _build_with_soong("json-module-graph", target_product) |
| # Run query.sh on the module graph for the top level module type |
| result = subprocess.check_output( |
| [ |
| "build/bazel/json_module_graph/query.sh", |
| "fullTransitiveModuleTypeDeps", |
| "out/soong/module-graph.json", |
| module_type, |
| ], |
| cwd=SRC_ROOT_DIR, |
| ) |
| return json.loads(result) |
| |
| |
| def is_windows_variation(module): |
| """Returns True if input module's variant is Windows. |
| |
| Args: |
| module: an entry parsed from Soong's json-module-graph |
| """ |
| dep_variations = module.get("Variations") |
| dep_variation_os = "" |
| if dep_variations != None: |
| for v in dep_variations: |
| if v["Mutator"] == "os": |
| dep_variation_os = v["Variation"] |
| return dep_variation_os == "windows" |
| |
| |
| def ignore_kind(kind, queryview=False): |
| if queryview and kind in _QUERYVIEW_IGNORE_KINDS: |
| return True |
| return kind in IGNORED_KINDS or "defaults" in kind |
| |
| |
| def is_prebuilt_to_source_dep(dep): |
| # Soong always adds a dependency from a source module to its corresponding |
| # prebuilt module, if it exists. |
| # https://cs.android.com/android/platform/superproject/+/master:build/soong/android/prebuilt.go;l=395-396;drc=5d6fa4d8571d01a6e5a63a8b7aa15e61f45737a9 |
| # This makes it appear that the prebuilt is a transitive dependency regardless |
| # of whether it is actually necessary. Skip these to keep the graph to modules |
| # used to build. |
| return dep["Tag"] == "android.prebuiltDependencyTag {BaseDependencyTag:{}}" |
| |
| |
| def _is_toolchain_dep(dep): |
| return dep["Tag"] in _TOOLCHAIN_DEP_TYPES |
| |
| |
| def _is_java_auto_dep(dep): |
| # Soong adds a number of dependencies automatically for Java deps, making it |
| # difficult to understand the actual dependencies, remove the |
| # non-user-specified deps |
| tag = dep["Tag"] |
| if not tag: |
| return False |
| |
| if tag.startswith("java.dependencyTag") and ( |
| "name:system modules" in tag or "name:bootclasspath" in tag |
| ): |
| name = dep["Name"] |
| # only remove automatically added bootclasspath/system modules |
| return ( |
| name |
| in frozenset([ |
| "core-lambda-stubs", |
| "core-module-lib-stubs-system-modules", |
| "core-public-stubs-system-modules", |
| "core-system-server-stubs-system-modules", |
| "core-system-stubs-system-modules", |
| "core-test-stubs-system-modules", |
| "core.current.stubs", |
| "legacy-core-platform-api-stubs-system-modules", |
| "legacy.core.platform.api.stubs", |
| "stable-core-platform-api-stubs-system-modules", |
| "stable.core.platform.api.stubs", |
| ]) |
| or (name.startswith("android_") and name.endswith("_stubs_current")) |
| or (name.startswith("sdk_") and name.endswith("_system_modules")) |
| ) |
| return ( |
| ( |
| tag.startswith("java.dependencyTag") |
| and ( |
| "name:proguard-raise" in tag |
| or "name:framework-res" in tag |
| or "name:sdklib" in tag |
| or "name:java9lib" in tag |
| ) |
| or ( |
| tag.startswith("java.usesLibraryDependencyTag") |
| or tag.startswith("java.hiddenAPIStubsDependencyTag") |
| ) |
| ) |
| or ( |
| tag.startswith("android.sdkMemberDependencyTag") |
| or tag.startswith("java.scopeDependencyTag") |
| ) |
| or tag.startswith("dexpreopt.dex2oatDependencyTag") |
| ) |
| |
| |
| def ignore_json_dep(dep, module_name, ignored_keys, ignore_java_auto_deps): |
| """Whether to ignore a json dependency based on heuristics. |
| |
| Args: |
| dep: dependency struct from an entry in Soogn's json-module-graph |
| module_name: name of the module this is a dependency of |
| ignored_names: a set of _ModuleKey to ignore |
| """ |
| if is_prebuilt_to_source_dep(dep): |
| return True |
| if _is_toolchain_dep(dep): |
| return True |
| elif dep["Name"] == "py3-stdlib": |
| return True |
| if ignore_java_auto_deps and _is_java_auto_dep(dep): |
| return True |
| name = dep["Name"] |
| return ( |
| _ModuleKey(name, dep["Variations"]) in ignored_keys or name == module_name |
| ) |