1#!/usr/bin/env python3
2#
3# Copyright (C) 2021 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
17import argparse
18import collections
19import dataclasses
20import datetime
21import functools
22import os.path
23import subprocess
24import sys
25from typing import DefaultDict, Dict, FrozenSet, List, Optional, Set, Tuple
26import xml
27from bp2build_metrics_proto.bp2build_metrics_pb2 import Bp2BuildMetrics, UnconvertedReasonType
28import bp2build_pb2
29import dependency_analysis
30
31
32@dataclasses.dataclass(frozen=True, order=True)
33class GraphFilterInfo:
34  module_names: Set[str] = dataclasses.field(default_factory=set)
35  module_types: Set[str] = dataclasses.field(default_factory=set)
36  package_dir: str = dataclasses.field(default_factory=str)
37  recursive: bool = dataclasses.field(default_factory=bool)
38
39
40@dataclasses.dataclass(frozen=True, order=True)
41class ModuleInfo:
42  name: str
43  kind: str
44  dirname: str
45  created_by: Optional[str]
46  reasons_from_heuristics: FrozenSet[str] = frozenset()
47  reason_from_metric: str = ""
48  props: FrozenSet[str] = frozenset()
49  num_deps: int = 0
50  converted: bool = False
51
52  def __str__(self):
53    converted = " (converted)" if self.converted else ""
54    return f"{self.name} [{self.kind}] [{self.dirname}]{converted}"
55
56  def short_string(self, converted: Set[str]):
57    converted = " (c)" if self.is_converted(converted) else ""
58    return f"{self.name} [{self.kind}]{converted}"
59
60  def get_reasons_from_heuristics(self):
61    if len(self.reasons_from_heuristics) == 0:
62      return ""
63    return (
64        "unconverted reasons from heuristics: {reasons_from_heuristics}"
65        .format(reasons_from_heuristics=", ".join(self.reasons_from_heuristics))
66    )
67
68  def get_reason_from_metric(self):
69    if len(self.reason_from_metric) == 0:
70      return ""
71    return "unconverted reason from metric: {reason_from_metric}".format(
72        reason_from_metric=self.reason_from_metric
73    )
74
75  def is_converted(self, converted: Dict[str, Set[str]]):
76    return self.name in converted and self.kind in converted[self.name]
77
78  def is_skipped(self):
79    # these are implementation details of another module type that can never be
80    # created in a BUILD file
81    return ".go_android/soong" in self.kind and (
82        self.kind.endswith("__loadHookModule")
83        or self.kind.endswith("__topDownMutatorModule")
84    )
85
86  def is_converted_or_skipped(self, converted: Dict[str, Set[str]]):
87    return self.is_converted(converted) or self.is_skipped()
88
89
90@dataclasses.dataclass(frozen=True, order=True)
91class DepInfo:
92  direct_deps: Set[ModuleInfo] = dataclasses.field(default_factory=set)
93  transitive_deps: Set[ModuleInfo] = dataclasses.field(default_factory=set)
94
95  def all_deps(self):
96    return set.union(self.direct_deps, self.transitive_deps)
97
98
99@dataclasses.dataclass(frozen=True, order=True)
100class InputModule:
101  module: ModuleInfo
102  num_deps: int = 0
103  num_unconverted_deps: int = 0
104
105  def __str__(self):
106    total = self.num_deps
107    converted = self.num_deps - self.num_unconverted_deps
108    percent = 100
109    if self.num_deps > 0:
110      percent = converted / self.num_deps * 100
111    return f"{self.module.name}: {percent:.1f}% ({converted}/{total}) converted"
112
113
114@dataclasses.dataclass(frozen=True)
115class ReportData:
116  total_deps: Set[ModuleInfo]
117  unconverted_deps: Set[str]
118  all_unconverted_modules: Dict[str, Set[ModuleInfo]]
119  blocked_modules: Dict[ModuleInfo, Set[str]]
120  blocked_modules_transitive: Dict[ModuleInfo, Set[str]]
121  dirs_with_unconverted_modules: Set[str]
122  kind_of_unconverted_modules: Set[str]
123  converted: Dict[str, Set[str]]
124  show_converted: bool
125  hide_unconverted_modules_reasons: bool
126  package_dir: str
127  input_modules: Set[InputModule] = dataclasses.field(default_factory=set)
128  input_types: Set[str] = dataclasses.field(default_factory=set)
129
130
131# Generate a dot file containing the transitive closure of the module.
132def generate_dot_file(
133    modules: Dict[ModuleInfo, DepInfo],
134    converted: Dict[str, Set[str]],
135    show_converted: bool,
136):
137  # Check that all modules in the argument are in the list of converted modules
138  all_converted = lambda modules: all(
139      m.is_converted(converted) for m in modules
140  )
141
142  dot_entries = []
143
144  for module, dep_info in sorted(modules.items()):
145    deps = dep_info.direct_deps
146    if module.is_converted(converted):
147      if show_converted:
148        color = "dodgerblue"
149      else:
150        continue
151    elif all_converted(deps):
152      color = "yellow"
153    else:
154      color = "tomato"
155
156    dot_entries.append(
157        f'"{module.name}" [label="{module.name}\\n{module.kind}" color=black,'
158        f" style=filled, fillcolor={color}]"
159    )
160    dot_entries.extend(
161        f'"{module.name}" -> "{dep.name}"'
162        for dep in sorted(deps)
163        if show_converted or not dep.is_converted(converted)
164    )
165
166  return """
167digraph mygraph {{
168  node [shape=box];
169
170  %s
171}}
172""" % "\n  ".join(dot_entries)
173
174
175def get_transitive_unconverted_deps(
176    cache: Dict[DepInfo, Set[DepInfo]],
177    module: ModuleInfo,
178    modules: Dict[ModuleInfo, DepInfo],
179    converted: Dict[str, Set[str]],
180) -> Set[str]:
181  if module in cache:
182    return cache[module]
183  unconverted_deps = set()
184  dep = modules[module]
185  for d in dep.direct_deps:
186    if d.is_converted_or_skipped(converted):
187      continue
188    unconverted_deps.add(d)
189    transitive = get_transitive_unconverted_deps(cache, d, modules, converted)
190    unconverted_deps = unconverted_deps.union(transitive)
191  cache[module] = unconverted_deps
192  return unconverted_deps
193
194
195# Filter modules based on the module and graph_filter
196def module_matches_filter(module, graph_filter):
197  dirname = module.dirname + "/"
198  if graph_filter.package_dir is not None:
199    if graph_filter.recursive:
200      return dirname.startswith(graph_filter.package_dir)
201    return dirname == graph_filter.package_dir
202  return (
203      module.name in graph_filter.module_names
204      or module.kind in graph_filter.module_types
205  )
206
207
208def unconverted_reasons_from_heuristics(
209    module, unconverted_transitive_deps, props_by_converted_module_type
210):
211  """Heuristics for determining the reason for unconverted module"""
212  reasons = []
213  if module.converted:
214    raise RuntimeError(
215        "Heuristics should not be run on converted module %s" % module.name
216    )
217  if module.kind in props_by_converted_module_type:
218    props_diff = module.props.difference(
219        props_by_converted_module_type[module.kind]
220    )
221    if len(props_diff) != 0:
222      reasons.append(
223          "unconverted properties: [%s]" % ", ".join(sorted(props_diff))
224      )
225  else:
226    reasons.append("type missing converter")
227  if len(unconverted_transitive_deps) > 0:
228    reasons.append("unconverted dependencies")
229  return frozenset(reasons)
230
231
232# Generate a report for each module in the transitive closure, and the blockers for each module
233def generate_report_data(
234    modules: Dict[ModuleInfo, DepInfo],
235    converted: Set[str],
236    graph_filter: GraphFilterInfo,
237    props_by_converted_module_type: DefaultDict[str, Set[str]],
238    use_queryview: bool,
239    bp2build_metrics: Bp2BuildMetrics,
240    hide_unconverted_modules_reasons: bool = False,
241    show_converted: bool = False,
242) -> ReportData:
243  # Map of [number of unconverted deps] to list of entries,
244  # with each entry being the string: "<module>: <comma separated list of unconverted modules>"
245  blocked_modules = collections.defaultdict(set)
246  blocked_modules_transitive = collections.defaultdict(set)
247
248  # Map of unconverted modules to the modules they're blocking
249  # (i.e. reverse deps)
250  all_unconverted_modules = collections.defaultdict(set)
251
252  dirs_with_unconverted_modules = set()
253  kind_of_unconverted_modules = collections.defaultdict(int)
254
255  input_all_deps = set()
256  input_unconverted_deps = set()
257  input_modules = set()
258
259  transitive_deps_by_dep_info = {}
260
261  for module, dep_info in sorted(modules.items()):
262    deps = dep_info.direct_deps
263    unconverted_deps = set(
264        dep for dep in deps if not dep.is_converted_or_skipped(converted)
265    )
266
267    unconverted_transitive_deps = get_transitive_unconverted_deps(
268        transitive_deps_by_dep_info, module, modules, converted
269    )
270
271    # ModuleInfo.reason_from_metric will be an empty string if the module is converted or --use-queryview flag is passed
272    unconverted_module_reason_from_metrics = ""
273    if (
274        module.name in bp2build_metrics.unconvertedModules
275        and not use_queryview
276        and not hide_unconverted_modules_reasons
277    ):
278      # TODO(b/291642059): Concatenate the value of UnconvertedReason.detail field with unconverted_module_reason_from_metrics.
279      unconverted_module_reason_from_metrics = UnconvertedReasonType.Name(
280          bp2build_metrics.unconvertedModules[module.name].type
281      )
282
283    unconverted_module_reasons_from_heuristics = (
284        unconverted_reasons_from_heuristics(
285            module, unconverted_transitive_deps, props_by_converted_module_type
286        )
287        if not (
288            module.is_skipped()
289            or module.is_converted(converted)
290            or use_queryview
291            or hide_unconverted_modules_reasons
292        )
293        else frozenset()
294    )
295
296    # replace deps count with transitive deps rather than direct deps count
297    module = ModuleInfo(
298        module.name,
299        module.kind,
300        module.dirname,
301        module.created_by,
302        unconverted_module_reasons_from_heuristics,
303        unconverted_module_reason_from_metrics,
304        module.props,
305        len(dep_info.all_deps()),
306        module.is_converted(converted),
307    )
308
309    for dep in unconverted_transitive_deps:
310      all_unconverted_modules[dep].add(module)
311
312    if not module.is_skipped() and (
313        not module.is_converted(converted) or show_converted
314    ):
315      if show_converted:
316        full_deps = set(dep for dep in deps)
317        blocked_modules[module].update(full_deps)
318        full_deps = set(dep for dep in dep_info.all_deps())
319        blocked_modules_transitive[module].update(full_deps)
320      else:
321        blocked_modules[module].update(unconverted_deps)
322        blocked_modules_transitive[module].update(unconverted_transitive_deps)
323
324    if not module.is_converted_or_skipped(converted):
325      dirs_with_unconverted_modules.add(module.dirname)
326      kind_of_unconverted_modules[module.kind] += 1
327
328    if module_matches_filter(module, graph_filter):
329      transitive_deps = dep_info.all_deps()
330      input_modules.add(
331          InputModule(
332              module, len(transitive_deps), len(unconverted_transitive_deps)
333          )
334      )
335      input_all_deps.update(transitive_deps)
336      input_unconverted_deps.update(unconverted_transitive_deps)
337
338  kinds = set(
339      f"{k}: {kind_of_unconverted_modules[k]}"
340      for k in kind_of_unconverted_modules.keys()
341  )
342
343  return ReportData(
344      input_modules=input_modules,
345      input_types=graph_filter.module_types,
346      total_deps=input_all_deps,
347      unconverted_deps=input_unconverted_deps,
348      all_unconverted_modules=all_unconverted_modules,
349      blocked_modules=blocked_modules,
350      blocked_modules_transitive=blocked_modules_transitive,
351      dirs_with_unconverted_modules=dirs_with_unconverted_modules,
352      kind_of_unconverted_modules=kinds,
353      converted=converted,
354      show_converted=show_converted,
355      hide_unconverted_modules_reasons=hide_unconverted_modules_reasons,
356      package_dir=graph_filter.package_dir,
357  )
358
359
360def generate_proto(report_data):
361  message = bp2build_pb2.Bp2buildConversionProgress(
362      root_modules=[m.module.name for m in report_data.input_modules],
363      num_deps=len(report_data.total_deps),
364  )
365  for (
366      module,
367      unconverted_deps,
368  ) in report_data.blocked_modules_transitive.items():
369    message.unconverted.add(
370        name=module.name,
371        directory=module.dirname,
372        type=module.kind,
373        unconverted_deps={d.name for d in unconverted_deps},
374        num_deps=module.num_deps,
375        # when the module is converted or queryview is being used, an empty list will be assigned
376        unconverted_reasons_from_heuristics=list(
377            module.reasons_from_heuristics
378        ),
379    )
380  return message
381
382
383def generate_report(report_data):
384  report_lines = []
385  if len(report_data.input_types) > 0:
386    input_module_str = ", ".join(
387        str(i) for i in sorted(report_data.input_types)
388    )
389  else:
390    input_module_str = ", ".join(
391        str(i) for i in sorted(report_data.input_modules)
392    )
393
394  report_lines.append("# bp2build progress report for: %s\n" % input_module_str)
395
396  if report_data.show_converted:
397    report_lines.append(
398        "# progress report includes data both for converted and unconverted"
399        " modules"
400    )
401
402  total = len(report_data.total_deps)
403  unconverted = len(report_data.unconverted_deps)
404  converted = total - unconverted
405  if total > 0:
406    percent = converted / total * 100
407  else:
408    percent = 100
409  report_lines.append(f"Percent converted: {percent:.2f} ({converted}/{total})")
410  report_lines.append(f"Total unique unconverted dependencies: {unconverted}")
411
412  report_lines.append(
413      "Ignored module types: %s\n" % sorted(dependency_analysis.IGNORED_KINDS)
414  )
415  report_lines.append("# Transitive dependency closure:")
416
417  current_count = -1
418  for module, unconverted_transitive_deps in sorted(
419      report_data.blocked_modules_transitive.items(), key=lambda x: len(x[1])
420  ):
421    count = len(unconverted_transitive_deps)
422    if current_count != count:
423      report_lines.append(f"\n{count} unconverted transitive deps remaining:")
424      current_count = count
425    unconverted_deps = report_data.blocked_modules.get(module, set())
426    unconverted_deps = set(
427        d.short_string(report_data.converted) for d in unconverted_deps
428    )
429    report_lines.append(f"{module}")
430    if not report_data.hide_unconverted_modules_reasons:
431      report_lines.append("\tunconverted due to:")
432      reason_from_metric = module.get_reason_from_metric()
433      reasons_from_heuristics = module.get_reasons_from_heuristics()
434      if reason_from_metric != "":
435        report_lines.append(f"\t\t{reason_from_metric}")
436      if reasons_from_heuristics != "":
437        report_lines.append(f"\t\t{reasons_from_heuristics}")
438    if len(unconverted_deps) == 0:
439      report_lines.append('\tdirect deps:')
440    else:
441      report_lines.append(
442        "\tdirect deps: {deps}".format(deps=", ".join(sorted(unconverted_deps)))
443      )
444
445  report_lines.append("\n")
446  report_lines.append("# Unconverted deps of {}:\n".format(input_module_str))
447  for count, dep in sorted(
448      (
449          (len(unconverted), dep)
450          for dep, unconverted in report_data.all_unconverted_modules.items()
451      ),
452      reverse=True,
453  ):
454    report_lines.append(
455        "%s: blocking %d modules"
456        % (dep.short_string(report_data.converted), count)
457    )
458
459  report_lines.append("\n")
460  report_lines.append(
461      "# Dirs with unconverted modules:\n\n{}".format(
462          "\n".join(sorted(report_data.dirs_with_unconverted_modules))
463      )
464  )
465
466  report_lines.append("\n")
467  report_lines.append(
468      "# Kinds with unconverted modules:\n\n{}".format(
469          "\n".join(sorted(report_data.kind_of_unconverted_modules))
470      )
471  )
472
473  report_lines.append("\n")
474  if report_data.show_converted:
475    report_lines.append(
476      "# Converted modules:\n\n%s" % "\n".join(sorted(report_data.converted))
477    )
478  else:
479    report_lines.append("# Converted modules not shown")
480
481  report_lines.append("\n")
482  report_lines.append(
483      "Generated by:"
484      " https://cs.android.com/android/platform/superproject/+/master:build/bazel/scripts/bp2build_progress/bp2build_progress.py"
485  )
486  report_lines.append(
487      "Generated at: %s"
488      % datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S %z")
489  )
490
491  return "\n".join(report_lines)
492
493
494def adjacency_list_from_json(
495    module_graph: ...,
496    ignore_by_name: List[str],
497    ignore_java_auto_deps: bool,
498    graph_filter: GraphFilterInfo,
499    collect_transitive_dependencies: bool = True,
500) -> Dict[ModuleInfo, Set[ModuleInfo]]:
501  def filtering(json):
502    module = ModuleInfo(
503        name=json["Name"],
504        created_by=json["CreatedBy"],
505        kind=json["Type"],
506        dirname=os.path.dirname(json["Blueprint"]),
507    )
508    return module_matches_filter(module, graph_filter)
509
510  module_adjacency_list = {}
511  name_to_info = {}
512
513  def collect_dependencies(module, deps_names):
514    module_info = None
515    name = module["Name"]
516    props = dependency_analysis.get_properties(module)
517    converted = (
518        props.get("Bazel_module.Bp2build_available", "false") == "true"
519        or props.get("Bazel_module.Label", "") != ""
520    )
521    name_to_info.setdefault(
522        name,
523        ModuleInfo(
524            name=name,
525            created_by=module["CreatedBy"],
526            kind=module["Type"],
527            props=frozenset(
528                prop for prop in dependency_analysis.get_property_names(module)
529            ),
530            dirname=os.path.dirname(module["Blueprint"]),
531            num_deps=len(deps_names),
532            converted=converted,
533        ),
534    )
535
536    module_info = name_to_info[name]
537
538    # ensure module_info added to adjacency list even with no deps
539    module_adjacency_list.setdefault(module_info, DepInfo())
540    for dep in deps_names:
541      # this may occur if there is a cycle between a module and created_by
542      # module
543      if not dep in name_to_info:
544        continue
545      dep_module_info = name_to_info[dep]
546      module_adjacency_list[module_info].direct_deps.add(dep_module_info)
547      if collect_transitive_dependencies:
548        transitive_dep_info = module_adjacency_list.get(
549            dep_module_info, DepInfo()
550        )
551        module_adjacency_list[module_info].transitive_deps.update(
552            transitive_dep_info.all_deps()
553        )
554
555  dependency_analysis.visit_json_module_graph_post_order(
556      module_graph,
557      ignore_by_name,
558      ignore_java_auto_deps,
559      filtering,
560      collect_dependencies,
561  )
562
563  return module_adjacency_list
564
565
566def adjacency_list_from_queryview_xml(
567    module_graph: xml.etree.ElementTree,
568    graph_filter: GraphFilterInfo,
569    ignore_by_name: List[str],
570    collect_transitive_dependencies: bool = True,
571) -> Dict[ModuleInfo, DepInfo]:
572  def filtering(module):
573    return (
574        module.name in graph_filter.module_names
575        or module.kind in graph_filter.module_types
576    )
577
578  module_adjacency_list = collections.defaultdict(set)
579  name_to_info = {}
580
581  def collect_dependencies(module, deps_names):
582    module_info = None
583    name_to_info.setdefault(
584        module.name,
585        ModuleInfo(
586            name=module.name,
587            kind=module.kind,
588            dirname=module.dirname,
589            # required so that it cannot be forgotten when updating num_deps
590            created_by=None,
591            num_deps=len(deps_names),
592        ),
593    )
594    module_info = name_to_info[module.name]
595
596    # ensure module_info added to adjacency list even with no deps
597    module_adjacency_list.setdefault(module_info, DepInfo())
598    for dep in deps_names:
599      dep_module_info = name_to_info[dep]
600      module_adjacency_list[module_info].direct_deps.add(dep_module_info)
601      if collect_transitive_dependencies:
602        transitive_dep_info = module_adjacency_list.get(
603            dep_module_info, DepInfo()
604        )
605        module_adjacency_list[module_info].transitive_deps.update(
606            transitive_dep_info.all_deps()
607        )
608
609  dependency_analysis.visit_queryview_xml_module_graph_post_order(
610      module_graph, ignore_by_name, filtering, collect_dependencies
611  )
612
613  return module_adjacency_list
614
615
616# this function gets map of converted module types to set of properties for heuristics
617def get_props_by_converted_module_type(module_graph, converted, ignore_by_name):
618  props_by_converted_module_type = collections.defaultdict(set)
619
620  def collect_module_props(module):
621    props = set(prop for prop in dependency_analysis.get_property_names(module))
622    if module["Type"] not in props_by_converted_module_type:
623      props_by_converted_module_type[module["Type"]] = props
624    else:
625      props_by_converted_module_type[module["Type"]].update(props)
626
627  for module in module_graph:
628    if module[
629        "Name"
630    ] not in converted or dependency_analysis.ignore_json_module(
631        module, ignore_by_name
632    ):
633      continue
634    collect_module_props(module)
635
636  return props_by_converted_module_type
637
638
639def get_module_adjacency_list_and_props_by_converted_module_type(
640    graph_filter: GraphFilterInfo,
641    use_queryview: bool,
642    ignore_by_name: List[str],
643    converted: Set[str],
644    target_product: dependency_analysis.TargetProduct,
645    ignore_java_auto_deps: bool = False,
646    collect_transitive_dependencies: bool = True,
647) -> Tuple[Dict[ModuleInfo, DepInfo], DefaultDict[str, Set[str]]]:
648  # The main module graph containing _all_ modules in the Soong build,
649  # and the list of converted modules.
650
651  # Map of converted modules types to the set of properties.
652  # This is only used in heuristics implementation.
653  props_by_converted_module_type = collections.defaultdict(set)
654
655  try:
656    if use_queryview:
657      if len(graph_filter.module_names) > 0:
658        module_graph = dependency_analysis.get_queryview_module_info(
659            graph_filter.module_names, target_product
660        )
661      else:
662        module_graph = dependency_analysis.get_queryview_module_info_by_type(
663            graph_filter.module_types, target_product
664        )
665
666      module_adjacency_list = adjacency_list_from_queryview_xml(
667          module_graph,
668          graph_filter,
669          ignore_by_name,
670          collect_transitive_dependencies,
671      )
672    else:
673      module_graph = dependency_analysis.get_json_module_info(target_product)
674      module_adjacency_list = adjacency_list_from_json(
675          module_graph,
676          ignore_by_name,
677          ignore_java_auto_deps,
678          graph_filter,
679          collect_transitive_dependencies,
680      )
681      props_by_converted_module_type = get_props_by_converted_module_type(
682          module_graph, converted, ignore_by_name
683      )
684  except subprocess.CalledProcessError as err:
685    sys.exit(f"""Error running: '{' '.join(err.cmd)}':"
686Stdout:
687{err.stdout.decode('utf-8') if err.stdout else ''}
688Stderr:
689{err.stderr.decode('utf-8') if err.stderr else ''}""")
690
691  return module_adjacency_list, props_by_converted_module_type
692
693
694def add_manual_conversion_to_converted(
695    converted: Dict[str, Set[str]], module_adjacency_list: Dict[ModuleInfo, DepInfo]
696) -> Set[str]:
697  modules_by_name = {m.name: m for m in module_adjacency_list.keys()}
698
699  converted_modules = collections.defaultdict(set)
700  converted_modules.update(converted)
701
702  def _update_converted(module_name):
703    if module_name in converted_modules:
704      return True
705    if module_name not in modules_by_name:
706      return False
707    module = modules_by_name[module_name]
708    if module.converted:
709      converted_modules[module_name].add(module.kind)
710      return True
711    return False
712
713  for module in modules_by_name.keys():
714    _update_converted(module)
715
716  return converted_modules
717
718
719def main():
720  parser = argparse.ArgumentParser()
721  parser.add_argument("mode", help="mode: graph or report")
722  parser.add_argument(
723      "--product",
724      help="Product to collect module graph for. (Optional)",
725      required=False,
726      default="aosp_cf_arm64_phone",
727  )
728  parser.add_argument(
729      "--module",
730      "-m",
731      action="append",
732      help=(
733          "name(s) of Soong module(s). Multiple modules only supported for"
734          " report"
735      ),
736  )
737  parser.add_argument(
738      "--type",
739      "-t",
740      action="append",
741      help=(
742          "type(s) of Soong module(s). Multiple modules only supported for"
743          " report"
744      ),
745  )
746  parser.add_argument(
747      "--package-dir",
748      "-p",
749      action="store",
750      help=(
751          "package directory for Soong modules. Single package directory only"
752          " supported for report."
753      ),
754  )
755  parser.add_argument(
756      "--recursive",
757      "-r",
758      action="store_true",
759      help=(
760          "whether to perform recursive search when --package-dir flag is"
761          " passed."
762      ),
763  )
764  parser.add_argument(
765      "--use-queryview",
766      action="store_true",
767      help="whether to use queryview or module_info",
768  )
769  parser.add_argument(
770      "--ignore-by-name",
771      default="",
772      help=(
773          "Comma-separated list. When building the tree of transitive"
774          " dependencies, will not follow dependency edges pointing to module"
775          " names listed by this flag."
776      ),
777  )
778  parser.add_argument(
779      "--ignore-java-auto-deps",
780      action="store_true",
781      default=True,
782      help="whether to ignore automatically added java deps",
783  )
784  parser.add_argument(
785      "--banchan",
786      action="store_true",
787      help="whether to run Soong in a banchan configuration rather than lunch",
788  )
789  # TODO(b/283512659): Fix the relative path bug and update the README file
790  parser.add_argument(
791      "--proto-file",
792      help="Path to write proto output",
793  )
794  # TODO(b/283512659): Fix the relative path bug and update the README file
795  parser.add_argument(
796      "--out-file",
797      "-o",
798      type=argparse.FileType("w"),
799      default="-",
800      help="Path to write output, if omitted, writes to stdout",
801  )
802  parser.add_argument(
803      "--show-converted",
804      "-s",
805      action="store_true",
806      help=(
807          "Show bp2build-converted modules in addition to the unconverted"
808          " dependencies to see full dependencies post-migration. By default"
809          " converted dependencies are not shown"
810      ),
811  )
812  parser.add_argument(
813      # This flag is only relevant when used by the CI script. Don't use it when running b command independently.
814      "--bp2build-metrics-location",
815      default=os.path.join(dependency_analysis.SRC_ROOT_DIR, "out"),
816      help=(
817          "Path to get bp2build_metrics, if omitted, gets bp2build_metrics from"
818          " the SRC_ROOT_DIR/out directory"
819      ),
820  )
821  parser.add_argument(
822      "--hide-unconverted-modules-reasons",
823      action="store_true",
824      help=(
825          "Hide unconverted modules reasons of heuristics and"
826          " bp2build_metrics.pb. By default unconverted modules reasons are"
827          " shown"
828      ),
829  )
830  args = parser.parse_args()
831
832  if args.proto_file and args.mode == "graph":
833    sys.exit(f"Proto file only supported for report mode, not {args.mode}")
834
835  mode = args.mode
836  use_queryview = args.use_queryview
837  ignore_by_name = args.ignore_by_name.split(",")
838  ignore_java_auto_deps = args.ignore_java_auto_deps
839  target_product = dependency_analysis.TargetProduct(
840      banchan_mode=args.banchan,
841      product=args.product,
842  )
843  modules = set(args.module) if args.module is not None else set()
844  types = set(args.type) if args.type is not None else set()
845  recursive = args.recursive
846  package_dir = (
847      os.path.normpath(args.package_dir) + "/"
848      if args.package_dir
849      else args.package_dir
850  )
851  bp2build_metrics_location = args.bp2build_metrics_location
852  graph_filter = GraphFilterInfo(modules, types, package_dir, recursive)
853
854  if package_dir is None:
855    if len(modules) == 0 and len(types) == 0:
856      sys.exit("Must specify at least one module, type or package directory")
857    if recursive:
858      sys.exit("Cannot support --recursive with modules or types")
859  if package_dir is not None:
860    if args.use_queryview:
861      sys.exit("Can only support the package directory with json module graph")
862    if args.mode == "graph":
863      sys.exit(f"Cannot support --package-dir with mode graph")
864    if len(modules) > 0 or len(types) > 0:
865      sys.exit("Can only support either modules, types or package directory")
866  if len(modules) > 0 and len(types) > 0 and args.use_queryview:
867    sys.exit("Can only support either of modules or types with --use-queryview")
868  if len(modules) > 1 and args.mode == "graph":
869    sys.exit(f"Can only support one module with mode graph")
870  if len(types) and args.mode == "graph":
871    sys.exit(f"Cannot support --type with mode graph")
872  if args.hide_unconverted_modules_reasons:
873    if args.use_queryview:
874      sys.exit(
875          "Cannot support --hide-unconverted-modules-reasons with"
876          " --use-queryview"
877      )
878    if args.mode == "graph":
879      sys.exit(
880          f"Cannot support --hide-unconverted-modules-reasons with mode graph"
881      )
882
883  converted = dependency_analysis.get_bp2build_converted_modules(target_product)
884  bp2build_metrics = dependency_analysis.get_bp2build_metrics(
885      bp2build_metrics_location
886  )
887
888  module_adjacency_list, props_by_converted_module_type = (
889      get_module_adjacency_list_and_props_by_converted_module_type(
890          graph_filter,
891          use_queryview,
892          ignore_by_name,
893          converted,
894          target_product,
895          ignore_java_auto_deps,
896          collect_transitive_dependencies=mode != "graph",
897      )
898  )
899
900  if len(module_adjacency_list) == 0:
901    sys.exit(
902        f"Found no modules, verify that the module ({args.module}), type"
903        f" ({args.type}) or package {args.package_dir} you requested are valid."
904    )
905
906  converted = add_manual_conversion_to_converted(converted, module_adjacency_list)
907
908  output_file = args.out_file
909  if mode == "graph":
910    dot_file = generate_dot_file(
911        module_adjacency_list, converted, args.show_converted
912    )
913    output_file.write(dot_file)
914  elif mode == "report":
915    report_data = generate_report_data(
916        module_adjacency_list,
917        converted,
918        graph_filter,
919        props_by_converted_module_type,
920        args.use_queryview,
921        bp2build_metrics,
922        args.hide_unconverted_modules_reasons,
923        args.show_converted,
924    )
925    report = generate_report(report_data)
926    output_file.write(report)
927    if args.proto_file:
928      bp2build_conversion_progress_message = generate_proto(report_data)
929      with open(args.proto_file, "wb") as f:
930        f.write(bp2build_conversion_progress_message.SerializeToString())
931  else:
932    raise RuntimeError("unknown mode: %s" % mode)
933
934
935if __name__ == "__main__":
936  main()
937