• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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