From 93949c5f7661a04947157280b518f8f44ccc16c0 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 22 May 2021 22:29:20 +0800 Subject: [PATCH 1/5] improve context resolve failure info --- src/rez/resolved_context.py | 25 +++++-- src/rez/utils/graph_utils.py | 127 +++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 6 deletions(-) diff --git a/src/rez/resolved_context.py b/src/rez/resolved_context.py index 8ff3ca868..409675c81 100644 --- a/src/rez/resolved_context.py +++ b/src/rez/resolved_context.py @@ -28,7 +28,8 @@ from rez.shells import create_shell from rez.exceptions import ResolvedContextError, PackageCommandError, \ RezError, _NeverError, PackageCacheError, PackageNotFoundError -from rez.utils.graph_utils import write_dot, write_compacted, read_graph_from_string +from rez.utils.graph_utils import write_dot, write_compacted, \ + read_graph_from_string, failure_detail_from_graph from rez.vendor.six import six from rez.vendor.version.version import VersionRange from rez.vendor.version.requirement import Requirement @@ -807,13 +808,13 @@ def _rt(t): return time.strftime("%a %b %d %H:%M:%S %Y", time.localtime(t)) if self.status_ in (ResolverStatus.failed, ResolverStatus.aborted): - _pr("The context failed to resolve:\n%s" - % self.failure_description, critical) - return + res_status = "resolve failed," + else: + res_status = "resolved" t_str = _rt(self.created) - _pr("resolved by %s@%s, on %s, using Rez v%s" - % (self.user, self.host, t_str, self.rez_version)) + _pr("%s by %s@%s, on %s, using Rez v%s" + % (res_status, self.user, self.host, t_str, self.rez_version)) if self.requested_timestamp: t_str = _rt(self.requested_timestamp) _pr("packages released after %s were ignored" % t_str) @@ -863,6 +864,18 @@ def _rt(t): _pr(line, col) _pr() + # show resolved, or not + # + if self.status_ in (ResolverStatus.failed, ResolverStatus.aborted): + _pr("The context failed to resolve:\n%s" + % self.failure_description, critical) + + _pr() + _pr(failure_detail_from_graph(self.graph(as_dot=False))) + _pr() + + return + _pr("resolved packages:", heading) rows = [] colors = [] diff --git a/src/rez/utils/graph_utils.py b/src/rez/utils/graph_utils.py index 92639fa1a..1af71094f 100644 --- a/src/rez/utils/graph_utils.py +++ b/src/rez/utils/graph_utils.py @@ -306,6 +306,133 @@ def _request_from_label(label): return label.strip('"').strip("'").rsplit('[', 1)[0] +def _get_node_label(graph, node): + label_ = next(at[1] for at in graph.node_attr[node] if at[0] == "label") + return _request_from_label(label_) + + +def _is_request_node(graph, node): + style = next(at[1] for at in graph.node_attr[node] if at[0] == "style") + return "dashed" in style + + +def _iter_init_request_nodes(graph): + request_color = "#FFFFAA" # NOTE: the color code is hardcoded in `solver` + for node, attrs in graph.node_attr.items(): + for at in attrs: + if at[0] == "fillcolor" and at[1] == request_color: + yield node + + +def failure_detail_from_graph(graph): + """Generate detailed resolve failure messages from graph + + Args: + graph (rez.vendor.pygraph.classes.digraph.digraph): context graph object + + """ + # Base on `rez.solver._ResolvePhase.get_graph()` + # the failure reason has three types: + # + # * DependencyConflicts + # which have "conflict" edge in graph + # + # * TotalReduction + # which have "conflict" edge and may also have "reduct" edge in graph + # + # * Cycle + # which have "cycle" edge in graph + # + # so we find cycle edge first, and try other if not found. + # + + # find cycle + cycled_edge = next((k for k, v in graph.edge_properties.items() + if v["label"] == "CYCLE"), None) + if cycled_edge: + return _cycled_detail_from_graph(graph) + + # find conflict + conflicted_edge = next((k for k, v in graph.edge_properties.items() + if v["label"] == "CONFLICT"), None) + if conflicted_edge: + return _conflicted_detail_from_graph(graph, conflicted_edge) + + # should be a healthy graph + return "" + + +def _cycled_detail_from_graph(graph): + """Find all initial requests, and walk down till circle back""" + + messages = ["Resolve paths starting from initial requests to cycle:"] + + for init_request in _iter_init_request_nodes(graph): + node = init_request + visited = list() + while True: + visited.append(node) + down = next((ne for ne in graph.node_neighbors[node]), None) + if down in visited and not _is_request_node(graph, down): + visited.append(down) # circle back + break + if down is None: + break + + node = down + + line = " %s ?" % _get_node_label(graph, visited[0]) # init request + for node in visited[1:]: + # should be more readable if opt-out requests + if not _is_request_node(graph, node): + line += " --> %s" % _get_node_label(graph, node) + + messages.append(line) + + return "\n".join(messages) + + +def _conflicted_detail_from_graph(graph, conflicted_edge): + """Find all initial requests, and walk down till in conflicted edge""" + + messages = ["Resolve paths starting from initial requests to conflict:"] + + for init_request in _iter_init_request_nodes(graph): + node = init_request + visited = list() + while True: + visited.append(node) + down = next((ne for ne in graph.node_neighbors[node]), None) + if down is None: + break + + node = down + + line = " %s ?" % _get_node_label(graph, visited[0]) # init request + for node in visited[1:]: + # should be more readable if opt-out requests + if not _is_request_node(graph, node): + line += " --> %s" % _get_node_label(graph, node) + + # show conflicted request at the end + if _is_request_node(graph, node) and node in conflicted_edge: + line += " >>> %s" % _get_node_label(graph, node) + break + + messages.append(line) + + return "\n".join(messages) + + +def _print_each_graph_edges(graph): + """for debug""" + for (from_, to_), properties in graph.edge_properties.items(): + edge_status = properties["label"] + from_name = _get_node_label(graph, from_) + to_name = _get_node_label(graph, to_) + print("%s -> %s %s" % (from_name, to_name, edge_status)) + + # Copyright 2013-2016 Allan Johns. # # This library is free software: you can redistribute it and/or From 7f00b51386b24edd658aa125e989096eecc12b5c Mon Sep 17 00:00:00 2001 From: David Lai Date: Tue, 25 May 2021 18:05:21 +0800 Subject: [PATCH 2/5] cleaner cycle chain message --- src/rez/utils/graph_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rez/utils/graph_utils.py b/src/rez/utils/graph_utils.py index 1af71094f..08f64b947 100644 --- a/src/rez/utils/graph_utils.py +++ b/src/rez/utils/graph_utils.py @@ -350,7 +350,7 @@ def failure_detail_from_graph(graph): cycled_edge = next((k for k, v in graph.edge_properties.items() if v["label"] == "CYCLE"), None) if cycled_edge: - return _cycled_detail_from_graph(graph) + return _cycled_detail_from_graph(graph, cycled_edge) # find conflict conflicted_edge = next((k for k, v in graph.edge_properties.items() @@ -362,7 +362,7 @@ def failure_detail_from_graph(graph): return "" -def _cycled_detail_from_graph(graph): +def _cycled_detail_from_graph(graph, cycled_edge): """Find all initial requests, and walk down till circle back""" messages = ["Resolve paths starting from initial requests to cycle:"] @@ -373,8 +373,8 @@ def _cycled_detail_from_graph(graph): while True: visited.append(node) down = next((ne for ne in graph.node_neighbors[node]), None) - if down in visited and not _is_request_node(graph, down): - visited.append(down) # circle back + if down in cycled_edge: + visited.append(down) break if down is None: break From fc1c1057f7cec76fc8e0de3381227ce022c47d33 Mon Sep 17 00:00:00 2001 From: David Lai Date: Tue, 25 May 2021 18:06:06 +0800 Subject: [PATCH 3/5] remove extra marks --- src/rez/utils/graph_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rez/utils/graph_utils.py b/src/rez/utils/graph_utils.py index 08f64b947..b185e2d90 100644 --- a/src/rez/utils/graph_utils.py +++ b/src/rez/utils/graph_utils.py @@ -381,7 +381,7 @@ def _cycled_detail_from_graph(graph, cycled_edge): node = down - line = " %s ?" % _get_node_label(graph, visited[0]) # init request + line = " %s" % _get_node_label(graph, visited[0]) # init request for node in visited[1:]: # should be more readable if opt-out requests if not _is_request_node(graph, node): @@ -408,7 +408,7 @@ def _conflicted_detail_from_graph(graph, conflicted_edge): node = down - line = " %s ?" % _get_node_label(graph, visited[0]) # init request + line = " %s" % _get_node_label(graph, visited[0]) # init request for node in visited[1:]: # should be more readable if opt-out requests if not _is_request_node(graph, node): @@ -416,7 +416,7 @@ def _conflicted_detail_from_graph(graph, conflicted_edge): # show conflicted request at the end if _is_request_node(graph, node) and node in conflicted_edge: - line += " >>> %s" % _get_node_label(graph, node) + line += " --> %s" % _get_node_label(graph, node) break messages.append(line) From 11d71004e7046b23c78331e68fd7e0d2d66e39f3 Mon Sep 17 00:00:00 2001 From: David Lai Date: Tue, 25 May 2021 18:36:46 +0800 Subject: [PATCH 4/5] add fail graph viewing hint message --- src/rez/resolved_context.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/rez/resolved_context.py b/src/rez/resolved_context.py index 409675c81..0f07ddd7c 100644 --- a/src/rez/resolved_context.py +++ b/src/rez/resolved_context.py @@ -873,6 +873,9 @@ def _rt(t): _pr() _pr(failure_detail_from_graph(self.graph(as_dot=False))) _pr() + _pr("To see a graph of the failed resolution, add --fail-graph " + "in your rez-env or rez-build command.") + _pr() return From 41606c1d782587596eca7ac2fad747fcea70a951 Mon Sep 17 00:00:00 2001 From: David Lai Date: Tue, 25 May 2021 18:41:23 +0800 Subject: [PATCH 5/5] separate module --- src/rez/resolved_context.py | 3 +- src/rez/utils/graph_utils.py | 127 -------------------------------- src/rez/utils/resolve_graph.py | 129 +++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 128 deletions(-) create mode 100644 src/rez/utils/resolve_graph.py diff --git a/src/rez/resolved_context.py b/src/rez/resolved_context.py index 0f07ddd7c..d57dd8361 100644 --- a/src/rez/resolved_context.py +++ b/src/rez/resolved_context.py @@ -29,7 +29,8 @@ from rez.exceptions import ResolvedContextError, PackageCommandError, \ RezError, _NeverError, PackageCacheError, PackageNotFoundError from rez.utils.graph_utils import write_dot, write_compacted, \ - read_graph_from_string, failure_detail_from_graph + read_graph_from_string +from rez.utils.resolve_graph import failure_detail_from_graph from rez.vendor.six import six from rez.vendor.version.version import VersionRange from rez.vendor.version.requirement import Requirement diff --git a/src/rez/utils/graph_utils.py b/src/rez/utils/graph_utils.py index b185e2d90..92639fa1a 100644 --- a/src/rez/utils/graph_utils.py +++ b/src/rez/utils/graph_utils.py @@ -306,133 +306,6 @@ def _request_from_label(label): return label.strip('"').strip("'").rsplit('[', 1)[0] -def _get_node_label(graph, node): - label_ = next(at[1] for at in graph.node_attr[node] if at[0] == "label") - return _request_from_label(label_) - - -def _is_request_node(graph, node): - style = next(at[1] for at in graph.node_attr[node] if at[0] == "style") - return "dashed" in style - - -def _iter_init_request_nodes(graph): - request_color = "#FFFFAA" # NOTE: the color code is hardcoded in `solver` - for node, attrs in graph.node_attr.items(): - for at in attrs: - if at[0] == "fillcolor" and at[1] == request_color: - yield node - - -def failure_detail_from_graph(graph): - """Generate detailed resolve failure messages from graph - - Args: - graph (rez.vendor.pygraph.classes.digraph.digraph): context graph object - - """ - # Base on `rez.solver._ResolvePhase.get_graph()` - # the failure reason has three types: - # - # * DependencyConflicts - # which have "conflict" edge in graph - # - # * TotalReduction - # which have "conflict" edge and may also have "reduct" edge in graph - # - # * Cycle - # which have "cycle" edge in graph - # - # so we find cycle edge first, and try other if not found. - # - - # find cycle - cycled_edge = next((k for k, v in graph.edge_properties.items() - if v["label"] == "CYCLE"), None) - if cycled_edge: - return _cycled_detail_from_graph(graph, cycled_edge) - - # find conflict - conflicted_edge = next((k for k, v in graph.edge_properties.items() - if v["label"] == "CONFLICT"), None) - if conflicted_edge: - return _conflicted_detail_from_graph(graph, conflicted_edge) - - # should be a healthy graph - return "" - - -def _cycled_detail_from_graph(graph, cycled_edge): - """Find all initial requests, and walk down till circle back""" - - messages = ["Resolve paths starting from initial requests to cycle:"] - - for init_request in _iter_init_request_nodes(graph): - node = init_request - visited = list() - while True: - visited.append(node) - down = next((ne for ne in graph.node_neighbors[node]), None) - if down in cycled_edge: - visited.append(down) - break - if down is None: - break - - node = down - - line = " %s" % _get_node_label(graph, visited[0]) # init request - for node in visited[1:]: - # should be more readable if opt-out requests - if not _is_request_node(graph, node): - line += " --> %s" % _get_node_label(graph, node) - - messages.append(line) - - return "\n".join(messages) - - -def _conflicted_detail_from_graph(graph, conflicted_edge): - """Find all initial requests, and walk down till in conflicted edge""" - - messages = ["Resolve paths starting from initial requests to conflict:"] - - for init_request in _iter_init_request_nodes(graph): - node = init_request - visited = list() - while True: - visited.append(node) - down = next((ne for ne in graph.node_neighbors[node]), None) - if down is None: - break - - node = down - - line = " %s" % _get_node_label(graph, visited[0]) # init request - for node in visited[1:]: - # should be more readable if opt-out requests - if not _is_request_node(graph, node): - line += " --> %s" % _get_node_label(graph, node) - - # show conflicted request at the end - if _is_request_node(graph, node) and node in conflicted_edge: - line += " --> %s" % _get_node_label(graph, node) - break - - messages.append(line) - - return "\n".join(messages) - - -def _print_each_graph_edges(graph): - """for debug""" - for (from_, to_), properties in graph.edge_properties.items(): - edge_status = properties["label"] - from_name = _get_node_label(graph, from_) - to_name = _get_node_label(graph, to_) - print("%s -> %s %s" % (from_name, to_name, edge_status)) - - # Copyright 2013-2016 Allan Johns. # # This library is free software: you can redistribute it and/or diff --git a/src/rez/utils/resolve_graph.py b/src/rez/utils/resolve_graph.py new file mode 100644 index 000000000..0d35344ae --- /dev/null +++ b/src/rez/utils/resolve_graph.py @@ -0,0 +1,129 @@ + +from rez.utils.graph_utils import _request_from_label + + +def failure_detail_from_graph(graph): + """Generate detailed resolve failure messages from graph + + Args: + graph (rez.vendor.pygraph.classes.digraph.digraph): context graph object + + """ + # Base on `rez.solver._ResolvePhase.get_graph()` + # the failure reason has three types: + # + # * DependencyConflicts + # which have "conflict" edge in graph + # + # * TotalReduction + # which have "conflict" edge and may also have "reduct" edge in graph + # + # * Cycle + # which have "cycle" edge in graph + # + # so we find cycle edge first, and try other if not found. + # + + # find cycle + cycled_edge = next((k for k, v in graph.edge_properties.items() + if v["label"] == "CYCLE"), None) + if cycled_edge: + return _cycled_detail_from_graph(graph, cycled_edge) + + # find conflict + conflicted_edge = next((k for k, v in graph.edge_properties.items() + if v["label"] == "CONFLICT"), None) + if conflicted_edge: + return _conflicted_detail_from_graph(graph, conflicted_edge) + + # should be a healthy graph + return "" + + +def _cycled_detail_from_graph(graph, cycled_edge): + """Find all initial requests, and walk down till circle back""" + + messages = ["Resolve paths starting from initial requests to cycle:"] + + for init_request in _iter_init_request_nodes(graph): + node = init_request + visited = list() + while True: + visited.append(node) + down = next((ne for ne in graph.node_neighbors[node]), None) + if down in cycled_edge: + visited.append(down) + break + if down is None: + break + + node = down + + line = " %s" % _get_node_label(graph, visited[0]) # init request + for node in visited[1:]: + # should be more readable if opt-out requests + if not _is_request_node(graph, node): + line += " --> %s" % _get_node_label(graph, node) + + messages.append(line) + + return "\n".join(messages) + + +def _conflicted_detail_from_graph(graph, conflicted_edge): + """Find all initial requests, and walk down till in conflicted edge""" + + messages = ["Resolve paths starting from initial requests to conflict:"] + + for init_request in _iter_init_request_nodes(graph): + node = init_request + visited = list() + while True: + visited.append(node) + down = next((ne for ne in graph.node_neighbors[node]), None) + if down is None: + break + + node = down + + line = " %s" % _get_node_label(graph, visited[0]) # init request + for node in visited[1:]: + # should be more readable if opt-out requests + if not _is_request_node(graph, node): + line += " --> %s" % _get_node_label(graph, node) + + # show conflicted request at the end + if _is_request_node(graph, node) and node in conflicted_edge: + line += " --> %s" % _get_node_label(graph, node) + break + + messages.append(line) + + return "\n".join(messages) + + +def _iter_init_request_nodes(graph): + request_color = "#FFFFAA" # NOTE: the color code is hardcoded in `solver` + for node, attrs in graph.node_attr.items(): + for at in attrs: + if at[0] == "fillcolor" and at[1] == request_color: + yield node + + +def _get_node_label(graph, node): + label_ = next(at[1] for at in graph.node_attr[node] if at[0] == "label") + return _request_from_label(label_) + + +def _is_request_node(graph, node): + style = next(at[1] for at in graph.node_attr[node] if at[0] == "style") + return "dashed" in style + + +def _print_each_graph_edges(graph): + """for debug""" + for (from_, to_), properties in graph.edge_properties.items(): + edge_status = properties["label"] + from_name = _get_node_label(graph, from_) + to_name = _get_node_label(graph, to_) + print("%s -> %s %s" % (from_name, to_name, edge_status))