Skip to content

Commit 501c928

Browse files
committed
refactor(libstore): add BGL-based dependency graph for path analysis
Introduces a reusable directed graph template built on Boost Graph Library (BGL) to provide graph operations for store path dependency analysis. This will be used by `nix why-depends` and future cycle detection.
1 parent 70176ed commit 501c928

File tree

7 files changed

+519
-0
lines changed

7 files changed

+519
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#include "nix/store/dependency-graph-impl.hh"
2+
3+
#include <gtest/gtest.h>
4+
5+
namespace nix {
6+
7+
TEST(DependencyGraph, BasicAddEdge)
8+
{
9+
FilePathGraph depGraph;
10+
depGraph.addEdge("a", "b");
11+
depGraph.addEdge("b", "c");
12+
13+
EXPECT_TRUE(depGraph.hasNode("a"));
14+
EXPECT_TRUE(depGraph.hasNode("b"));
15+
EXPECT_TRUE(depGraph.hasNode("c"));
16+
EXPECT_FALSE(depGraph.hasNode("d"));
17+
18+
// Verify edges using high-level API
19+
auto successors = depGraph.getSuccessors("a");
20+
EXPECT_EQ(successors.size(), 1);
21+
EXPECT_EQ(successors[0], "b");
22+
}
23+
24+
TEST(DependencyGraph, DfsTraversalOrder)
25+
{
26+
// Build a graph: A->B->D, A->C->D
27+
// Successors should be visited in distance order (B and C before recursing)
28+
FilePathGraph depGraph;
29+
depGraph.addEdge("a", "b");
30+
depGraph.addEdge("a", "c");
31+
depGraph.addEdge("b", "d");
32+
depGraph.addEdge("c", "d");
33+
34+
std::vector<std::string> visitedNodes;
35+
std::vector<std::pair<std::string, std::string>> visitedEdges;
36+
37+
depGraph.dfsFromTarget(
38+
"a",
39+
"d",
40+
[&](const std::string & node, size_t depth) {
41+
visitedNodes.push_back(node);
42+
return true;
43+
},
44+
[&](const std::string & from, const std::string & to, bool isLast, size_t depth) {
45+
visitedEdges.emplace_back(from, to);
46+
},
47+
[](const std::string &) { return false; });
48+
49+
EXPECT_EQ(visitedNodes[0], "a");
50+
// B and C both at distance 1, could be in either order
51+
EXPECT_TRUE(
52+
(visitedNodes[1] == "b" && visitedNodes[2] == "d") || (visitedNodes[1] == "c" && visitedNodes[2] == "d"));
53+
}
54+
55+
TEST(DependencyGraph, GetSuccessors)
56+
{
57+
FilePathGraph depGraph;
58+
depGraph.addEdge("a", "b");
59+
depGraph.addEdge("a", "c");
60+
61+
auto successors = depGraph.getSuccessors("a");
62+
EXPECT_EQ(successors.size(), 2);
63+
EXPECT_TRUE(std::ranges::contains(successors, "b"));
64+
EXPECT_TRUE(std::ranges::contains(successors, "c"));
65+
}
66+
67+
TEST(DependencyGraph, GetAllNodes)
68+
{
69+
FilePathGraph depGraph;
70+
depGraph.addEdge("foo", "bar");
71+
depGraph.addEdge("bar", "baz");
72+
73+
auto nodes = depGraph.getAllNodes();
74+
EXPECT_EQ(nodes.size(), 3);
75+
EXPECT_TRUE(std::ranges::contains(nodes, "foo"));
76+
EXPECT_TRUE(std::ranges::contains(nodes, "bar"));
77+
EXPECT_TRUE(std::ranges::contains(nodes, "baz"));
78+
}
79+
80+
TEST(DependencyGraph, ThrowsOnMissingNode)
81+
{
82+
FilePathGraph depGraph;
83+
depGraph.addEdge("a", "b");
84+
85+
EXPECT_THROW((void) depGraph.getSuccessors("nonexistent"), nix::Error);
86+
}
87+
88+
TEST(DependencyGraph, EmptyGraph)
89+
{
90+
FilePathGraph depGraph;
91+
92+
EXPECT_FALSE(depGraph.hasNode("anything"));
93+
EXPECT_EQ(depGraph.numVertices(), 0);
94+
EXPECT_EQ(depGraph.getAllNodes().size(), 0);
95+
}
96+
97+
} // namespace nix

src/libstore-tests/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ subdir('nix-meson-build-support/common')
5656
sources = files(
5757
'common-protocol.cc',
5858
'content-address.cc',
59+
'dependency-graph.cc',
5960
'derivation-advanced-attrs.cc',
6061
'derivation.cc',
6162
'derived-path.cc',

src/libstore/dependency-graph.cc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#include "nix/store/dependency-graph.hh"
2+
#include "nix/store/dependency-graph-impl.hh"
3+
4+
#include <string>
5+
6+
namespace nix {
7+
8+
// Explicit instantiations for common types
9+
template class DependencyGraph<StorePath>;
10+
template class DependencyGraph<std::string>;
11+
template class DependencyGraph<StorePath, FileListEdgeProperty>;
12+
13+
} // namespace nix
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#pragma once
2+
/**
3+
* @file
4+
*
5+
* Template implementations (as opposed to mere declarations).
6+
*
7+
* This file is an example of the "impl.hh" pattern. See the
8+
* contributing guide.
9+
*
10+
* One only needs to include this when instantiating DependencyGraph
11+
* with custom NodeId or EdgeProperty types beyond the pre-instantiated
12+
* common types (StorePath, std::string).
13+
*/
14+
15+
#include "nix/store/dependency-graph.hh"
16+
#include "nix/store/store-api.hh"
17+
#include "nix/util/error.hh"
18+
19+
#include <boost/graph/graph_traits.hpp>
20+
#include <boost/graph/reverse_graph.hpp>
21+
#include <boost/graph/properties.hpp>
22+
23+
#include <algorithm>
24+
#include <ranges>
25+
26+
namespace nix {
27+
28+
template<GraphNodeId NodeId, typename EdgeProperty>
29+
DependencyGraph<NodeId, EdgeProperty>::DependencyGraph(Store & store, const StorePathSet & closure)
30+
requires std::same_as<NodeId, StorePath>
31+
{
32+
for (auto & path : closure) {
33+
for (auto & ref : store.queryPathInfo(path)->references) {
34+
addEdge(path, ref);
35+
}
36+
}
37+
}
38+
39+
template<GraphNodeId NodeId, typename EdgeProperty>
40+
typename DependencyGraph<NodeId, EdgeProperty>::vertex_descriptor
41+
DependencyGraph<NodeId, EdgeProperty>::addOrGetVertex(const NodeId & id)
42+
{
43+
auto it = nodeToVertex.find(id);
44+
if (it != nodeToVertex.end()) {
45+
return it->second;
46+
}
47+
48+
auto v = boost::add_vertex(VertexProperty{std::make_optional(id)}, graph);
49+
nodeToVertex[id] = v;
50+
return v;
51+
}
52+
53+
template<GraphNodeId NodeId, typename EdgeProperty>
54+
void DependencyGraph<NodeId, EdgeProperty>::addEdge(const NodeId & from, const NodeId & to)
55+
{
56+
auto vFrom = addOrGetVertex(from);
57+
auto vTo = addOrGetVertex(to);
58+
59+
// Check for existing edge to prevent duplicates (idempotent)
60+
auto [existingEdge, found] = boost::edge(vFrom, vTo, graph);
61+
if (!found) {
62+
boost::add_edge(vFrom, vTo, graph);
63+
}
64+
// If edge exists, this is a no-op (idempotent)
65+
}
66+
67+
template<GraphNodeId NodeId, typename EdgeProperty>
68+
void DependencyGraph<NodeId, EdgeProperty>::addEdge(const NodeId & from, const NodeId & to, const EdgeProperty & prop)
69+
requires(!std::same_as<EdgeProperty, boost::no_property>)
70+
{
71+
auto vFrom = addOrGetVertex(from);
72+
auto vTo = addOrGetVertex(to);
73+
74+
auto [existingEdge, found] = boost::edge(vFrom, vTo, graph);
75+
if (found) {
76+
// Merge properties for existing edge
77+
if constexpr (std::same_as<EdgeProperty, FileListEdgeProperty>) {
78+
// Set handles deduplication automatically
79+
auto & edgeFiles = graph[existingEdge].files;
80+
edgeFiles.insert(prop.files.begin(), prop.files.end());
81+
} else {
82+
// For other property types, overwrite with new value
83+
graph[existingEdge] = prop;
84+
}
85+
} else {
86+
// New edge
87+
boost::add_edge(vFrom, vTo, prop, graph);
88+
}
89+
}
90+
91+
template<GraphNodeId NodeId, typename EdgeProperty>
92+
std::optional<typename DependencyGraph<NodeId, EdgeProperty>::vertex_descriptor>
93+
DependencyGraph<NodeId, EdgeProperty>::getVertex(const NodeId & id) const
94+
{
95+
auto it = nodeToVertex.find(id);
96+
if (it == nodeToVertex.end()) {
97+
return std::nullopt;
98+
}
99+
return it->second;
100+
}
101+
102+
template<GraphNodeId NodeId, typename EdgeProperty>
103+
const NodeId & DependencyGraph<NodeId, EdgeProperty>::getNodeId(vertex_descriptor v) const
104+
{
105+
return *graph[v].id;
106+
}
107+
108+
template<GraphNodeId NodeId, typename EdgeProperty>
109+
bool DependencyGraph<NodeId, EdgeProperty>::hasNode(const NodeId & id) const
110+
{
111+
return nodeToVertex.contains(id);
112+
}
113+
114+
template<GraphNodeId NodeId, typename EdgeProperty>
115+
typename DependencyGraph<NodeId, EdgeProperty>::vertex_descriptor
116+
DependencyGraph<NodeId, EdgeProperty>::getVertexOrThrow(const NodeId & id) const
117+
{
118+
auto opt = getVertex(id);
119+
if (!opt.has_value()) {
120+
// Note: NodeId is not included as it may not be formattable in all instantiations
121+
throw Error("node not found in graph");
122+
}
123+
return *opt;
124+
}
125+
126+
template<GraphNodeId NodeId, typename EdgeProperty>
127+
template<typename NodeVisitor, typename EdgeVisitor, typename StopPredicate>
128+
void DependencyGraph<NodeId, EdgeProperty>::dfsFromTarget(
129+
const NodeId & start,
130+
const NodeId & target,
131+
NodeVisitor && visitNode,
132+
EdgeVisitor && visitEdge,
133+
StopPredicate && shouldStop) const
134+
{
135+
// Compute distances locally for this traversal
136+
auto targetVertex = getVertexOrThrow(target);
137+
size_t n = boost::num_vertices(graph);
138+
139+
std::vector<size_t> distances(n, std::numeric_limits<size_t>::max());
140+
distances[targetVertex] = 0;
141+
142+
// Use reverse_graph to follow incoming edges
143+
auto reversedGraph = boost::make_reverse_graph(graph);
144+
145+
// Create uniform weight map (all edges have weight 1)
146+
auto weightMap =
147+
boost::make_constant_property<typename boost::graph_traits<decltype(reversedGraph)>::edge_descriptor>(1);
148+
149+
// Run Dijkstra on reversed graph with uniform weights
150+
boost::dijkstra_shortest_paths(
151+
reversedGraph,
152+
targetVertex,
153+
boost::weight_map(weightMap).distance_map(
154+
boost::make_iterator_property_map(distances.begin(), boost::get(boost::vertex_index, reversedGraph))));
155+
156+
// DFS with distance-based ordering
157+
std::function<bool(const NodeId &, size_t)> dfs = [&](const NodeId & node, size_t depth) -> bool {
158+
// Visit node - if returns false, skip this subtree
159+
if (!visitNode(node, depth)) {
160+
return false;
161+
}
162+
163+
// Check if we should stop the entire traversal
164+
if (shouldStop(node)) {
165+
return true; // Signal to stop
166+
}
167+
168+
// Get and sort successors by distance
169+
auto successors = getSuccessors(node);
170+
auto sortedSuccessors = successors | std::views::transform([&](const auto & ref) -> std::pair<size_t, NodeId> {
171+
auto v = getVertexOrThrow(ref);
172+
return {distances[v], ref}; // Use local distances
173+
})
174+
| std::views::filter([](const auto & p) {
175+
// Filter unreachable nodes
176+
return p.first != std::numeric_limits<size_t>::max();
177+
})
178+
| std::ranges::to<std::vector>();
179+
180+
std::ranges::sort(sortedSuccessors);
181+
182+
// Visit each edge and recurse
183+
for (size_t i = 0; i < sortedSuccessors.size(); ++i) {
184+
const auto & [dist, successor] = sortedSuccessors[i];
185+
bool isLast = (i == sortedSuccessors.size() - 1);
186+
187+
visitEdge(node, successor, isLast, depth);
188+
189+
if (dfs(successor, depth + 1)) {
190+
return true; // Propagate stop signal
191+
}
192+
}
193+
194+
return false; // Continue traversal
195+
};
196+
197+
dfs(start, 0);
198+
}
199+
200+
template<GraphNodeId NodeId, typename EdgeProperty>
201+
std::vector<NodeId> DependencyGraph<NodeId, EdgeProperty>::getSuccessors(const NodeId & node) const
202+
{
203+
auto v = getVertexOrThrow(node);
204+
auto [adjBegin, adjEnd] = boost::adjacent_vertices(v, graph);
205+
206+
return std::ranges::subrange(adjBegin, adjEnd) | std::views::transform([&](auto v) { return getNodeId(v); })
207+
| std::ranges::to<std::vector>();
208+
}
209+
210+
template<GraphNodeId NodeId, typename EdgeProperty>
211+
std::optional<EdgeProperty>
212+
DependencyGraph<NodeId, EdgeProperty>::getEdgeProperty(const NodeId & from, const NodeId & to) const
213+
requires(!std::same_as<EdgeProperty, boost::no_property>)
214+
{
215+
auto vFrom = getVertexOrThrow(from);
216+
auto vTo = getVertexOrThrow(to);
217+
218+
auto [edge, found] = boost::edge(vFrom, vTo, graph);
219+
if (!found) {
220+
return std::nullopt;
221+
}
222+
223+
return graph[edge];
224+
}
225+
226+
template<GraphNodeId NodeId, typename EdgeProperty>
227+
std::vector<NodeId> DependencyGraph<NodeId, EdgeProperty>::getAllNodes() const
228+
{
229+
return nodeToVertex | std::views::keys | std::ranges::to<std::vector>();
230+
}
231+
232+
} // namespace nix

0 commit comments

Comments
 (0)