conandeps: replace kind/level columns with a "via" shortest-path column
Show how each indirect dependency is reached ("libfoo -> libbar") instead
of a bare hop count, "-" for direct dependencies, and "(+N)" when other
packages also require it directly. Rows sort direct-first then by path so
a branch's transitive dependencies stay together.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYNdaGDrpaqDyT8QtrTQbM
This commit is contained in:
parent
0a96c6c2f7
commit
4399d90925
4 changed files with 83 additions and 48 deletions
12
CLAUDE.md
12
CLAUDE.md
|
|
@ -51,11 +51,13 @@ Users install/upgrade on their own machine with
|
|||
replace this with settings-dict matching against `conan search` output.
|
||||
- **"Missing" means missing for the exact profile.** Not "no binary with
|
||||
that build_type at all".
|
||||
- **Dependency table order is by level of indirection.** `_depths()` does a
|
||||
breadth-first walk from the root; `Dep.depth` is the *shortest* path (1 =
|
||||
direct, so a package that is both direct and transitive is direct). Rows
|
||||
sort on (depth, host before build, name) and the `level` column shows the
|
||||
number. The tree section keeps graph order on purpose.
|
||||
- **Dependency table shows the shortest path, not a level number.**
|
||||
`_paths()` does a breadth-first walk from the root; `Dep.path` is the list
|
||||
of package names on the *shortest* path (empty = direct, so a package that
|
||||
is both direct and transitive is direct). The `via` column renders it as
|
||||
`A -> B` (`→` in HTML) with `(+N)` for further direct parents from
|
||||
`Dep.parents`. Rows sort on (depth, path, host before build, name). The
|
||||
tree section keeps graph order and shows every path.
|
||||
- **Only the remote counts, and the source is always shown.** Conan says
|
||||
`Cache` for a locally cached binary without asking the remote, so
|
||||
`_RemoteIndex` verifies cache hits with `search_packages` on the remote.
|
||||
|
|
|
|||
24
README.md
24
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 level ctx Release Debug RelWithDebInfo
|
||||
-----------------------------------------------------------------
|
||||
MyDepA/1.0 direct 1 host knor knor knor
|
||||
MyDepB/1.0 direct 1 host knor knor knor
|
||||
boost/1.8 indirect 2 host knor knor knor
|
||||
Zigma/1.0 indirect 2 host knor knor MISSING
|
||||
package via ctx Release Debug RelWithDebInfo
|
||||
----------------------------------------------------------------------
|
||||
MyDepA/1.0 - host knor knor knor
|
||||
MyDepB/1.0 - host knor knor knor
|
||||
boost/1.8 MyDepA (+1) host knor knor knor
|
||||
Zigma/1.0 MyDepB host knor knor MISSING
|
||||
|
||||
Missing binaries (1)
|
||||
|
||||
|
|
@ -127,11 +127,13 @@ 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: rows are sorted with direct dependencies first
|
||||
(alphabetically), then indirect ones by increasing level of indirection. The
|
||||
`level` column is the number of hops from your conanfile: 1 is direct, 2 is
|
||||
required by a direct dependency, 3 by one of those, and so on (shortest path
|
||||
counts). The HTML report has the same column.
|
||||
Reading the table: the `via` column is the shortest path from your conanfile
|
||||
to the package – `-` for a direct dependency, `MyDepB` for something MyDepB
|
||||
requires, `MyDepB -> Zigma` for something Zigma requires, and so on. `(+N)`
|
||||
means N other packages also require it directly (boost above is pulled in by
|
||||
both MyDepA and Zigma); the dependency hierarchy section shows every path.
|
||||
Rows are sorted direct-first, then by path, so a branch's transitive
|
||||
dependencies stay together.
|
||||
|
||||
|
||||
* `knor` (a remote name) – the remote has a binary for the package_id your
|
||||
|
|
|
|||
66
conandeps.py
66
conandeps.py
|
|
@ -84,6 +84,11 @@ class Dep:
|
|||
# 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
|
||||
# Package names on the shortest path from the consumer to this package,
|
||||
# excluding the package itself: [] for direct, ["A"] for A -> pkg, ...
|
||||
path: List[str] = field(default_factory=list)
|
||||
# Names of every package that directly requires this one (all paths).
|
||||
parents: List[str] = field(default_factory=list)
|
||||
build_require: bool = False
|
||||
requires: List[str] = field(default_factory=list) # names of its own requirements
|
||||
settings: Dict[str, str] = field(default_factory=dict)
|
||||
|
|
@ -248,20 +253,25 @@ 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]
|
||||
def _paths(root) -> Dict[str, List[str]]:
|
||||
"""Breadth-first shortest path (list of package names) from the root to every node.
|
||||
|
||||
The path excludes the node itself, so direct dependencies get []. Among
|
||||
equally short paths the first one found in graph order wins; the other
|
||||
parents are still reported through ``Dep.parents``.
|
||||
"""
|
||||
paths: Dict[str, List[str]] = {}
|
||||
frontier = [(e.dst, []) for e in root.dependencies]
|
||||
while frontier:
|
||||
next_frontier = []
|
||||
for node, depth in frontier:
|
||||
for node, path in frontier:
|
||||
key = _node_key(node)
|
||||
if key in depths:
|
||||
if key in paths:
|
||||
continue
|
||||
depths[key] = depth
|
||||
next_frontier.extend((e.dst, depth + 1) for e in node.dependencies)
|
||||
paths[key] = path
|
||||
next_frontier.extend((e.dst, path + [node.ref.name]) for e in node.dependencies)
|
||||
frontier = next_frontier
|
||||
return depths
|
||||
return paths
|
||||
|
||||
|
||||
def _collect(
|
||||
|
|
@ -278,7 +288,7 @@ def _collect(
|
|||
"""
|
||||
root = graph.root
|
||||
direct = {_node_key(e.dst) for e in root.dependencies}
|
||||
depths = _depths(root)
|
||||
paths = _paths(root)
|
||||
for node in graph.nodes:
|
||||
if node is root:
|
||||
continue
|
||||
|
|
@ -288,8 +298,14 @@ 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
|
||||
path = paths.get(key, [])
|
||||
depth = len(path) + 1
|
||||
if not dep.depth or depth < dep.depth:
|
||||
dep.depth, dep.path = depth, path
|
||||
for edge in node.dependants:
|
||||
parent = "your conanfile" if edge.src is root else edge.src.ref.name
|
||||
if parent not in dep.parents:
|
||||
dep.parents.append(parent)
|
||||
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
|
||||
|
|
@ -375,7 +391,10 @@ def build_report(
|
|||
|
||||
# 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()))
|
||||
ordered = sorted(
|
||||
deps.values(),
|
||||
key=lambda d: (d.depth, [p.lower() for p in d.path], 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(
|
||||
|
|
@ -520,8 +539,17 @@ def _override_kind(o: Override) -> str:
|
|||
return "explicit (override=True)" if o.explicit else "implicit (newer requirement downstream)"
|
||||
|
||||
|
||||
def _kind(dep: Dep) -> str:
|
||||
return "direct" if dep.direct else "indirect"
|
||||
def _via(dep: Dep, arrow: str = " -> ") -> str:
|
||||
"""Shortest path to the package: '-' for direct, 'A -> B' for consumer -> A -> B -> pkg.
|
||||
|
||||
``(+N)`` marks N further packages that also require it directly; the
|
||||
dependency hierarchy section shows every path in full.
|
||||
"""
|
||||
if dep.direct:
|
||||
return "-"
|
||||
via = arrow.join(dep.path)
|
||||
extra = len(dep.parents) - 1
|
||||
return via + (" (+%d)" % extra if extra > 0 else "")
|
||||
|
||||
|
||||
def render_text(report: Report, color: bool = False) -> str:
|
||||
|
|
@ -537,11 +565,11 @@ def render_text(report: Report, color: bool = False) -> str:
|
|||
w(pal.bold("Dependencies (%d) and binary availability" % len(report.deps)))
|
||||
w("")
|
||||
rows = [
|
||||
[dep.ref, _kind(dep), str(dep.depth), dep.context]
|
||||
[dep.ref, _via(dep), dep.context]
|
||||
+ [_cell(*dep.binaries[bt], pal) for bt in report.build_types]
|
||||
for dep in report.deps.values()
|
||||
]
|
||||
out.extend(_table(["package", "kind", "level", "ctx"] + list(report.build_types), rows, pal))
|
||||
out.extend(_table(["package", "via", "ctx"] + list(report.build_types), rows, pal))
|
||||
w("")
|
||||
|
||||
# ---- missing binaries ------------------------------------------------
|
||||
|
|
@ -640,14 +668,14 @@ 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), e(_kind(dep)), str(dep.depth), e(dep.context)]
|
||||
row = [e(dep.ref), e(_via(dep, " \u2192 ")), 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")
|
||||
text = e(_cell(status, pid, remote, _Palette(False)))
|
||||
row.append('<span class="%s" title="package_id %s">%s</span>' % (cls, e(pid), text))
|
||||
rows.append(row)
|
||||
w(_html_table(["package", "kind", "level", "context"] + list(report.build_types), rows))
|
||||
w(_html_table(["package", "via", "context"] + list(report.build_types), rows))
|
||||
|
||||
# ---- missing binaries ------------------------------------------------
|
||||
problems = report.problems()
|
||||
|
|
|
|||
|
|
@ -231,23 +231,26 @@ 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.
|
||||
def test_sorted_by_path(report):
|
||||
order = [(d.ref, d.path) for d in report.deps.values()]
|
||||
# Direct deps first (alphabetical), then by shortest path.
|
||||
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),
|
||||
("hdr/1.0", []),
|
||||
("libbaz/2.0", []),
|
||||
("libfoo/1.0", []),
|
||||
("libqux/1.1", []), # required directly, even though libbaz also pulls it in
|
||||
("libbar/1.1", ["libfoo"]),
|
||||
("libdeep/1.0", ["libfoo", "libbar"]),
|
||||
]
|
||||
# libqux is required by both the consumer and libbaz -> two parents.
|
||||
assert sorted(report.deps["libqux#host"].parents) == ["libbaz", "your conanfile"]
|
||||
text = conandeps.render_text(report)
|
||||
assert re.search(r"libbar/1\.1\s+indirect\s+2\s+host", text)
|
||||
assert re.search(r"libdeep/1\.0\s+indirect\s+3\s+host", text)
|
||||
assert re.search(r"hdr/1\.0\s+-\s+host", text)
|
||||
assert re.search(r"libbar/1\.1\s+libfoo\s+host", text)
|
||||
assert re.search(r"libdeep/1\.0\s+libfoo -> libbar\s+host", text)
|
||||
page = conandeps.render_html(report)
|
||||
assert "<th>level</th>" in page
|
||||
assert "<td>libdeep/1.0</td><td>indirect</td><td>3</td><td>host</td>" in page
|
||||
assert "<th>via</th>" in page
|
||||
assert "<td>libdeep/1.0</td><td>libfoo \u2192 libbar</td><td>host</td>" in page
|
||||
|
||||
|
||||
def test_missing_binaries(report):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue