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 18from typing import Set 19import collections 20import dataclasses 21import json 22import os 23import os.path 24import subprocess 25import sys 26import xml.etree.ElementTree 27 28 29@dataclasses.dataclass(frozen=True, order=True) 30class _ModuleKey: 31 """_ModuleKey uniquely identifies a module by name nad variations.""" 32 name: str 33 variations: list 34 35 def __str__(self): 36 return f"{self.name}, {self.variations}" 37 38 def __hash__(self): 39 return (self.name + str(self.variations)).__hash__() 40 41 42# This list of module types are omitted from the report and graph 43# for brevity and simplicity. Presence in this list doesn't mean 44# that they shouldn't be converted, but that they are not that useful 45# to be recorded in the graph or report currently. 46IGNORED_KINDS = set([ 47 "cc_defaults", 48 "hidl_package_root", # not being converted, contents converted as part of hidl_interface 49 "java_defaults", 50 "license", 51 "license_kind", 52]) 53 54# queryview doesn't have information on the type of deps, so we explicitly skip 55# prebuilt types 56_QUERYVIEW_IGNORE_KINDS = set([ 57 "android_app_import", 58 "android_library_import", 59 "cc_prebuilt_library", 60 "cc_prebuilt_library_headers", 61 "cc_prebuilt_library_shared", 62 "cc_prebuilt_library_static", 63 "cc_prebuilt_library_static", 64 "cc_prebuilt_object", 65 "java_import", 66 "java_import_host", 67 "java_sdk_library_import", 68]) 69 70SRC_ROOT_DIR = os.path.abspath(__file__ + "/../../../../..") 71 72LUNCH_ENV = { 73 # Use aosp_arm as the canonical target product. 74 "TARGET_PRODUCT": "aosp_arm", 75 "TARGET_BUILD_VARIANT": "userdebug", 76} 77 78BANCHAN_ENV = { 79 # Use module_arm64 as the canonical banchan target product. 80 "TARGET_PRODUCT": "module_arm64", 81 "TARGET_BUILD_VARIANT": "eng", 82 # just needs to be non-empty, not the specific module for Soong 83 # analysis purposes 84 "TARGET_BUILD_APPS": "all", 85} 86 87 88def _build_with_soong(target, banchan_mode=False): 89 subprocess.check_output( 90 [ 91 "build/soong/soong_ui.bash", 92 "--make-mode", 93 "--skip-soong-tests", 94 target, 95 ], 96 cwd=SRC_ROOT_DIR, 97 env=BANCHAN_ENV if banchan_mode else LUNCH_ENV, 98 ) 99 100 101def get_properties(json_module): 102 set_properties = {} 103 if "Module" not in json_module: 104 return set_properties 105 if "Android" not in json_module["Module"]: 106 return set_properties 107 if "SetProperties" not in json_module["Module"]["Android"]: 108 return set_properties 109 110 for prop in json_module['Module']['Android']['SetProperties']: 111 if prop["Values"]: 112 value = prop["Values"] 113 else: 114 value = prop["Value"] 115 set_properties[prop["Name"]] = value 116 return set_properties 117 118 119def get_property_names(json_module): 120 return get_properties(json_module).keys() 121 122 123def get_queryview_module_info(modules, banchan_mode): 124 """Returns the list of transitive dependencies of input module as built by queryview.""" 125 _build_with_soong("queryview", banchan_mode) 126 127 queryview_xml = subprocess.check_output( 128 [ 129 "build/bazel/bin/bazel", 130 "query", 131 "--config=ci", 132 "--config=queryview", 133 "--output=xml", 134 # union of queries to get the deps of all Soong modules with the give names 135 " + ".join(f'deps(attr("soong_module_name", "^{m}$", //...))' 136 for m in modules) 137 ], 138 cwd=SRC_ROOT_DIR, 139 ) 140 try: 141 return xml.etree.ElementTree.fromstring(queryview_xml) 142 except xml.etree.ElementTree.ParseError as err: 143 sys.exit(f"""Could not parse XML: 144{queryview_xml} 145ParseError: {err}""") 146 147 148def get_json_module_info(banchan_mode=False): 149 """Returns the list of transitive dependencies of input module as provided by Soong's json module graph.""" 150 _build_with_soong("json-module-graph", banchan_mode) 151 try: 152 with open(os.path.join(SRC_ROOT_DIR,"out/soong/module-graph.json")) as f: 153 return json.load(f) 154 except json.JSONDecodeError as err: 155 sys.exit(f"""Could not decode json: 156out/soong/module-graph.json 157JSONDecodeError: {err}""") 158 159 160def _ignore_json_module(json_module, ignore_by_name): 161 # windows is not a priority currently 162 if is_windows_variation(json_module): 163 return True 164 if ignore_kind(json_module['Type']): 165 return True 166 if json_module['Name'] in ignore_by_name: 167 return True 168 # for filegroups with a name the same as the source, we are not migrating the 169 # filegroup and instead just rely on the filename being exported 170 if json_module['Type'] == 'filegroup': 171 set_properties = get_properties(json_module) 172 srcs = set_properties.get('Srcs', []) 173 if len(srcs) == 1: 174 return json_module['Name'] in srcs 175 return False 176 177 178def visit_json_module_graph_post_order( 179 module_graph, ignore_by_name, ignore_java_auto_deps, filter_predicate, visit 180): 181 # The set of ignored modules. These modules (and their dependencies) are not shown 182 # in the graph or report. 183 ignored = set() 184 185 # name to all module variants 186 module_graph_map = {} 187 root_module_keys = [] 188 name_to_keys = collections.defaultdict(set) 189 190 # Do a single pass to find all top-level modules to be ignored 191 for module in module_graph: 192 name = module["Name"] 193 key = _ModuleKey(name, module["Variations"]) 194 if _ignore_json_module(module, ignore_by_name): 195 ignored.add(key) 196 continue 197 name_to_keys[name].add(key) 198 module_graph_map[key] = module 199 if filter_predicate(module): 200 root_module_keys.append(key) 201 202 visited = set() 203 204 def json_module_graph_post_traversal(module_key): 205 if module_key in ignored or module_key in visited: 206 return 207 visited.add(module_key) 208 209 deps = set() 210 module = module_graph_map[module_key] 211 created_by = module["CreatedBy"] 212 to_visit = set() 213 214 if created_by: 215 for key in name_to_keys[created_by]: 216 if key in ignored: 217 continue 218 # treat created by as a dep so it appears as a blocker, otherwise the 219 # module will be disconnected from the traversal graph despite having a 220 # direct relationship to a module and must addressed in the migration 221 deps.add(created_by) 222 json_module_graph_post_traversal(key) 223 224 for dep in module["Deps"]: 225 if ignore_json_dep( 226 dep, module["Name"], ignored, ignore_java_auto_deps 227 ): 228 continue 229 230 dep_name = dep["Name"] 231 deps.add(dep_name) 232 dep_key = _ModuleKey(dep_name, dep["Variations"]) 233 234 if dep_key not in visited: 235 json_module_graph_post_traversal(dep_key) 236 237 visit(module, deps) 238 239 for module_key in root_module_keys: 240 json_module_graph_post_traversal(module_key) 241 242 243QueryviewModule = collections.namedtuple("QueryviewModule", [ 244 "name", 245 "kind", 246 "variant", 247 "dirname", 248 "deps", 249 "srcs", 250]) 251 252 253def _bazel_target_to_dir(full_target): 254 dirname, _ = full_target.split(":") 255 return dirname[len("//"):] # discard prefix 256 257 258def _get_queryview_module(name_with_variant, module, kind): 259 name = None 260 variant = "" 261 deps = [] 262 srcs = [] 263 for attr in module: 264 attr_name = attr.attrib["name"] 265 if attr.tag == "rule-input": 266 deps.append(attr_name) 267 elif attr_name == "soong_module_name": 268 name = attr.attrib["value"] 269 elif attr_name == "soong_module_variant": 270 variant = attr.attrib["value"] 271 elif attr_name == "soong_module_type" and kind == "generic_soong_module": 272 kind = attr.attrib["value"] 273 elif attr_name == "srcs": 274 for item in attr: 275 srcs.append(item.attrib["value"]) 276 277 return QueryviewModule( 278 name=name, 279 kind=kind, 280 variant=variant, 281 dirname=_bazel_target_to_dir(name_with_variant), 282 deps=deps, 283 srcs=srcs, 284 ) 285 286 287def _ignore_queryview_module(module, ignore_by_name): 288 if module.name in ignore_by_name: 289 return True 290 if ignore_kind(module.kind, queryview=True): 291 return True 292 # special handling for filegroup srcs, if a source has the same name as 293 # the filegroup module, we don't convert it 294 if module.kind == "filegroup" and module.name in module.srcs: 295 return True 296 return module.variant.startswith("windows") 297 298 299def visit_queryview_xml_module_graph_post_order(module_graph, ignored_by_name, 300 filter_predicate, visit): 301 # The set of ignored modules. These modules (and their dependencies) are 302 # not shown in the graph or report. 303 ignored = set() 304 305 # queryview embeds variant in long name, keep a map of the name with vaiarnt 306 # to just name 307 name_with_variant_to_name = dict() 308 309 module_graph_map = dict() 310 to_visit = [] 311 312 for module in module_graph: 313 ignore = False 314 if module.tag != "rule": 315 continue 316 kind = module.attrib["class"] 317 name_with_variant = module.attrib["name"] 318 319 qv_module = _get_queryview_module(name_with_variant, module, kind) 320 321 if _ignore_queryview_module(qv_module, ignored_by_name): 322 ignored.add(name_with_variant) 323 continue 324 325 if filter_predicate(qv_module): 326 to_visit.append(name_with_variant) 327 328 name_with_variant_to_name.setdefault(name_with_variant, qv_module.name) 329 module_graph_map[name_with_variant] = qv_module 330 331 visited = set() 332 333 def queryview_module_graph_post_traversal(name_with_variant): 334 module = module_graph_map[name_with_variant] 335 if name_with_variant in ignored or name_with_variant in visited: 336 return 337 visited.add(name_with_variant) 338 339 name = name_with_variant_to_name[name_with_variant] 340 341 deps = set() 342 for dep_name_with_variant in module.deps: 343 if dep_name_with_variant in ignored: 344 continue 345 dep_name = name_with_variant_to_name[dep_name_with_variant] 346 if dep_name == "prebuilt_" + name: 347 continue 348 if dep_name_with_variant not in visited: 349 queryview_module_graph_post_traversal(dep_name_with_variant) 350 351 if name != dep_name: 352 deps.add(dep_name) 353 354 visit(module, deps) 355 356 for name_with_variant in to_visit: 357 queryview_module_graph_post_traversal(name_with_variant) 358 359 360def get_bp2build_converted_modules() -> Set[str]: 361 """ Returns the list of modules that bp2build can currently convert. """ 362 _build_with_soong("bp2build") 363 # Parse the list of converted module names from bp2build 364 with open( 365 os.path.join(SRC_ROOT_DIR, 366 "out/soong/soong_injection/metrics/converted_modules.txt"), 367 "r") as f: 368 # Read line by line, excluding comments. 369 # Each line is a module name. 370 ret = set(line.strip() for line in f if not line.strip().startswith("#")) 371 return ret 372 373 374def get_json_module_type_info(module_type): 375 """Returns the combined transitive dependency closures of all modules of module_type.""" 376 _build_with_soong("json-module-graph") 377 # Run query.sh on the module graph for the top level module type 378 result = subprocess.check_output( 379 [ 380 "build/bazel/json_module_graph/query.sh", 381 "fullTransitiveModuleTypeDeps", "out/soong/module-graph.json", 382 module_type 383 ], 384 cwd=SRC_ROOT_DIR, 385 ) 386 return json.loads(result) 387 388 389def is_windows_variation(module): 390 """Returns True if input module's variant is Windows. 391 392 Args: 393 module: an entry parsed from Soong's json-module-graph 394 """ 395 dep_variations = module.get("Variations") 396 dep_variation_os = "" 397 if dep_variations != None: 398 for v in dep_variations: 399 if v["Mutator"] == "os": 400 dep_variation_os = v["Variation"] 401 return dep_variation_os == "windows" 402 403 404def ignore_kind(kind, queryview=False): 405 if queryview and kind in _QUERYVIEW_IGNORE_KINDS: 406 return True 407 return kind in IGNORED_KINDS or "defaults" in kind 408 409 410def is_prebuilt_to_source_dep(dep): 411 # Soong always adds a dependency from a source module to its corresponding 412 # prebuilt module, if it exists. 413 # https://cs.android.com/android/platform/superproject/+/master:build/soong/android/prebuilt.go;l=395-396;drc=5d6fa4d8571d01a6e5a63a8b7aa15e61f45737a9 414 # This makes it appear that the prebuilt is a transitive dependency regardless 415 # of whether it is actually necessary. Skip these to keep the graph to modules 416 # used to build. 417 return dep["Tag"] == "android.prebuiltDependencyTag {BaseDependencyTag:{}}" 418 419 420def _is_java_auto_dep(dep): 421 # Soong adds a number of dependencies automatically for Java deps, making it 422 # difficult to understand the actual dependencies, remove the 423 # non-user-specified deps 424 tag = dep["Tag"] 425 if not tag: 426 return False 427 return ( 428 ( 429 tag.startswith("java.dependencyTag") 430 and ( 431 "name:proguard-raise" in tag 432 or "name:bootclasspath" in tag 433 or "name:system modules" in tag 434 or "name:framework-res" in tag 435 or "name:sdklib" in tag 436 or "name:java9lib" in tag 437 ) 438 or ( 439 tag.startswith("java.usesLibraryDependencyTag") 440 or tag.startswith("java.hiddenAPIStubsDependencyTag") 441 ) 442 ) 443 or ( 444 tag.startswith("android.sdkMemberDependencyTag") 445 or tag.startswith("java.scopeDependencyTag") 446 ) 447 or tag.startswith("dexpreopt.dex2oatDependencyTag") 448 ) 449 450 451def ignore_json_dep(dep, module_name, ignored_keys, ignore_java_auto_deps): 452 """Whether to ignore a json dependency based on heuristics. 453 454 Args: 455 dep: dependency struct from an entry in Soogn's json-module-graph 456 module_name: name of the module this is a dependency of 457 ignored_names: a set of _ModuleKey to ignore 458 """ 459 if is_prebuilt_to_source_dep(dep): 460 return True 461 if ignore_java_auto_deps and _is_java_auto_dep(dep): 462 return True 463 name = dep["Name"] 464 return ( 465 _ModuleKey(name, dep["Variations"]) in ignored_keys or name == module_name 466 ) 467