Skip to content

Commit d043aaf

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 dd0d006 commit d043aaf

File tree

7 files changed

+503
-0
lines changed

7 files changed

+503
-0
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#include "nix/store/dependency-graph.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, ComputeDistancesSimplePath)
25+
{
26+
// Build a simple chain: A->B->C->D
27+
FilePathGraph depGraph;
28+
depGraph.addEdge("a", "b");
29+
depGraph.addEdge("b", "c");
30+
depGraph.addEdge("c", "d");
31+
32+
// Compute distances from D
33+
depGraph.computeDistancesFrom("d");
34+
35+
// Check distances
36+
EXPECT_EQ(depGraph.getDistance("d"), 0);
37+
EXPECT_EQ(depGraph.getDistance("c"), 1);
38+
EXPECT_EQ(depGraph.getDistance("b"), 2);
39+
EXPECT_EQ(depGraph.getDistance("a"), 3);
40+
41+
// Check predecessors form the path
42+
EXPECT_EQ(depGraph.getPredecessor("a"), std::make_optional<std::string>("b"));
43+
EXPECT_EQ(depGraph.getPredecessor("b"), std::make_optional<std::string>("c"));
44+
EXPECT_EQ(depGraph.getPredecessor("c"), std::make_optional<std::string>("d"));
45+
EXPECT_EQ(depGraph.getPredecessor("d"), std::nullopt); // Target has no predecessor
46+
}
47+
48+
TEST(DependencyGraph, ComputeDistancesWithBranches)
49+
{
50+
// Build a graph with multiple paths:
51+
// A->B->D
52+
// A->C->D
53+
// Shortest path from A to D should be 2 (via either B or C)
54+
FilePathGraph depGraph;
55+
depGraph.addEdge("a", "b");
56+
depGraph.addEdge("a", "c");
57+
depGraph.addEdge("b", "d");
58+
depGraph.addEdge("c", "d");
59+
60+
depGraph.computeDistancesFrom("d");
61+
62+
EXPECT_EQ(depGraph.getDistance("d"), 0);
63+
EXPECT_EQ(depGraph.getDistance("b"), 1);
64+
EXPECT_EQ(depGraph.getDistance("c"), 1);
65+
EXPECT_EQ(depGraph.getDistance("a"), 2);
66+
67+
// Predecessor of A should be either B or C (both are valid shortest paths)
68+
auto prevA = depGraph.getPredecessor("a");
69+
EXPECT_TRUE(prevA.has_value());
70+
EXPECT_TRUE(prevA.value() == "b" || prevA.value() == "c");
71+
}
72+
73+
TEST(DependencyGraph, ComputeDistancesUnreachable)
74+
{
75+
// Build a disconnected graph: A->B, C->D
76+
FilePathGraph depGraph;
77+
depGraph.addEdge("a", "b");
78+
depGraph.addEdge("c", "d");
79+
80+
depGraph.computeDistancesFrom("d");
81+
82+
EXPECT_EQ(depGraph.getDistance("d"), 0);
83+
EXPECT_EQ(depGraph.getDistance("c"), 1);
84+
// A and B are unreachable from D
85+
EXPECT_EQ(depGraph.getDistance("a"), std::numeric_limits<size_t>::max());
86+
EXPECT_EQ(depGraph.getDistance("b"), std::numeric_limits<size_t>::max());
87+
}
88+
89+
TEST(DependencyGraph, GetSuccessors)
90+
{
91+
FilePathGraph depGraph;
92+
depGraph.addEdge("a", "b");
93+
depGraph.addEdge("a", "c");
94+
95+
auto successors = depGraph.getSuccessors("a");
96+
EXPECT_EQ(successors.size(), 2);
97+
EXPECT_TRUE(std::ranges::contains(successors, "b"));
98+
EXPECT_TRUE(std::ranges::contains(successors, "c"));
99+
}
100+
101+
TEST(DependencyGraph, GetAllNodes)
102+
{
103+
FilePathGraph depGraph;
104+
depGraph.addEdge("foo", "bar");
105+
depGraph.addEdge("bar", "baz");
106+
107+
auto nodes = depGraph.getAllNodes();
108+
EXPECT_EQ(nodes.size(), 3);
109+
EXPECT_TRUE(std::ranges::contains(nodes, "foo"));
110+
EXPECT_TRUE(std::ranges::contains(nodes, "bar"));
111+
EXPECT_TRUE(std::ranges::contains(nodes, "baz"));
112+
}
113+
114+
TEST(DependencyGraph, ThrowsOnMissingNode)
115+
{
116+
FilePathGraph depGraph;
117+
depGraph.addEdge("a", "b");
118+
119+
EXPECT_THROW(depGraph.getSuccessors("nonexistent"), nix::Error);
120+
EXPECT_THROW(depGraph.computeDistancesFrom("nonexistent"), nix::Error);
121+
}
122+
123+
TEST(DependencyGraph, ThrowsWhenDistanceNotComputed)
124+
{
125+
FilePathGraph depGraph;
126+
depGraph.addEdge("a", "b");
127+
128+
EXPECT_THROW(depGraph.getDistance("a"), nix::Error);
129+
EXPECT_THROW(depGraph.getPredecessor("a"), nix::Error);
130+
}
131+
132+
TEST(DependencyGraph, EmptyGraph)
133+
{
134+
FilePathGraph depGraph;
135+
136+
EXPECT_FALSE(depGraph.hasNode("anything"));
137+
EXPECT_EQ(depGraph.numVertices(), 0);
138+
EXPECT_EQ(depGraph.getAllNodes().size(), 0);
139+
}
140+
141+
} // 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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#include "nix/store/dependency-graph-impl.hh"
2+
3+
namespace nix {
4+
5+
// Explicit instantiations for common types
6+
template class DependencyGraph<StorePath>;
7+
template class DependencyGraph<std::string>;
8+
template class DependencyGraph<StorePath, FileListEdgeProperty>;
9+
10+
} // namespace nix
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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{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+
boost::add_edge(vFrom, vTo, graph);
59+
}
60+
61+
template<GraphNodeId NodeId, typename EdgeProperty>
62+
void DependencyGraph<NodeId, EdgeProperty>::addEdge(const NodeId & from, const NodeId & to, const EdgeProperty & prop)
63+
requires(!std::same_as<EdgeProperty, boost::no_property>)
64+
{
65+
auto vFrom = addOrGetVertex(from);
66+
auto vTo = addOrGetVertex(to);
67+
68+
auto [existingEdge, found] = boost::edge(vFrom, vTo, graph);
69+
if (found) {
70+
if constexpr (std::same_as<EdgeProperty, FileListEdgeProperty>) {
71+
auto & edgeFiles = graph[existingEdge].files;
72+
edgeFiles.insert(edgeFiles.end(), prop.files.begin(), prop.files.end());
73+
}
74+
} else {
75+
boost::add_edge(vFrom, vTo, prop, graph);
76+
}
77+
}
78+
79+
template<GraphNodeId NodeId, typename EdgeProperty>
80+
std::optional<typename DependencyGraph<NodeId, EdgeProperty>::vertex_descriptor>
81+
DependencyGraph<NodeId, EdgeProperty>::getVertex(const NodeId & id) const
82+
{
83+
auto it = nodeToVertex.find(id);
84+
if (it == nodeToVertex.end()) {
85+
return std::nullopt;
86+
}
87+
return it->second;
88+
}
89+
90+
template<GraphNodeId NodeId, typename EdgeProperty>
91+
const NodeId & DependencyGraph<NodeId, EdgeProperty>::getNodeId(vertex_descriptor v) const
92+
{
93+
return graph[v].id;
94+
}
95+
96+
template<GraphNodeId NodeId, typename EdgeProperty>
97+
bool DependencyGraph<NodeId, EdgeProperty>::hasNode(const NodeId & id) const
98+
{
99+
return nodeToVertex.contains(id);
100+
}
101+
102+
template<GraphNodeId NodeId, typename EdgeProperty>
103+
typename DependencyGraph<NodeId, EdgeProperty>::vertex_descriptor
104+
DependencyGraph<NodeId, EdgeProperty>::getVertexOrThrow(const NodeId & id) const
105+
{
106+
auto opt = getVertex(id);
107+
if (!opt.has_value()) {
108+
throw Error("node not found in graph");
109+
}
110+
return *opt;
111+
}
112+
113+
template<GraphNodeId NodeId, typename EdgeProperty>
114+
void DependencyGraph<NodeId, EdgeProperty>::computeDistancesFrom(const NodeId & target)
115+
{
116+
auto targetVertex = getVertexOrThrow(target);
117+
size_t n = boost::num_vertices(graph);
118+
119+
// Setup property maps for distances and predecessors
120+
std::vector<size_t> distances(n, std::numeric_limits<size_t>::max());
121+
std::vector<vertex_descriptor> predecessors(n);
122+
123+
// Initialize source
124+
distances[targetVertex] = 0;
125+
predecessors[targetVertex] = targetVertex;
126+
127+
// Use reverse_graph to follow incoming edges (predecessors)
128+
auto reversedGraph = boost::make_reverse_graph(graph);
129+
130+
// Create uniform weight map (all edges have weight 1)
131+
auto weightMap =
132+
boost::make_constant_property<typename boost::graph_traits<decltype(reversedGraph)>::edge_descriptor>(1);
133+
134+
// Run Dijkstra on reversed graph with uniform weights
135+
boost::dijkstra_shortest_paths(
136+
reversedGraph,
137+
targetVertex,
138+
boost::weight_map(weightMap)
139+
.distance_map(
140+
boost::make_iterator_property_map(distances.begin(), boost::get(boost::vertex_index, reversedGraph)))
141+
.predecessor_map(
142+
boost::make_iterator_property_map(
143+
predecessors.begin(), boost::get(boost::vertex_index, reversedGraph))));
144+
145+
cachedDistances = std::move(distances);
146+
cachedPredecessors = std::move(predecessors);
147+
distanceTarget = target;
148+
}
149+
150+
template<GraphNodeId NodeId, typename EdgeProperty>
151+
size_t DependencyGraph<NodeId, EdgeProperty>::getDistance(const NodeId & node) const
152+
{
153+
if (!cachedDistances.has_value()) {
154+
throw Error("must call computeDistancesFrom() before querying distances");
155+
}
156+
auto v = getVertexOrThrow(node);
157+
return (*cachedDistances)[v];
158+
}
159+
160+
template<GraphNodeId NodeId, typename EdgeProperty>
161+
std::optional<NodeId> DependencyGraph<NodeId, EdgeProperty>::getPredecessor(const NodeId & node) const
162+
{
163+
if (!cachedPredecessors.has_value()) {
164+
throw Error("must call computeDistancesFrom() before querying predecessors");
165+
}
166+
auto v = getVertexOrThrow(node);
167+
auto pred = (*cachedPredecessors)[v];
168+
169+
// If predecessor points to itself, it's the target (no predecessor)
170+
if (pred == v) {
171+
return std::nullopt;
172+
}
173+
174+
return getNodeId(pred);
175+
}
176+
177+
template<GraphNodeId NodeId, typename EdgeProperty>
178+
std::vector<NodeId> DependencyGraph<NodeId, EdgeProperty>::getSuccessors(const NodeId & node) const
179+
{
180+
auto v = getVertexOrThrow(node);
181+
auto [adjBegin, adjEnd] = boost::adjacent_vertices(v, graph);
182+
183+
return std::ranges::subrange(adjBegin, adjEnd) | std::views::transform([&](auto v) { return getNodeId(v); })
184+
| std::ranges::to<std::vector>();
185+
}
186+
187+
template<GraphNodeId NodeId, typename EdgeProperty>
188+
std::optional<EdgeProperty>
189+
DependencyGraph<NodeId, EdgeProperty>::getEdgeProperty(const NodeId & from, const NodeId & to) const
190+
requires(!std::same_as<EdgeProperty, boost::no_property>)
191+
{
192+
auto vFrom = getVertexOrThrow(from);
193+
auto vTo = getVertexOrThrow(to);
194+
195+
auto [edge, found] = boost::edge(vFrom, vTo, graph);
196+
if (!found) {
197+
return std::nullopt;
198+
}
199+
200+
return graph[edge];
201+
}
202+
203+
template<GraphNodeId NodeId, typename EdgeProperty>
204+
std::vector<NodeId> DependencyGraph<NodeId, EdgeProperty>::getAllNodes() const
205+
{
206+
return nodeToVertex | std::views::keys | std::ranges::to<std::vector>();
207+
}
208+
209+
} // namespace nix

0 commit comments

Comments
 (0)