Skip to content

Commit 57b0fb7

Browse files
authored
Merge pull request #695 from dhellmann/to-graph-install-only
feat(graph): add --install-only option to to_dot command
2 parents ba1eec7 + f41e300 commit 57b0fb7

1 file changed

Lines changed: 101 additions & 14 deletions

File tree

src/fromager/commands/graph.py

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
DependencyGraph,
1919
DependencyNode,
2020
)
21+
from fromager.packagesettings import PatchMap
2122
from fromager.requirements_file import RequirementType
2223

2324
logger = logging.getLogger(__name__)
@@ -70,57 +71,143 @@ def to_constraints(wkctx: context.WorkContext, graph_file: str, output: pathlib.
7071
"-o",
7172
"--output",
7273
type=clickext.ClickPath(),
74+
default=None,
75+
)
76+
@click.option(
77+
"--install-only",
78+
is_flag=True,
79+
help="Only show installation dependencies, excluding build dependencies",
7380
)
7481
@click.argument(
7582
"graph-file",
7683
type=str,
7784
)
7885
@click.pass_obj
79-
def to_dot(wkctx: context.WorkContext, graph_file: str, output: pathlib.Path):
86+
def to_dot(
87+
wkctx: context.WorkContext,
88+
graph_file: str,
89+
output: pathlib.Path | None,
90+
install_only: bool,
91+
):
8092
"Convert a graph file to a DOT file suitable to pass to graphviz."
8193
graph = DependencyGraph.from_file(graph_file)
8294
if output:
8395
with open(output, "w") as f:
84-
write_dot(graph, f)
96+
write_dot(wkctx, graph, f, install_only=install_only)
8597
else:
86-
write_dot(graph, sys.stdout)
98+
write_dot(wkctx, graph, sys.stdout, install_only=install_only)
8799

88100

89-
def write_dot(graph: DependencyGraph, output: typing.TextIO) -> None:
101+
def write_dot(
102+
wkctx: context.WorkContext,
103+
graph: DependencyGraph,
104+
output: typing.TextIO,
105+
install_only: bool = False,
106+
) -> None:
90107
install_constraints = set(node.key for node in graph.get_install_dependencies())
108+
overridden_packages: set[str] = set(wkctx.settings.list_overrides())
91109

92110
output.write("digraph {\n")
93111
output.write("\n")
94112

95-
seen_nodes = {}
113+
seen_nodes: dict[str, str] = {}
96114
id_generator = itertools.count(1)
97115

98-
def get_node_id(node):
116+
def get_node_id(node: str) -> str:
99117
if node not in seen_nodes:
100118
seen_nodes[node] = f"node{next(id_generator)}"
101119
return seen_nodes[node]
102120

103-
for node in graph.get_all_nodes():
121+
_node_shape_properties = {
122+
"build_settings": "shape=box",
123+
"build": "shape=oval",
124+
"default": "shape=oval",
125+
"patches": "shape=note",
126+
"plugin_and_patches": "shape=tripleoctagon",
127+
"plugin": "shape=trapezium",
128+
"pre_built": "shape=parallelogram",
129+
"toplevel": "shape=circle",
130+
}
131+
132+
# Determine which nodes to include
133+
if install_only:
134+
nodes_to_include = [graph.nodes[ROOT]]
135+
nodes_to_include.extend(graph.get_install_dependencies())
136+
else:
137+
nodes_to_include = list(graph.get_all_nodes())
138+
139+
for node in sorted(nodes_to_include, key=lambda x: x.key):
104140
node_id = get_node_id(node.key)
105-
properties = f'label="{node.key}"'
141+
106142
if not node:
107-
properties = 'label="*"'
108-
if node.key in install_constraints:
109-
properties += " style=filled fillcolor=red color=red fontcolor=white"
143+
label = "*"
110144
else:
111-
properties += " style=filled fillcolor=lightgrey color=lightgrey"
145+
label = node.key
146+
147+
node_type: list[str] = []
148+
name = node.canonicalized_name
149+
if not name:
150+
node_type.append("toplevel")
151+
else:
152+
pbi = wkctx.settings.package_build_info(name)
153+
all_patches: PatchMap = pbi.get_all_patches()
154+
155+
if node.pre_built:
156+
node_type.append("pre_built")
157+
elif pbi.plugin and all_patches:
158+
node_type.append("plugin_and_patches")
159+
elif pbi.plugin:
160+
node_type.append("plugin")
161+
elif all_patches:
162+
node_type.append("patches")
163+
elif name in overridden_packages:
164+
node_type.append("build_settings")
165+
else:
166+
node_type.append("default")
167+
168+
style = "filled"
169+
if not install_only:
170+
if node.key in install_constraints or node.key == ROOT:
171+
style += ",bold"
172+
else:
173+
style += ",dashed"
174+
175+
properties = f'label="{label}" style="{style}" color=black fillcolor=white fontcolor=black '
176+
properties += " ".join(_node_shape_properties[t] for t in node_type)
177+
112178
output.write(f" {node_id} [{properties}]\n")
113179

114180
output.write("\n")
115181

116-
for node in graph.get_all_nodes():
182+
# Create a set of included node keys for efficient lookup
183+
included_node_keys = {node.key for node in nodes_to_include}
184+
185+
known_edges: set[tuple[str, str]] = set()
186+
for node in nodes_to_include:
117187
node_id = get_node_id(node.key)
118188
for edge in node.children:
189+
# Skip edges if we're in install-only mode and the edge is a build dependency
190+
if install_only and edge.req_type not in [
191+
RequirementType.INSTALL,
192+
RequirementType.TOP_LEVEL,
193+
]:
194+
continue
195+
196+
# Skip duplicate edges
197+
if (node.key, edge.destination_node.key) in known_edges:
198+
continue
199+
known_edges.add((node.key, edge.destination_node.key))
200+
201+
# Skip edges to nodes that aren't included
202+
if edge.destination_node.key not in included_node_keys:
203+
continue
204+
119205
child_id = get_node_id(edge.destination_node.key)
120206
sreq = str(edge.req).replace('"', "'")
121207
properties = f'labeltooltip="{sreq}"'
122-
if edge.req_type != "install":
208+
if edge.req_type != RequirementType.INSTALL:
123209
properties += " style=dotted"
210+
124211
output.write(f" {node_id} -> {child_id} [{properties}]\n")
125212
output.write("}\n")
126213

0 commit comments

Comments
 (0)