conandeps: sort dependencies by level of indirection
Record each package's shortest distance from the consumer while collecting the graph (1 = direct) and order the dependency table direct-first, then by increasing depth, alphabetically within a level; the kind column now reads "indirect (N)". Fixture gains a depth-3 package (libdeep under libbar) and a test pins the ordering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYNdaGDrpaqDyT8QtrTQbM
This commit is contained in:
parent
857500f20e
commit
d44abc43ef
3 changed files with 82 additions and 15 deletions
18
README.md
18
README.md
|
|
@ -92,12 +92,12 @@ profile: arch=x86_64, compiler=gcc, compiler.libcxx=libstdc++11, compiler.versio
|
|||
|
||||
Dependencies (4) and binary availability
|
||||
|
||||
package kind ctx Release Debug RelWithDebInfo
|
||||
----------------------------------------------------------
|
||||
MyDepA/1.0 direct host knor knor knor
|
||||
boost/1.8 indirect host knor knor knor
|
||||
MyDepB/1.0 direct host knor knor knor
|
||||
Zigma/1.0 indirect host knor knor MISSING
|
||||
package kind ctx Release Debug RelWithDebInfo
|
||||
--------------------------------------------------------------
|
||||
MyDepA/1.0 direct host knor knor knor
|
||||
MyDepB/1.0 direct host knor knor knor
|
||||
boost/1.8 indirect (2) host knor knor knor
|
||||
Zigma/1.0 indirect (2) host knor knor MISSING
|
||||
|
||||
Missing binaries (1)
|
||||
|
||||
|
|
@ -127,7 +127,11 @@ environment variable turns that off, and redirecting stdout to a file never
|
|||
produces colour codes unless you pass `--color always`. The HTML report uses
|
||||
the same colour coding.
|
||||
|
||||
Reading the table:
|
||||
Reading the table: rows are sorted with direct dependencies first
|
||||
(alphabetically), then indirect ones by increasing level of indirection –
|
||||
`indirect (2)` is required by a direct dependency, `indirect (3)` by one of
|
||||
those, and so on (shortest path counts).
|
||||
|
||||
|
||||
* `knor` (a remote name) – the remote has a binary for the package_id your
|
||||
profile produces. Hover a cell in the HTML report to see the package_id.
|
||||
|
|
|
|||
39
conandeps.py
39
conandeps.py
|
|
@ -81,6 +81,9 @@ class Dep:
|
|||
ref: str
|
||||
context: str # "host" or "build"
|
||||
direct: bool = False
|
||||
# Shortest path length from the consumer: 1 = direct, 2 = required by a
|
||||
# direct dependency, ... Used to order the table by level of indirection.
|
||||
depth: int = 0
|
||||
build_require: bool = False
|
||||
requires: List[str] = field(default_factory=list) # names of its own requirements
|
||||
settings: Dict[str, str] = field(default_factory=dict)
|
||||
|
|
@ -170,6 +173,10 @@ def _node_key(node) -> str:
|
|||
return "%s#%s" % (node.ref.name, node.context)
|
||||
|
||||
|
||||
def _key_of(dep: Dep) -> str:
|
||||
return "%s#%s" % (dep.name, dep.context)
|
||||
|
||||
|
||||
def _explicit_overrides(graph) -> set:
|
||||
"""Set of (declaring ref or 'your conanfile', package name) for override=True requires."""
|
||||
result = set()
|
||||
|
|
@ -241,6 +248,22 @@ class _RemoteIndex:
|
|||
return found
|
||||
|
||||
|
||||
def _depths(root) -> Dict[str, int]:
|
||||
"""Breadth-first shortest distance from the root for every node key."""
|
||||
depths: Dict[str, int] = {}
|
||||
frontier = [(e.dst, 1) for e in root.dependencies]
|
||||
while frontier:
|
||||
next_frontier = []
|
||||
for node, depth in frontier:
|
||||
key = _node_key(node)
|
||||
if key in depths:
|
||||
continue
|
||||
depths[key] = depth
|
||||
next_frontier.extend((e.dst, depth + 1) for e in node.dependencies)
|
||||
frontier = next_frontier
|
||||
return depths
|
||||
|
||||
|
||||
def _collect(
|
||||
graph,
|
||||
build_type: str,
|
||||
|
|
@ -255,6 +278,7 @@ def _collect(
|
|||
"""
|
||||
root = graph.root
|
||||
direct = {_node_key(e.dst) for e in root.dependencies}
|
||||
depths = _depths(root)
|
||||
for node in graph.nodes:
|
||||
if node is root:
|
||||
continue
|
||||
|
|
@ -264,6 +288,8 @@ def _collect(
|
|||
dep = Dep(ref=str(node.ref), context=node.context)
|
||||
deps[key] = dep
|
||||
dep.direct = dep.direct or key in direct
|
||||
depth = depths.get(key, 0)
|
||||
dep.depth = min(dep.depth, depth) if dep.depth else depth
|
||||
dep.build_require = dep.build_require or any(e.build_require for e in node.dependants)
|
||||
for edge in node.dependencies:
|
||||
name = edge.dst.ref.name
|
||||
|
|
@ -347,6 +373,10 @@ def build_report(
|
|||
overrides = _parse_overrides(log.getvalue(), explicit)
|
||||
_progress(quiet, "%d overrides detected" % len(overrides))
|
||||
|
||||
# Direct dependencies first, then by increasing level of indirection,
|
||||
# alphabetically within a level. Build-context nodes sort after host.
|
||||
ordered = sorted(deps.values(), key=lambda d: (d.depth, d.context != "host", d.name.lower()))
|
||||
deps = OrderedDict((_key_of(d), d) for d in ordered)
|
||||
for dep in deps.values(): # keep column order stable
|
||||
dep.binaries = OrderedDict(
|
||||
(bt, dep.binaries.get(bt, ("n/a", "", None))) for bt in build_types
|
||||
|
|
@ -490,6 +520,11 @@ def _override_kind(o: Override) -> str:
|
|||
return "explicit (override=True)" if o.explicit else "implicit (newer requirement downstream)"
|
||||
|
||||
|
||||
def _kind(dep: Dep) -> str:
|
||||
"""'direct', or 'indirect (N)' where N is the number of hops from the consumer."""
|
||||
return "direct" if dep.direct else "indirect (%d)" % dep.depth
|
||||
|
||||
|
||||
def render_text(report: Report, color: bool = False) -> str:
|
||||
pal = _Palette(color)
|
||||
out: List[str] = []
|
||||
|
|
@ -503,7 +538,7 @@ def render_text(report: Report, color: bool = False) -> str:
|
|||
w(pal.bold("Dependencies (%d) and binary availability" % len(report.deps)))
|
||||
w("")
|
||||
rows = [
|
||||
[dep.ref, "direct" if dep.direct else "indirect", dep.context]
|
||||
[dep.ref, _kind(dep), dep.context]
|
||||
+ [_cell(*dep.binaries[bt], pal) for bt in report.build_types]
|
||||
for dep in report.deps.values()
|
||||
]
|
||||
|
|
@ -606,7 +641,7 @@ def render_html(report: Report) -> str:
|
|||
w("<h2>Dependencies (%d) and binary availability</h2>" % len(report.deps))
|
||||
rows = []
|
||||
for dep in report.deps.values():
|
||||
row = [e(dep.ref), "direct" if dep.direct else "indirect", e(dep.context)]
|
||||
row = [e(dep.ref), e(_kind(dep)), e(dep.context)]
|
||||
for bt in report.build_types:
|
||||
status, pid, remote = dep.binaries[bt]
|
||||
cls = {"Remote": "ok", "Missing": "missing", "CacheOnly": "cacheonly"}.get(status, "na")
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ the usual settings but their build() does nothing, so no compiler is needed):
|
|||
consumer (conanfile.py)
|
||||
|-- libfoo/1.0 binaries: Release, Debug, RelWithDebInfo
|
||||
| `-- libbar/1.0 binaries: Release only <- MISSING Debug/RWDI
|
||||
| `-- libdeep/1.0 binaries: all three <- depth 3
|
||||
|-- libbar/1.1 (override=True) <- explicit override
|
||||
|-- libbaz/2.0 binaries: all three
|
||||
| `-- libqux/1.0 binaries: all three
|
||||
|
|
@ -17,6 +18,7 @@ remotes are never touched.
|
|||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
|
|
@ -177,8 +179,9 @@ def conan_env(tmp_path_factory):
|
|||
_conan(env, "create", str(d), "-s", "build_type=%s" % bt, "--build=missing")
|
||||
_conan(env, "upload", "%s/%s" % (name, version), "-r", "test", "--all", "-c")
|
||||
|
||||
create("libbar", "1.0", build_types=("Release",))
|
||||
create("libbar", "1.1", build_types=("Release",))
|
||||
create("libdeep", "1.0")
|
||||
create("libbar", "1.0", requires=("libdeep/1.0",), build_types=("Release",))
|
||||
create("libbar", "1.1", requires=("libdeep/1.0",), build_types=("Release",))
|
||||
create("libfoo", "1.0", requires=("libbar/1.0",))
|
||||
create("libqux", "1.0")
|
||||
create("libqux", "1.1")
|
||||
|
|
@ -213,7 +216,14 @@ def report(conan_env):
|
|||
|
||||
def test_dependencies_listed(report):
|
||||
refs = {d.ref for d in report.deps.values()}
|
||||
assert refs == {"libfoo/1.0", "libbar/1.1", "libbaz/2.0", "libqux/1.1", "hdr/1.0"}
|
||||
assert refs == {
|
||||
"libfoo/1.0",
|
||||
"libbar/1.1",
|
||||
"libbaz/2.0",
|
||||
"libqux/1.1",
|
||||
"hdr/1.0",
|
||||
"libdeep/1.0",
|
||||
}
|
||||
direct = {d.ref for d in report.deps.values() if d.direct}
|
||||
assert direct == {"libfoo/1.0", "libbaz/2.0", "libqux/1.1", "hdr/1.0"}
|
||||
# libbar is only reachable through libfoo -> indirect, even though the
|
||||
|
|
@ -221,6 +231,22 @@ def test_dependencies_listed(report):
|
|||
assert not report.deps["libbar#host"].direct
|
||||
|
||||
|
||||
def test_sorted_by_level_of_indirection(report):
|
||||
order = [(d.ref, d.depth) for d in report.deps.values()]
|
||||
# Direct deps first (alphabetical), then depth 2, then depth 3.
|
||||
assert order == [
|
||||
("hdr/1.0", 1),
|
||||
("libbaz/2.0", 1),
|
||||
("libfoo/1.0", 1),
|
||||
("libqux/1.1", 1), # required directly, even though libbaz also pulls it in
|
||||
("libbar/1.1", 2),
|
||||
("libdeep/1.0", 3),
|
||||
]
|
||||
text = conandeps.render_text(report)
|
||||
assert re.search(r"libbar/1\.1\s+indirect \(2\)", text)
|
||||
assert re.search(r"libdeep/1\.0\s+indirect \(3\)", text)
|
||||
|
||||
|
||||
def test_missing_binaries(report):
|
||||
missing = {d.ref: d.missing() for d in report.deps.values() if d.missing()}
|
||||
assert missing == {"libbar/1.1": ["Debug", "RelWithDebInfo"]}
|
||||
|
|
@ -243,7 +269,9 @@ def test_cache_only_is_not_available(conan_env):
|
|||
env = dict(os.environ)
|
||||
d = Path(conan_env["home"]) / "libbar-local"
|
||||
d.mkdir()
|
||||
(d / "conanfile.py").write_text(RECIPE.format(name="libbar", version="1.1", requires=""))
|
||||
(d / "conanfile.py").write_text(
|
||||
RECIPE.format(name="libbar", version="1.1", requires='requires = ("libdeep/1.0",)')
|
||||
)
|
||||
_conan(env, "create", str(d), "-s", "build_type=Debug")
|
||||
_conan(env, "install", "libbar/1.1@", "-s", "build_type=Release", "-r", "test")
|
||||
|
||||
|
|
@ -275,11 +303,11 @@ def test_text_and_html_render(report, tmp_path):
|
|||
text = conandeps.render_text(report)
|
||||
assert "libbar/1.1" in text and "MISSING" in text
|
||||
table = text.split("Missing binaries")[0]
|
||||
assert table.count("test") >= 4 * 3 - 2 # every available cell names the remote
|
||||
assert table.count("test") >= 5 * 3 - 2 # every available cell names the remote
|
||||
assert "\033[" not in text # no ANSI codes unless asked for
|
||||
# Missing binaries and overrides are tables with a header row.
|
||||
assert "package build type package_id" in text
|
||||
assert "libbar/1.1 Debug 8bfd7e15c6920f4673ca8dee419ab5320ae445e4 no binary" in text
|
||||
assert re.search(r"libbar/1\.1\s+Debug\s+[0-9a-f]{40}\s+no binary anywhere", text)
|
||||
assert "package wanted forced to by kind" in text
|
||||
assert "libfoo/1.0 libbar/1.0 libbar/1.1 your conanfile explicit (override=True)" in text
|
||||
assert "overrides libbar/1.0 (explicit" in text # tree edge annotation
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue