diff --git a/CLAUDE.md b/CLAUDE.md
index 6684129..3ee26d7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -56,10 +56,11 @@ Users install/upgrade on their own machine with
- **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.
+ is both direct and transitive is direct). `Dep.paths` holds one path per
+ direct parent (shortest path to that parent + parent) – bounded, unlike
+ "all paths". The `via` column renders them one per line as `A -> B`
+ (`→`/`
` in HTML); `_table()` supports multi-line cells. Rows sort on
+ (depth, path, host before build, name). The tree section keeps graph order.
- **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.
diff --git a/README.md b/README.md
index e303467..224d566 100644
--- a/README.md
+++ b/README.md
@@ -92,12 +92,13 @@ profile: arch=x86_64, compiler=gcc, compiler.libcxx=libstdc++11, compiler.versio
Dependencies (4) and binary availability
-package via ctx Release Debug RelWithDebInfo
------------------------------------------------------------------
-MyDepA/1.0 - host myremote myremote myremote
-MyDepB/1.0 - host myremote myremote myremote
-boost/1.8 MyDepA (+1) host myremote myremote myremote
-Zigma/1.0 MyDepB host myremote myremote MISSING
+package via ctx Release Debug RelWithDebInfo
+---------------------------------------------------------------------
+MyDepA/1.0 - host myremote myremote myremote
+MyDepB/1.0 - host myremote myremote myremote
+boost/1.8 MyDepA host myremote myremote myremote
+ MyDepB -> Zigma
+Zigma/1.0 MyDepB host myremote myremote MISSING
Missing binaries (1)
@@ -129,11 +130,12 @@ the same colour coding.
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.
+requires, `MyDepB -> Zigma` for something Zigma requires, and so on. A
+package required by several others gets one line per parent (boost above is
+pulled in by both MyDepA and Zigma), shortest path first. Rows are sorted
+direct-first, then by path, so a branch's transitive dependencies stay
+together; the dependency hierarchy section at the end shows the same
+information as a tree.
* a remote name (e.g. `myremote`) – the remote has a binary for the package_id your
diff --git a/conandeps.py b/conandeps.py
index 6928614..a8de228 100755
--- a/conandeps.py
+++ b/conandeps.py
@@ -89,6 +89,9 @@ class Dep:
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)
+ # One path per direct parent: shortest path to that parent plus the
+ # parent itself. Bounded by the number of parents, unlike "all paths".
+ paths: List[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)
@@ -303,9 +306,14 @@ def _collect(
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 edge.src is root:
+ parent, via = "your conanfile", []
+ else:
+ parent = edge.src.ref.name
+ via = paths.get(_node_key(edge.src), []) + [parent]
if parent not in dep.parents:
dep.parents.append(parent)
+ dep.paths.append(via)
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
@@ -508,18 +516,27 @@ def _visible_len(text: str) -> int:
def _table(headers: List[str], rows: List[List[str]], pal: _Palette) -> List[str]:
- """Render a column-aligned text table; cells may contain ANSI colour codes."""
+ """Render a column-aligned text table.
+
+ Cells may contain ANSI colour codes (alignment uses the visible width) and
+ newlines: a multi-line cell spreads the row over several lines, with the
+ other columns left blank on the continuation lines.
+ """
+ split_rows = [[c.split("\n") for c in row] for row in rows]
widths = [len(h) for h in headers]
- for row in rows:
- for i, cell in enumerate(row):
- widths[i] = max(widths[i], _visible_len(cell))
+ for row in split_rows:
+ for i, cell_lines in enumerate(row):
+ widths[i] = max([widths[i]] + [_visible_len(line) for line in cell_lines])
def fmt(cells: List[str]) -> str:
padded = [c + " " * (widths[i] - _visible_len(c)) for i, c in enumerate(cells)]
return " ".join(padded).rstrip()
lines = [pal.bold(fmt(headers)), "-" * (sum(widths) + 2 * (len(widths) - 1))]
- lines.extend(fmt(r) for r in rows)
+ for row in split_rows:
+ height = max(len(cell_lines) for cell_lines in row)
+ for n in range(height):
+ lines.append(fmt([cell[n] if n < len(cell) else "" for cell in row]))
return lines
@@ -539,17 +556,17 @@ def _override_kind(o: Override) -> str:
return "explicit (override=True)" if o.explicit else "implicit (newer requirement downstream)"
-def _via(dep: Dep, arrow: str = " -> ") -> str:
- """Shortest path to the package: '-' for direct, 'A -> B' for consumer -> A -> B -> pkg.
+def _via(dep: Dep, arrow: str = " -> ", newline: str = "\n") -> str:
+ """How the package is reached: '-' for direct, otherwise one line per path.
- ``(+N)`` marks N further packages that also require it directly; the
- dependency hierarchy section shows every path in full.
+ ``A -> B`` means consumer -> A -> B -> pkg. A package required by several
+ others gets one line per parent (shortest path to that parent), shortest
+ first.
"""
if dep.direct:
return "-"
- via = arrow.join(dep.path)
- extra = len(dep.parents) - 1
- return via + (" (+%d)" % extra if extra > 0 else "")
+ paths = sorted(dep.paths, key=lambda p: (len(p), [x.lower() for x in p]))
+ return newline.join(arrow.join(p) for p in paths)
def render_text(report: Report, color: bool = False) -> str:
@@ -668,7 +685,7 @@ def render_html(report: Report) -> str:
w("