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:
Ole-Morten Duesund 2026-08-25 15:29:46 +02:00
commit d44abc43ef
3 changed files with 82 additions and 15 deletions

View file

@ -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