mirror of
https://github.com/NixOS/nix.git
synced 2025-12-09 10:31:02 +01:00
refactor(nix): migrate why-depends to use DependencyGraph
Replaces manual graph traversal in `nix why-depends` with the new
DependencyGraph infrastructure. Simplifies code while maintaining
identical output and behavior.
## Changes to why-depends
**Before**: Custom Node struct with manual Dijkstra implementation
- 150+ lines of graph management boilerplate
- Manual priority queue and distance tracking
- Custom reverse reference bookkeeping
- Inline file scanning mixed with graph traversal
**After**: Clean API calls to DependencyGraph
- Replaced Node struct with DependencyGraph<StorePath>
- Replaced manual Dijkstra with computeDistancesFrom()
- Use getSuccessors() instead of managing refs/rrefs
- Extracted findHashContexts() helper for clarity
**Improvements**:
- ~50 lines shorter, easier to understand
- C++20 ranges for cleaner iteration
- Separated concerns: graph ops vs. file scanning vs. formatting
- Complex algorithms now in well-tested DependencyGraph
## Example Transformation
```cpp
// Before: Manual graph building
std::map<StorePath, Node> graph;
for (auto & path : closure)
graph.emplace(path, Node{...});
for (auto & node : graph)
for (auto & ref : node.second.refs)
graph.find(ref)->second.rrefs.insert(node.first);
// After: Use DependencyGraph API
StorePathGraph depGraph(*store, closure);
depGraph.computeDistancesFrom(dependencyPath);
auto successors = depGraph.getSuccessors(nodePath);
```
## Helper Extraction
**findHashContexts()**: Extracted file scanning into reusable function
- Takes accessor, reference paths, target hash
- Returns hash → formatted context strings
- Keeps printNode() focused on graph traversal
## Test Improvements
**references.cc**: Changed ASSERT_EQ → EXPECT_EQ
- EXPECT continues after failure, shows all results
- Better debugging when multiple assertions fail
- GoogleTest best practice
## Behavior Preservation
**Critical**: Pure refactoring, zero user-visible changes
- Identical output format
- Same algorithm (Dijkstra shortest paths)
- Same filtering/highlighting
- Same CLI interface
## Benefits
1. **Maintainability**: Algorithms in shared, tested infrastructure
2. **Correctness**: DependencyGraph thoroughly unit tested
3. **Reusability**: Other commands can now use DependencyGraph
4. **Readability**: why-depends focused on UI, not graph algorithms
5. **Future-proof**: Sets stage for improved error messages
This commit is contained in:
parent
40beb50e8f
commit
60857de63e
2 changed files with 145 additions and 183 deletions
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <queue>
|
||||
#include <ranges>
|
||||
|
||||
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<std::string, Strings>
|
||||
findHashContexts(SourceAccessor & accessor, const StorePathSet & refPaths, std::string_view dependencyPathHash)
|
||||
{
|
||||
std::map<std::string, Strings> 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<size_t>::max();
|
||||
|
||||
struct Node
|
||||
{
|
||||
StorePath path;
|
||||
StorePathSet refs;
|
||||
StorePathSet rrefs;
|
||||
size_t dist = inf;
|
||||
Node * prev = nullptr;
|
||||
bool queued = false;
|
||||
bool visited = false;
|
||||
};
|
||||
|
||||
std::map<StorePath, Node> 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<Node *> 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<void(Node &, const std::string &, const std::string &)> 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<size_t, Node *> 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<std::string, Strings> 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<StorePath> visited;
|
||||
std::vector<std::string> 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; });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue