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 |