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("

Dependencies (%d) and binary availability

" % len(report.deps)) rows = [] for dep in report.deps.values(): - row = [e(dep.ref), e(_via(dep, " \u2192 ")), e(dep.context)] + row = [e(dep.ref), e(_via(dep, " \u2192 ")).replace("\n", "
"), 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") diff --git a/tests/test_conandeps.py b/tests/test_conandeps.py index a65be48..dca9569 100644 --- a/tests/test_conandeps.py +++ b/tests/test_conandeps.py @@ -242,8 +242,10 @@ def test_sorted_by_path(report): ("libbar/1.1", ["libfoo"]), ("libdeep/1.0", ["libfoo", "libbar"]), ] - # libqux is required by both the consumer and libbaz -> two parents. + # libqux is required by both the consumer and libbaz -> two parents, but + # it is direct, so the via column just says "-". assert sorted(report.deps["libqux#host"].parents) == ["libbaz", "your conanfile"] + assert conandeps._via(report.deps["libqux#host"]) == "-" text = conandeps.render_text(report) assert re.search(r"hdr/1\.0\s+-\s+host", text) assert re.search(r"libbar/1\.1\s+libfoo\s+host", text) @@ -253,6 +255,24 @@ def test_sorted_by_path(report): assert "libdeep/1.0libfoo \u2192 libbarhost" in page +def test_via_lists_every_parent_path(): + boost = conandeps.Dep(ref="boost/1.8", context="host", depth=2, path=["MyDepA"]) + boost.parents = ["MyDepA", "Zigma"] + boost.paths = [["MyDepA"], ["MyDepB", "Zigma"]] + assert conandeps._via(boost) == "MyDepA\nMyDepB -> Zigma" + assert conandeps._via(boost, " > ", "; ") == "MyDepA; MyDepB > Zigma" + # Multi-line cells spread the row; other columns stay aligned and blank. + lines = conandeps._table( + ["package", "via", "ctx"], + [["boost/1.8", conandeps._via(boost), "host"]], + conandeps._Palette(False), + ) + assert lines[2:] == [ + "boost/1.8 MyDepA host", + " MyDepB -> Zigma", + ] + + 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"]}