1#!/usr/bin/env python3 2# 3# Copyright (C) 2022 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"""Utility functions to produce module or module type dependency graphs using json-module-graph or queryview.""" 17 18import collections 19import dataclasses 20import json 21import os 22import os.path 23import subprocess 24import sys 25from typing import Dict, Optional, Set 26import xml.etree.ElementTree 27from bp2build_metrics_proto.bp2build_metrics_pb2 import Bp2BuildMetrics 28 29 30@dataclasses.dataclass(frozen=True, order=True) 31class TargetProduct: 32 product: Optional[str] = None 33 banchan_mode: bool = False 34 35 36@dataclasses.dataclass(frozen=True, order=True) 37class _ModuleKey: 38 """_ModuleKey uniquely identifies a module by name nad variations.""" 39 40 name: str 41 variations: list 42 43 def __str__(self): 44 return f"{self.name}, {self.variations}" 45 46 def __hash__(self): 47 return (self.name + str(self.variations)).__hash__() 48 49 50# This list of module types are omitted from the report and graph 51# for brevity and simplicity. Presence in this list doesn't mean 52# that they shouldn't be converted, but that they are not that useful 53# to be recorded in the graph or report currently. 54IGNORED_KINDS = set([ 55 "cc_defaults", 56 "cpython3_python_stdlib", 57 "hidl_package_root", # not being converted, contents converted as part of hidl_interface 58 "java_defaults", 59 "license", 60 "license_kind", 61]) 62 63# queryview doesn't have information on the type of deps, so we explicitly skip 64# prebuilt types 65_QUERYVIEW_IGNORE_KINDS = set([ 66 "android_app_import", 67 "android_library_import", 68 "cc_prebuilt_library", 69 "cc_prebuilt_library_headers", 70 "cc_prebuilt_library_shared", 71 "cc_prebuilt_library_static", 72 "cc_prebuilt_library_static", 73 "cc_prebuilt_object", 74 "java_import", 75 "java_import_host", 76 "java_sdk_library_import", 77 "cpython3_python_stdlib", 78 "cpython2_python_stdlib", 79]) 80 81 82# Soong adds some dependencies that are handled by Bazel as part of the 83# toolchain 84_TOOLCHAIN_DEP_TYPES = frozenset([ 85 "python.dependencyTag {BaseDependencyTag:{} name:hostLauncher}", 86 "python.dependencyTag {BaseDependencyTag:{} name:hostLauncherSharedLib}", 87 "python.dependencyTag {BaseDependencyTag:{} name:hostStdLib}", 88 "python.dependencyTag {BaseDependencyTag:{} name:launcher}", 89 ( 90 "python.installDependencyTag {BaseDependencyTag:{}" 91 " InstallAlwaysNeededDependencyTag:{} name:launcherSharedLib}" 92 ), 93]) 94 95def get_src_root_dir() -> str: 96 # Search up the directory tree until we find soong_ui.bash as a regular file, not a symlink. 97 # This is so that we find the real source tree root, and not the bazel execroot which symlimks in 98 # soong_ui.bash. 99 def soong_ui(path): 100 return os.path.join(path, 'build/soong/soong_ui.bash') 101 102 path = '.' 103 while not os.path.isfile(soong_ui(path)) or os.path.islink(soong_ui(path)): 104 if os.path.abspath(path) == '/': 105 sys.exit('Could not find android source tree root.') 106 path = os.path.join(path, '..') 107 return os.path.abspath(path) 108 109SRC_ROOT_DIR = get_src_root_dir() 110 111LUNCH_ENV = { 112 # Use aosp_arm as the canonical target product. 113 "TARGET_PRODUCT": "aosp_arm", 114 "TARGET_BUILD_VARIANT": "userdebug", 115} 116 117BANCHAN_ENV = { 118 # Use module_arm64 as the canonical banchan target product. 119 "TARGET_PRODUCT": "module_arm64", 120 "TARGET_BUILD_VARIANT": "eng", 121 # just needs to be non-empty, not the specific module for Soong 122 # analysis purposes 123 "TARGET_BUILD_APPS": "all", 124} 125 126_REQUIRED_PROPERTIES = [ 127 "Required", 128 "Host_required", 129 "Target_required", 130] 131 132 133def _build_with_soong(target, target_product): 134 env = BANCHAN_ENV if target_product.banchan_mode else LUNCH_ENV 135 if target_product.product: 136 env["TARGET_PRODUCT"] = target_product.product 137 138 subprocess.check_output( 139 [ 140 "build/soong/soong_ui.bash", 141 "--make-mode", 142 "--skip-soong-tests", 143 target, 144 ], 145 cwd=SRC_ROOT_DIR, 146 env=env, 147 ) 148 149 150def get_properties(json_module): 151 set_properties = {} 152 if "Module" not in json_module: 153 return set_properties 154 if "Android" not in json_module["Module"]: 155 return set_properties 156 if "SetProperties" not in json_module["Module"]["Android"]: 157 return set_properties 158 if json_module["Module"]["Android"]["SetProperties"] is None: 159 return set_properties 160 161 for prop in json_module["Module"]["Android"]["SetProperties"]: 162 if prop["Values"]: 163 value = prop["Values"] 164 else: 165 value = prop["Value"] 166 set_properties[prop["Name"]] = value 167 168 return set_properties 169 170 171def get_property_names(json_module): 172 return get_properties(json_module).keys() 173 174 175def get_queryview_module_info_by_type(types, target_product): 176 """Returns the list of transitive dependencies of input module as built by queryview.""" 177 _build_with_soong("queryview", target_product) 178 179 queryview_xml = subprocess.check_output( 180 [ 181 "build/bazel/bin/bazel", 182 "query", 183 "--config=ci", 184 "--config=queryview", 185 "--output=xml", 186 # union of queries to get the deps of all Soong modules with the give names 187 " + ".join( 188 f'deps(attr("soong_module_type", "^{t}$", //...))' for t in types 189 ), 190 ], 191 cwd=SRC_ROOT_DIR, 192 ) 193 try: 194 return xml.etree.ElementTree.fromstring(queryview_xml) 195 except xml.etree.ElementTree.ParseError as err: 196 sys.exit(f"""Could not parse XML: 197{queryview_xml} 198ParseError: {err}""") 199 200 201def get_queryview_module_info(modules, target_product): 202 """Returns the list of transitive dependencies of input module as built by queryview.""" 203 _build_with_soong("queryview", target_product) 204 205 queryview_xml = subprocess.check_output( 206 [ 207 "build/bazel/bin/bazel", 208 "query", 209 "--config=ci", 210 "--config=queryview", 211 "--output=xml", 212 # union of queries to get the deps of all Soong modules with the give names 213 " + ".join( 214 f'deps(attr("soong_module_name", "^{m}$", //...))' 215 for m in modules 216 ), 217 ], 218 cwd=SRC_ROOT_DIR, 219 ) 220 try: 221 return xml.etree.ElementTree.fromstring(queryview_xml) 222 except xml.etree.ElementTree.ParseError as err: 223 sys.exit(f"""Could not parse XML: 224{queryview_xml} 225ParseError: {err}""") 226 227 228def get_json_module_info(target_product=None): 229 """Returns the list of transitive dependencies of input module as provided by Soong's json module graph.""" 230 _build_with_soong("json-module-graph", target_product) 231 try: 232 with open(os.path.join(SRC_ROOT_DIR, "out/soong/module-graph.json")) as f: 233 return json.load(f) 234 except json.JSONDecodeError as err: 235 sys.exit(f"""Could not decode json: 236out/soong/module-graph.json 237JSONDecodeError: {err}""") 238 239 240def ignore_json_module(json_module, ignore_by_name): 241 # windows is not a priority currently 242 if is_windows_variation(json_module): 243 return True 244 if ignore_kind(json_module["Type"]): 245 return True 246 if json_module["Name"] in ignore_by_name: 247 return True 248 # for filegroups with a name the same as the source, we are not migrating the 249 # filegroup and instead just rely on the filename being exported 250 if json_module["Type"] == "filegroup": 251 set_properties = get_properties(json_module) 252 srcs = set_properties.get("Srcs", []) 253 if len(srcs) == 1: 254 return json_module["Name"] in srcs 255 return False 256 257 258def visit_json_module_graph_post_order( 259 module_graph, ignore_by_name, ignore_java_auto_deps, filter_predicate, visit 260): 261 # The set of ignored modules. These modules (and their dependencies) are not shown 262 # in the graph or report. 263 ignored = set() 264 265 # name to all module variants 266 module_graph_map = {} 267 root_module_keys = [] 268 name_to_keys = collections.defaultdict(list) 269 270 # Do a single pass to find all top-level modules to be ignored 271 for module in module_graph: 272 name = module["Name"] 273 key = _ModuleKey(name, module["Variations"]) 274 if ignore_json_module(module, ignore_by_name): 275 ignored.add(key) 276 continue 277 name_to_keys[name].append(key) 278 module_graph_map[key] = module 279 if filter_predicate(module): 280 root_module_keys.append(key) 281 282 visited = set() 283 284 def json_module_graph_post_traversal(module_key): 285 if module_key in ignored or module_key in visited: 286 return 287 visited.add(module_key) 288 289 deps = set() 290 module = module_graph_map[module_key] 291 created_by = module["CreatedBy"] 292 293 extra_deps = [] 294 if created_by: 295 extra_deps.append(created_by) 296 297 set_properties = get_properties(module) 298 for prop in set_properties.keys(): 299 for req in _REQUIRED_PROPERTIES: 300 if prop.endswith(req): 301 modules = set_properties.get(prop, []) 302 extra_deps.extend(modules) 303 304 for m in extra_deps: 305 for key in name_to_keys.get(m, []): 306 if key in ignored: 307 continue 308 # treat created by as a dep so it appears as a blocker, otherwise the 309 # module will be disconnected from the traversal graph despite having a 310 # direct relationship to a module and must addressed in the migration 311 deps.add(m) 312 json_module_graph_post_traversal(key) 313 314 # collect all variants and dependencies from those variants 315 # we want to visit all deps before other variants 316 all_variants = {} 317 all_deps = [] 318 for k in name_to_keys[module["Name"]]: 319 visited.add(k) 320 m = module_graph_map[k] 321 all_variants[k] = m 322 all_deps.extend(m["Deps"]) 323 324 deps_visited = set() 325 for dep in all_deps: 326 dep_name = dep["Name"] 327 dep_key = _ModuleKey(dep_name, dep["Variations"]) 328 # only check if we need to ignore or visit each dep once but it might 329 # appear multiple times due to different variants 330 if dep_key in deps_visited: 331 continue 332 deps_visited.add(dep_key) 333 334 if ignore_json_dep(dep, module["Name"], ignored, ignore_java_auto_deps): 335 continue 336 337 deps.add(dep_name) 338 json_module_graph_post_traversal(dep_key) 339 340 for k, m in all_variants.items(): 341 visit(m, deps) 342 343 for module_key in root_module_keys: 344 json_module_graph_post_traversal(module_key) 345 346 347QueryviewModule = collections.namedtuple( 348 "QueryviewModule", 349 [ 350 "name", 351 "kind", 352 "variant", 353 "dirname", 354 "deps", 355 "srcs", 356 ], 357) 358 359 360def _bazel_target_to_dir(full_target): 361 dirname, _ = full_target.split(":") 362 return dirname[len("//") :] # discard prefix 363 364 365def _get_queryview_module(name_with_variant, module, kind): 366 name = None 367 variant = "" 368 deps = [] 369 srcs = [] 370 for attr in module: 371 attr_name = attr.attrib["name"] 372 if attr.tag == "rule-input": 373 deps.append(attr_name) 374 elif attr_name == "soong_module_name": 375 name = attr.attrib["value"] 376 elif attr_name == "soong_module_variant": 377 variant = attr.attrib["value"] 378 elif attr_name == "soong_module_type" and kind == "generic_soong_module": 379 kind = attr.attrib["value"] 380 elif attr_name == "srcs": 381 for item in attr: 382 srcs.append(item.attrib["value"]) 383 384 return QueryviewModule( 385 name=name, 386 kind=kind, 387 variant=variant, 388 dirname=_bazel_target_to_dir(name_with_variant), 389 deps=deps, 390 srcs=srcs, 391 ) 392 393 394def _ignore_queryview_module(module, ignore_by_name): 395 if module.name in ignore_by_name: 396 return True 397 if ignore_kind(module.kind, queryview=True): 398 return True 399 # special handling for filegroup srcs, if a source has the same name as 400 # the filegroup module, we don't convert it 401 if module.kind == "filegroup" and module.name in module.srcs: 402 return True 403 return module.variant.startswith("windows") 404 405 406def visit_queryview_xml_module_graph_post_order( 407 module_graph, ignored_by_name, filter_predicate, visit 408): 409 # The set of ignored modules. These modules (and their dependencies) are 410 # not shown in the graph or report. 411 ignored = set() 412 413 # queryview embeds variant in long name, keep a map of the name with vaiarnt 414 # to just name 415 name_with_variant_to_name = dict() 416 417 module_graph_map = dict() 418 to_visit = [] 419 420 for module in module_graph: 421 ignore = False 422 if module.tag != "rule": 423 continue 424 kind = module.attrib["class"] 425 name_with_variant = module.attrib["name"] 426 427 qv_module = _get_queryview_module(name_with_variant, module, kind) 428 429 if _ignore_queryview_module(qv_module, ignored_by_name): 430 ignored.add(name_with_variant) 431 continue 432 433 if filter_predicate(qv_module): 434 to_visit.append(name_with_variant) 435 436 name_with_variant_to_name.setdefault(name_with_variant, qv_module.name) 437 module_graph_map[name_with_variant] = qv_module 438 439 visited = set() 440 441 def queryview_module_graph_post_traversal(name_with_variant): 442 module = module_graph_map[name_with_variant] 443 if name_with_variant in ignored or name_with_variant in visited: 444 return 445 visited.add(name_with_variant) 446 447 name = name_with_variant_to_name[name_with_variant] 448 449 deps = set() 450 for dep_name_with_variant in module.deps: 451 if dep_name_with_variant in ignored: 452 continue 453 dep_name = name_with_variant_to_name[dep_name_with_variant] 454 if dep_name == "prebuilt_" + name: 455 continue 456 if dep_name_with_variant not in visited: 457 queryview_module_graph_post_traversal(dep_name_with_variant) 458 459 if name != dep_name: 460 deps.add(dep_name) 461 462 visit(module, deps) 463 464 for name_with_variant in to_visit: 465 queryview_module_graph_post_traversal(name_with_variant) 466 467 468def get_bp2build_converted_modules(target_product) -> Dict[str, Set[str]]: 469 """Returns the list of modules that bp2build can currently convert.""" 470 _build_with_soong("bp2build", target_product) 471 # Parse the list of converted module names from bp2build 472 with open( 473 os.path.join( 474 SRC_ROOT_DIR, 475 "out/soong/soong_injection/metrics/converted_modules.json", 476 ), 477 "r", 478 ) as f: 479 converted_mods = json.loads(f.read()) 480 ret = collections.defaultdict(set) 481 for m in converted_mods: 482 ret[m["name"]].add(m["type"]) 483 return ret 484 485 486def get_bp2build_metrics(bp2build_metrics_location): 487 """Returns the bp2build metrics""" 488 bp2build_metrics = Bp2BuildMetrics() 489 with open( 490 os.path.join(bp2build_metrics_location, "bp2build_metrics.pb"), "rb" 491 ) as f: 492 bp2build_metrics.ParseFromString(f.read()) 493 f.close() 494 return bp2build_metrics 495 496 497def get_json_module_type_info(module_type, target_product=None): 498 """Returns the combined transitive dependency closures of all modules of module_type.""" 499 if target_product is None: 500 target_product = TargetProduct(banchan_mode=False) 501 _build_with_soong("json-module-graph", target_product) 502 # Run query.sh on the module graph for the top level module type 503 result = subprocess.check_output( 504 [ 505 "build/bazel/json_module_graph/query.sh", 506 "fullTransitiveModuleTypeDeps", 507 "out/soong/module-graph.json", 508 module_type, 509 ], 510 cwd=SRC_ROOT_DIR, 511 ) 512 return json.loads(result) 513 514 515def is_windows_variation(module): 516 """Returns True if input module's variant is Windows. 517 518 Args: 519 module: an entry parsed from Soong's json-module-graph 520 """ 521 dep_variations = module.get("Variations") 522 dep_variation_os = "" 523 if dep_variations != None: 524 for v in dep_variations: 525 if v["Mutator"] == "os": 526 dep_variation_os = v["Variation"] 527 return dep_variation_os == "windows" 528 529 530def ignore_kind(kind, queryview=False): 531 if queryview and kind in _QUERYVIEW_IGNORE_KINDS: 532 return True 533 return kind in IGNORED_KINDS or "defaults" in kind 534 535 536def is_prebuilt_to_source_dep(dep): 537 # Soong always adds a dependency from a source module to its corresponding 538 # prebuilt module, if it exists. 539 # https://cs.android.com/android/platform/superproject/+/master:build/soong/android/prebuilt.go;l=395-396;drc=5d6fa4d8571d01a6e5a63a8b7aa15e61f45737a9 540 # This makes it appear that the prebuilt is a transitive dependency regardless 541 # of whether it is actually necessary. Skip these to keep the graph to modules 542 # used to build. 543 return dep["Tag"] == "android.prebuiltDependencyTag {BaseDependencyTag:{}}" 544 545 546def _is_toolchain_dep(dep): 547 return dep["Tag"] in _TOOLCHAIN_DEP_TYPES 548 549 550def _is_java_auto_dep(dep): 551 # Soong adds a number of dependencies automatically for Java deps, making it 552 # difficult to understand the actual dependencies, remove the 553 # non-user-specified deps 554 tag = dep["Tag"] 555 if not tag: 556 return False 557 558 if tag.startswith("java.dependencyTag") and ( 559 "name:system modules" in tag or "name:bootclasspath" in tag 560 ): 561 name = dep["Name"] 562 # only remove automatically added bootclasspath/system modules 563 return ( 564 name 565 in frozenset([ 566 "core-lambda-stubs", 567 "core-module-lib-stubs-system-modules", 568 "core-public-stubs-system-modules", 569 "core-system-server-stubs-system-modules", 570 "core-system-stubs-system-modules", 571 "core-test-stubs-system-modules", 572 "core.current.stubs", 573 "legacy-core-platform-api-stubs-system-modules", 574 "legacy.core.platform.api.stubs", 575 "stable-core-platform-api-stubs-system-modules", 576 "stable.core.platform.api.stubs", 577 ]) 578 or (name.startswith("android_") and name.endswith("_stubs_current")) 579 or (name.startswith("sdk_") and name.endswith("_system_modules")) 580 ) 581 return ( 582 ( 583 tag.startswith("java.dependencyTag") 584 and ( 585 "name:proguard-raise" in tag 586 or "name:framework-res" in tag 587 or "name:sdklib" in tag 588 or "name:java9lib" in tag 589 ) 590 or ( 591 tag.startswith("java.usesLibraryDependencyTag") 592 or tag.startswith("java.hiddenAPIStubsDependencyTag") 593 ) 594 ) 595 or ( 596 tag.startswith("android.sdkMemberDependencyTag") 597 or tag.startswith("java.scopeDependencyTag") 598 ) 599 or tag.startswith("dexpreopt.dex2oatDependencyTag") 600 ) 601 602 603def ignore_json_dep(dep, module_name, ignored_keys, ignore_java_auto_deps): 604 """Whether to ignore a json dependency based on heuristics. 605 606 Args: 607 dep: dependency struct from an entry in Soogn's json-module-graph 608 module_name: name of the module this is a dependency of 609 ignored_names: a set of _ModuleKey to ignore 610 """ 611 if is_prebuilt_to_source_dep(dep): 612 return True 613 if _is_toolchain_dep(dep): 614 return True 615 elif dep["Name"] == "py3-stdlib": 616 return True 617 if ignore_java_auto_deps and _is_java_auto_dep(dep): 618 return True 619 name = dep["Name"] 620 return ( 621 _ModuleKey(name, dep["Variations"]) in ignored_keys or name == module_name 622 ) 623