| OLD | NEW |
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 from __future__ import print_function | 5 from __future__ import print_function |
| 6 | 6 |
| 7 import argparse |
| 8 import sys |
| 9 |
| 7 from . import loader | 10 from . import loader |
| 8 | 11 |
| 9 | 12 |
| 10 _GRAPH_HEADER = """strict digraph { | 13 _GRAPH_HEADER = """strict digraph { |
| 11 concentrate = true; | 14 concentrate = true; |
| 12 ranksep = 2; | 15 ranksep = 2; |
| 13 nodesep = 0.25; | 16 nodesep = 0.25; |
| 14 """ | 17 """ |
| 15 | 18 |
| 16 _GRAPH_FOOTER = """} | 19 _GRAPH_FOOTER = """} |
| 17 """ | 20 """ |
| 18 | 21 |
| 19 | 22 |
| 20 def main(universe, own_package, ignore_packages, stdout, recipe_filter): | 23 def add_subparser(parser): |
| 24 depgraph_p = parser.add_parser( |
| 25 'depgraph', |
| 26 description=( |
| 27 'Produce graph of recipe and recipe module dependencies. Example: ' |
| 28 './recipes.py --package infra/config/recipes.cfg depgraph | tred | ' |
| 29 'dot -Tpdf > graph.pdf')) |
| 30 depgraph_p.add_argument( |
| 31 '--output', type=argparse.FileType('w'), default=sys.stdout, |
| 32 help='The file to write output to') |
| 33 depgraph_p.add_argument( |
| 34 '--ignore-package', action='append', default=[], |
| 35 dest='ignore_packages', |
| 36 help='Ignore a recipe package (e.g. recipe_engine). Can be passed ' |
| 37 'multiple times') |
| 38 depgraph_p.add_argument( |
| 39 '--recipe-filter', default='', |
| 40 help='A recipe substring to examine. If present, the depgraph will ' |
| 41 'include a recipe section containing recipes whose names contain ' |
| 42 'this substring. It will also filter all nodes of the graph to only ' |
| 43 'include modules touched by the filtered recipes.') |
| 44 |
| 45 depgraph_p.set_defaults(command='depgraph', func=main) |
| 46 |
| 47 |
| 48 def main(package_deps, args): |
| 49 universe = loader.RecipeUniverse(package_deps, args.package) |
| 50 own_package = package_deps.root_package |
| 51 |
| 21 module_to_package = {} | 52 module_to_package = {} |
| 22 | 53 |
| 23 # All deps maps a tuple of (is_recipe, id) to deps (list of ids). is_recipe is | 54 # All deps maps a tuple of (is_recipe, id) to deps (list of ids). is_recipe is |
| 24 # a boolean, all ids are strings. | 55 # a boolean, all ids are strings. |
| 25 all_deps = {} | 56 all_deps = {} |
| 26 for package, module_name in universe.loop_over_recipe_modules(): | 57 for package, module_name in universe.loop_over_recipe_modules(): |
| 27 if package in ignore_packages: | 58 if package in args.ignore_packages: |
| 28 continue | 59 continue |
| 29 mod = universe.load(package, module_name) | 60 mod = universe.load(package, module_name) |
| 30 | 61 |
| 31 all_deps[(False, mod.NAME)] = mod.LOADED_DEPS | 62 all_deps[(False, mod.NAME)] = mod.LOADED_DEPS |
| 32 module_to_package[mod.NAME] = package.name | 63 module_to_package[mod.NAME] = package.name |
| 33 | 64 |
| 34 if recipe_filter: | 65 if args.recipe_filter: |
| 35 recipe_to_package = {} | 66 recipe_to_package = {} |
| 36 universe_view = loader.UniverseView(universe, own_package) | 67 universe_view = loader.UniverseView(universe, own_package) |
| 37 for _, recipe_name in universe_view.loop_over_recipes(): | 68 for _, recipe_name in universe_view.loop_over_recipes(): |
| 38 if recipe_filter not in recipe_name: | 69 if args.recipe_filter not in recipe_name: |
| 39 continue | 70 continue |
| 40 | 71 |
| 41 recipe = universe_view.load_recipe(recipe_name) | 72 recipe = universe_view.load_recipe(recipe_name) |
| 42 | 73 |
| 43 all_deps[(True, recipe_name)] = recipe.LOADED_DEPS | 74 all_deps[(True, recipe_name)] = recipe.LOADED_DEPS |
| 44 recipe_to_package[recipe_name] = own_package | 75 recipe_to_package[recipe_name] = own_package |
| 45 | 76 |
| 46 # If we actually found any recipes | 77 # If we actually found any recipes |
| 47 if recipe_to_package: | 78 if recipe_to_package: |
| 48 # Prune anything our recipe doesn't see via BFS. | 79 # Prune anything our recipe doesn't see via BFS. |
| (...skipping 16 matching lines...) Expand all Loading... |
| 65 m_name: p_name for m_name, p_name in module_to_package.items() | 96 m_name: p_name for m_name, p_name in module_to_package.items() |
| 66 if m_name in mod_names} | 97 if m_name in mod_names} |
| 67 | 98 |
| 68 recipe_names = [ | 99 recipe_names = [ |
| 69 name for (is_recipe, name), _ in all_deps.items() if is_recipe] | 100 name for (is_recipe, name), _ in all_deps.items() if is_recipe] |
| 70 recipe_to_package = { | 101 recipe_to_package = { |
| 71 r_name: p_name for r_name, p_name in recipe_to_package.items() | 102 r_name: p_name for r_name, p_name in recipe_to_package.items() |
| 72 if r_name in recipe_names} | 103 if r_name in recipe_names} |
| 73 | 104 |
| 74 | 105 |
| 75 print(_GRAPH_HEADER, file=stdout) | 106 print(_GRAPH_HEADER, file=args.output) |
| 76 edges = [] | 107 edges = [] |
| 77 for (is_recipe, name), deps in all_deps.items(): | 108 for (is_recipe, name), deps in all_deps.items(): |
| 78 for dep in deps: | 109 for dep in deps: |
| 79 edges.append(((is_recipe, name), dep)) | 110 edges.append(((is_recipe, name), dep)) |
| 80 | 111 |
| 81 for edge in edges: | 112 for edge in edges: |
| 82 (is_recipe, first_name), second_name = edge | 113 (is_recipe, first_name), second_name = edge |
| 83 | 114 |
| 84 if not is_recipe: | 115 if not is_recipe: |
| 85 if module_to_package[first_name] in ignore_packages: | 116 if module_to_package[first_name] in args.ignore_packages: |
| 86 continue | 117 continue |
| 87 else: | 118 else: |
| 88 if recipe_to_package[first_name] in ignore_packages: | 119 if recipe_to_package[first_name] in args.ignore_packages: |
| 89 continue | 120 continue |
| 90 first_name = 'recipe ' + first_name | 121 first_name = 'recipe ' + first_name |
| 91 | 122 |
| 92 if module_to_package[second_name] in ignore_packages: | 123 if module_to_package[second_name] in args.ignore_packages: |
| 93 continue | 124 continue |
| 94 | 125 |
| 95 print(' "%s" -> "%s"' % (first_name, second_name), file=stdout) | 126 print(' "%s" -> "%s"' % (first_name, second_name), file=args.output) |
| 96 | 127 |
| 97 packages = {} | 128 packages = {} |
| 98 for module, package in module_to_package.iteritems(): | 129 for module, package in module_to_package.iteritems(): |
| 99 packages.setdefault(package, []).append(module) | 130 packages.setdefault(package, []).append(module) |
| 100 for package, modules in packages.iteritems(): | 131 for package, modules in packages.iteritems(): |
| 101 if package in ignore_packages: | 132 if package in args.ignore_packages: |
| 102 continue | 133 continue |
| 103 # The "cluster_" prefix has magic meaning for graphviz and makes it | 134 # The "cluster_" prefix has magic meaning for graphviz and makes it |
| 104 # draw a box around the subgraph. | 135 # draw a box around the subgraph. |
| 105 print(' subgraph "cluster_%s" { label="%s"; %s; }' % ( | 136 print(' subgraph "cluster_%s" { label="%s"; %s; }' % ( |
| 106 package, package, '; '.join(modules)), file=stdout) | 137 package, package, '; '.join(modules)), file=args.output) |
| 107 | 138 |
| 108 if recipe_filter and recipe_to_package: | 139 if args.recipe_filter and recipe_to_package: |
| 109 recipe_names = [ | 140 recipe_names = [ |
| 110 '"recipe %s"' % name for name in recipe_to_package.keys()] | 141 '"recipe %s"' % name for name in recipe_to_package.keys()] |
| 111 print(' subgraph "cluster_recipes" { label="recipes"; %s; }' % ( | 142 print(' subgraph "cluster_recipes" { label="recipes"; %s; }' % ( |
| 112 '; '.join(recipe_names)), file=stdout) | 143 '; '.join(recipe_names)), file=args.output) |
| 113 | 144 |
| 114 print(_GRAPH_FOOTER, file=stdout) | 145 print(_GRAPH_FOOTER, file=args.output) |
| OLD | NEW |