diff --git a/src/libstore-tests/references.cc b/src/libstore-tests/references.cc index 9cecd573e..09184f61a 100644 --- a/src/libstore-tests/references.cc +++ b/src/libstore-tests/references.cc @@ -32,7 +32,7 @@ TEST_P(RewriteTest, IdentityRewriteIsIdentity) auto rewriter = RewritingSink(param.rewrites, rewritten); rewriter(param.originalString); rewriter.flush(); - ASSERT_EQ(rewritten.s, param.finalString); + EXPECT_EQ(rewritten.s, param.finalString); } INSTANTIATE_TEST_CASE_P( @@ -52,14 +52,14 @@ TEST(references, scan) RefScanSink scanner(StringSet{hash1}); auto s = "foobar"; scanner(s); - ASSERT_EQ(scanner.getResult(), StringSet{}); + EXPECT_EQ(scanner.getResult(), StringSet{}); } { RefScanSink scanner(StringSet{hash1}); auto s = "foobar" + hash1 + "xyzzy"; scanner(s); - ASSERT_EQ(scanner.getResult(), StringSet{hash1}); + EXPECT_EQ(scanner.getResult(), StringSet{hash1}); } { @@ -69,7 +69,7 @@ TEST(references, scan) scanner(((std::string_view) s).substr(10, 5)); scanner(((std::string_view) s).substr(15, 5)); scanner(((std::string_view) s).substr(20)); - ASSERT_EQ(scanner.getResult(), StringSet({hash1, hash2})); + EXPECT_EQ(scanner.getResult(), StringSet({hash1, hash2})); } { @@ -77,7 +77,7 @@ TEST(references, scan) auto s = "foobar" + hash1 + "xyzzy" + hash2; for (auto & i : s) scanner(std::string(1, i)); - ASSERT_EQ(scanner.getResult(), StringSet({hash1, hash2})); + EXPECT_EQ(scanner.getResult(), StringSet({hash1, hash2})); } } @@ -161,7 +161,7 @@ TEST(references, scanForReferencesDeep) { CanonPath f1Path("/file1.txt"); auto it = foundRefs.find(f1Path); - ASSERT_TRUE(it != foundRefs.end()); + EXPECT_TRUE(it != foundRefs.end()); EXPECT_EQ(it->second.size(), 1); EXPECT_TRUE(it->second.count(path1)); } @@ -170,7 +170,7 @@ TEST(references, scanForReferencesDeep) { CanonPath f2Path("/file2.txt"); auto it = foundRefs.find(f2Path); - ASSERT_TRUE(it != foundRefs.end()); + EXPECT_TRUE(it != foundRefs.end()); EXPECT_EQ(it->second.size(), 2); EXPECT_TRUE(it->second.count(path2)); EXPECT_TRUE(it->second.count(path3)); @@ -186,7 +186,7 @@ TEST(references, scanForReferencesDeep) { CanonPath f4Path("/subdir/file4.txt"); auto it = foundRefs.find(f4Path); - ASSERT_TRUE(it != foundRefs.end()); + EXPECT_TRUE(it != foundRefs.end()); EXPECT_EQ(it->second.size(), 1); EXPECT_TRUE(it->second.count(path1)); } @@ -195,7 +195,7 @@ TEST(references, scanForReferencesDeep) { CanonPath linkPath("/link1"); auto it = foundRefs.find(linkPath); - ASSERT_TRUE(it != foundRefs.end()); + EXPECT_TRUE(it != foundRefs.end()); EXPECT_EQ(it->second.size(), 1); EXPECT_TRUE(it->second.count(path2)); } diff --git a/src/nix/why-depends.cc b/src/nix/why-depends.cc index 29da9e953..465aae2ef 100644 --- a/src/nix/why-depends.cc +++ b/src/nix/why-depends.cc @@ -1,10 +1,11 @@ #include "nix/cmd/command.hh" #include "nix/store/store-api.hh" #include "nix/store/path-references.hh" +#include "nix/store/dependency-graph-impl.hh" #include "nix/util/source-accessor.hh" #include "nix/main/shared.hh" -#include +#include using namespace nix; @@ -21,6 +22,63 @@ static std::string filterPrintable(const std::string & s) return res; } +/** + * Find and format hash references in scanned files with context. + * + * @param accessor Source accessor for the store path + * @param refPaths Store paths to search for + * @param dependencyPathHash Hash of the dependency (for coloring) + * @return Map from hash string to list of formatted hit strings showing context + */ +static std::map +findHashContexts(SourceAccessor & accessor, const StorePathSet & refPaths, std::string_view dependencyPathHash) +{ + std::map hits; + + auto getColour = [&](const std::string & hash) { return hash == dependencyPathHash ? ANSI_GREEN : ANSI_BLUE; }; + + scanForReferencesDeep(accessor, CanonPath::root, refPaths, [&](const FileRefScanResult & result) { + std::string p2 = + result.filePath.isRoot() ? std::string(result.filePath.abs()) : std::string(result.filePath.rel()); + auto st = accessor.lstat(result.filePath); + + if (st.type == SourceAccessor::Type::tRegular) { + auto contents = accessor.readFile(result.filePath); + + // For each reference found in this file, extract context + for (const auto & foundRef : result.foundRefs) { + std::string hash(foundRef.hashPart()); + auto pos = contents.find(hash); + if (pos != std::string::npos) { + size_t margin = 32; + auto pos2 = pos >= margin ? pos - margin : 0; + hits[hash].emplace_back( + fmt("%s: …%s…", + p2, + hilite( + filterPrintable(std::string(contents, pos2, pos - pos2 + hash.size() + margin)), + pos - pos2, + StorePath::HashLen, + getColour(hash)))); + } + } + } else if (st.type == SourceAccessor::Type::tSymlink) { + auto target = accessor.readLink(result.filePath); + + // For each reference found in this symlink, show it + for (const auto & foundRef : result.foundRefs) { + std::string hash(foundRef.hashPart()); + auto pos = target.find(hash); + if (pos != std::string::npos) + hits[hash].emplace_back( + fmt("%s -> %s", p2, hilite(target, pos, StorePath::HashLen, getColour(hash)))); + } + } + }); + + return hits; +} + struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions { std::string _package, _dependency; @@ -109,187 +167,91 @@ struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions auto dependencyPath = *optDependencyPath; auto dependencyPathHash = dependencyPath.hashPart(); - auto const inf = std::numeric_limits::max(); - - struct Node - { - StorePath path; - StorePathSet refs; - StorePathSet rrefs; - size_t dist = inf; - Node * prev = nullptr; - bool queued = false; - bool visited = false; - }; - - std::map graph; - - for (auto & path : closure) - graph.emplace( - path, - Node{ - .path = path, - .refs = store->queryPathInfo(path)->references, - .dist = path == dependencyPath ? 0 : inf}); - - // Transpose the graph. - for (auto & node : graph) - for (auto & ref : node.second.refs) - graph.find(ref)->second.rrefs.insert(node.first); - - /* Run Dijkstra's shortest path algorithm to get the distance - of every path in the closure to 'dependency'. */ - std::priority_queue queue; - - queue.push(&graph.at(dependencyPath)); - - while (!queue.empty()) { - auto & node = *queue.top(); - queue.pop(); - - for (auto & rref : node.rrefs) { - auto & node2 = graph.at(rref); - auto dist = node.dist + 1; - if (dist < node2.dist) { - node2.dist = dist; - node2.prev = &node; - if (!node2.queued) { - node2.queued = true; - queue.push(&node2); - } - } - } - } + // Build dependency graph from closure using store metadata + StorePathGraph depGraph(*store, closure); /* Print the subgraph of nodes that have 'dependency' in their closure (i.e., that have a non-infinite distance to 'dependency'). Print every edge on a path between `package` and `dependency`. */ - std::function printNode; - - struct BailOut - {}; - - printNode = [&](Node & node, const std::string & firstPad, const std::string & tailPad) { - assert(node.dist != inf); - if (precise) { - logger->cout( - "%s%s%s%s" ANSI_NORMAL, - firstPad, - node.visited ? "\e[38;5;244m" : "", - firstPad != "" ? "→ " : "", - store->printStorePath(node.path)); - } - - if (node.path == dependencyPath && !all && packagePath != dependencyPath) - throw BailOut(); - - if (node.visited) - return; - if (precise) - node.visited = true; - - /* Sort the references by distance to `dependency` to - ensure that the shortest path is printed first. */ - std::multimap refs; - StorePathSet refPaths; - - for (auto & ref : node.refs) { - if (ref == node.path && packagePath != dependencyPath) - continue; - auto & node2 = graph.at(ref); - if (node2.dist == inf) - continue; - refs.emplace(node2.dist, &node2); - refPaths.insert(node2.path); - } - - /* For each reference, find the files and symlinks that - contain the reference. */ - std::map hits; - - auto accessor = store->requireStoreObjectAccessor(node.path); - - auto getColour = [&](const std::string & hash) { - return hash == dependencyPathHash ? ANSI_GREEN : ANSI_BLUE; - }; - - if (precise) { - // Use scanForReferencesDeep to find files containing references - scanForReferencesDeep(*accessor, CanonPath::root, refPaths, [&](FileRefScanResult result) { - auto p2 = result.filePath.isRoot() ? result.filePath.abs() : result.filePath.rel(); - auto st = accessor->lstat(result.filePath); - - if (st.type == SourceAccessor::Type::tRegular) { - auto contents = accessor->readFile(result.filePath); - - // For each reference found in this file, extract context - for (auto & foundRef : result.foundRefs) { - std::string hash(foundRef.hashPart()); - auto pos = contents.find(hash); - if (pos != std::string::npos) { - size_t margin = 32; - auto pos2 = pos >= margin ? pos - margin : 0; - hits[hash].emplace_back(fmt( - "%s: …%s…", - p2, - hilite( - filterPrintable(std::string(contents, pos2, pos - pos2 + hash.size() + margin)), - pos - pos2, - StorePath::HashLen, - getColour(hash)))); - } - } - } else if (st.type == SourceAccessor::Type::tSymlink) { - auto target = accessor->readLink(result.filePath); - - // For each reference found in this symlink, show it - for (auto & foundRef : result.foundRefs) { - std::string hash(foundRef.hashPart()); - auto pos = target.find(hash); - if (pos != std::string::npos) - hits[hash].emplace_back( - fmt("%s -> %s", p2, hilite(target, pos, StorePath::HashLen, getColour(hash)))); - } - } - }); - } - - for (auto & ref : refs) { - std::string hash(ref.second->path.hashPart()); - - bool last = all ? ref == *refs.rbegin() : true; - - for (auto & hit : hits[hash]) { - bool first = hit == *hits[hash].begin(); - logger->cout( - "%s%s%s", tailPad, (first ? (last ? treeLast : treeConn) : (last ? treeNull : treeLine)), hit); - if (!all) - break; - } - - if (!precise) { - logger->cout( - "%s%s%s%s" ANSI_NORMAL, - firstPad, - ref.second->visited ? "\e[38;5;244m" : "", - last ? treeLast : treeConn, - store->printStorePath(ref.second->path)); - node.visited = true; - } - - printNode(*ref.second, tailPad + (last ? treeNull : treeLine), tailPad + (last ? treeNull : treeLine)); - } - }; RunPager pager; - try { - if (!precise) { - logger->cout("%s", store->printStorePath(graph.at(packagePath).path)); - } - printNode(graph.at(packagePath), "", ""); - } catch (BailOut &) { + + if (!precise) { + logger->cout("%s", store->printStorePath(packagePath)); } + + std::set visited; + std::vector padStack = {""}; + + depGraph.dfsFromTarget( + packagePath, + dependencyPath, + // Visit node callback + [&](const StorePath & node, size_t depth) -> bool { + std::string currentPad = padStack[depth]; + + if (precise) { + logger->cout( + "%s%s%s%s" ANSI_NORMAL, + currentPad, + visited.contains(node) ? "\e[38;5;244m" : "", + currentPad != "" ? "→ " : "", + store->printStorePath(node)); + } + + if (visited.contains(node)) { + return false; // Skip subtree + } + if (precise) { + visited.insert(node); + } + + return true; // Continue with this node's children + }, + // Visit edge callback + [&](const StorePath & from, const StorePath & to, bool isLast, size_t depth) { + std::string tailPad = padStack[depth]; + + // In non-all mode, we only traverse one path, so everything is "last" + bool effectivelyLast = !all || isLast; + + if (precise) { + auto accessor = store->requireStoreObjectAccessor(from); + auto hits = findHashContexts(*accessor, {to}, dependencyPathHash); + std::string hash(to.hashPart()); + auto & hashHits = hits[hash]; + + for (auto & hit : hashHits) { + bool first = hit == *hashHits.begin(); + logger->cout( + "%s%s%s", + tailPad, + (first ? (effectivelyLast ? treeLast : treeConn) : (effectivelyLast ? treeNull : treeLine)), + hit); + if (!all) + break; + } + } else { + std::string currentPad = padStack[depth]; + logger->cout( + "%s%s%s%s" ANSI_NORMAL, + currentPad, + visited.contains(to) ? "\e[38;5;244m" : "", + effectivelyLast ? treeLast : treeConn, + store->printStorePath(to)); + visited.insert(from); + } + + // Update padding for next level + if (padStack.size() == depth + 1) { + padStack.push_back(tailPad + (effectivelyLast ? treeNull : treeLine)); + } else { + padStack[depth + 1] = tailPad + (effectivelyLast ? treeNull : treeLine); + } + }, + // Stop condition + [&](const StorePath & node) { return node == dependencyPath && !all && packagePath != dependencyPath; }); } };