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