Skip to content

Commit de02ddb

Browse files
committed
utils/translator: produce merged cycles
- refactor with speed up via skipping visited nodes - merge cycles that have members in common - pick shortest name to be head of cycle set, so that between projects, the cycles remain the same
1 parent 26dce97 commit de02ddb

File tree

3 files changed

+148
-151
lines changed

3 files changed

+148
-151
lines changed

src/lib/getCycles.nix

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# A cycle is when packages depend on each other
2+
# The Nix store can't contain direct cycles, so cycles need special handling
3+
# They can be avoided by referencing by name, so the consumer sets up the cycle
4+
# internally, or by co-locating cycling packages in a single store path.
5+
# Both approaches are valid, it depends on the situation what fits better.
6+
#
7+
# The below code detects cycles by visiting all edges of the dependency graph
8+
# and keeping track of parents and already-visited nodes. Then it picks a head
9+
# for each cycle, and the other members are referred to as cyclees.
10+
# The head is the member with the shortest name, since that often results in a
11+
# head that "feels right".
12+
#
13+
# The visits are tracked by maintaining state in the accumulator during folding.
14+
{
15+
lib,
16+
dependencyGraph,
17+
}: let
18+
b = builtins;
19+
20+
# The separator char should never be in version
21+
mkTag = pkg: "${pkg.name}#${pkg.version}";
22+
trueAttr = tag: lib.listToAttrs [(lib.nameValuePair tag true)];
23+
24+
# discover cycles as sets with their members=true
25+
# a member is pkgname#pkgversion (# should not be in version string)
26+
# this walks dependencies depth-first
27+
# It will eventually see parents as children => cycle
28+
#
29+
# To visit only new nodes, we pass around state in parentAcc:
30+
# - visited: a set of already-visited packages
31+
# - cycles: a list of cycle sets
32+
getCycles = pkg: parents: parentAcc: let
33+
deps = dependencyGraph."${pkg.name}"."${pkg.version}";
34+
pkgTag = mkTag pkg;
35+
pkgTrue = trueAttr pkgTag;
36+
37+
visitOne = acc: dep: let
38+
depTag = mkTag dep;
39+
depTrue = trueAttr depTag;
40+
in
41+
if acc.visited ? "${depTag}"
42+
then
43+
# We will already have found all cycles it has, skip
44+
acc
45+
else if parents ? "${depTag}"
46+
then
47+
# We found a cycle
48+
{
49+
visited = acc.visited;
50+
cycles = acc.cycles ++ [(pkgTrue // depTrue)];
51+
}
52+
else
53+
# We need to check this dep
54+
# Don't add pkg to visited until all deps are processed
55+
getCycles dep (parents // pkgTrue) acc;
56+
initialAcc = {
57+
visited = parentAcc.visited;
58+
cycles = [];
59+
};
60+
61+
allVisited = b.foldl' visitOne initialAcc deps;
62+
in
63+
if parentAcc.visited ? "${pkgTag}"
64+
then
65+
# this can happen while walking the root nodes
66+
parentAcc
67+
else {
68+
visited = allVisited.visited // pkgTrue;
69+
cycles =
70+
if b.length allVisited.cycles != 0
71+
then mergeCycles parentAcc.cycles allVisited.cycles
72+
else parentAcc.cycles;
73+
};
74+
75+
# merge cycles: We want a set of disjoined cycles
76+
# meaning, for each cycle of the set e.g. {a=true; b=true; c=true;...},
77+
# there is no other cycle that has any member (a,b,c,...) of this set
78+
# We maintain a set of already disjoint cycles and add a new cycle
79+
# by merging all cycles of the set that have members in common with
80+
# the cycle. The rest stays disjoint.
81+
mergeCycles = b.foldl' mergeOneCycle;
82+
mergeOneCycle = djCycles: cycle: let
83+
cycleDeps = b.attrNames cycle;
84+
includesDep = s: lib.any (n: s ? "${n}") cycleDeps;
85+
partitions = lib.partition includesDep djCycles;
86+
mergedCycle =
87+
if b.length partitions.right != 0
88+
then b.zipAttrsWith (n: v: true) ([cycle] ++ partitions.right)
89+
else cycle;
90+
disjoined = [mergedCycle] ++ partitions.wrong;
91+
in
92+
disjoined;
93+
94+
# Walk all root nodes of the dependency graph
95+
allCycles = let
96+
mkHandleVersion = name: acc: version:
97+
getCycles {inherit name version;} {} acc;
98+
handleName = acc: name: let
99+
pkgVersions = b.attrNames dependencyGraph.${name};
100+
handleVersion = mkHandleVersion name;
101+
in
102+
b.foldl' handleVersion acc pkgVersions;
103+
104+
initalAcc = {
105+
visited = {};
106+
cycles = [];
107+
};
108+
rootNames = b.attrNames dependencyGraph;
109+
110+
allDone = b.foldl' handleName initalAcc rootNames;
111+
in
112+
allDone.cycles;
113+
114+
# Convert list of cycle sets to set of cycle lists
115+
getCycleSets = cycles: b.foldl' lib.recursiveUpdate {} (b.map getCycleSetEntry cycles);
116+
getCycleSetEntry = cycle: let
117+
split = b.map toNameVersion (b.attrNames cycle);
118+
toNameVersion = d: let
119+
matches = b.match "^(.*)#([^#]*)$" d;
120+
name = b.elemAt matches 0;
121+
version = b.elemAt matches 1;
122+
in {inherit name version;};
123+
sorted =
124+
b.sort
125+
(x: y: let
126+
lenX = b.stringLength x.name;
127+
lenY = b.stringLength y.name;
128+
in
129+
if lenX < lenY
130+
then true
131+
else if lenX == lenY
132+
then
133+
if x.name < y.name
134+
then true
135+
else if x.name == y.name
136+
then x.version > y.version
137+
else false
138+
else false)
139+
split;
140+
head = b.elemAt sorted 0;
141+
cyclees = lib.drop 1 sorted;
142+
in {${head.name}.${head.version} = cyclees;};
143+
144+
cyclicDependencies = getCycleSets allCycles;
145+
in
146+
cyclicDependencies

src/lib/simpleTranslate2.nix

Lines changed: 1 addition & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -178,88 +178,7 @@
178178
versions)
179179
relevantDependencies;
180180

181-
cyclicDependencies =
182-
# TODO: inefficient! Implement some kind of early cutoff
183-
let
184-
depGraphWithFakeRoot =
185-
l.recursiveUpdate
186-
dependencyGraph
187-
{
188-
__fake-entry.__fake-version =
189-
l.mapAttrsToList
190-
dlib.nameVersionPair
191-
exportedPackages;
192-
};
193-
194-
findCycles = node: prevNodes: cycles: let
195-
children =
196-
depGraphWithFakeRoot."${node.name}"."${node.version}";
197-
198-
cyclicChildren =
199-
lib.filter
200-
(child: prevNodes ? "${child.name}#${child.version}")
201-
children;
202-
203-
nonCyclicChildren =
204-
lib.filter
205-
(child: ! prevNodes ? "${child.name}#${child.version}")
206-
children;
207-
208-
cycles' =
209-
cycles
210-
++ (l.map (child: {
211-
from = node;
212-
to = child;
213-
})
214-
cyclicChildren);
215-
216-
# use set for efficient lookups
217-
prevNodes' =
218-
prevNodes
219-
// {"${node.name}#${node.version}" = null;};
220-
in
221-
if nonCyclicChildren == []
222-
then cycles'
223-
else
224-
lib.flatten
225-
(l.map
226-
(child: findCycles child prevNodes' cycles')
227-
nonCyclicChildren);
228-
229-
cyclesList =
230-
findCycles
231-
(dlib.nameVersionPair
232-
"__fake-entry"
233-
"__fake-version")
234-
{}
235-
[];
236-
in
237-
l.foldl'
238-
(cycles: cycle: (
239-
let
240-
existing =
241-
cycles."${cycle.from.name}"."${cycle.from.version}"
242-
or [];
243-
244-
reverse =
245-
cycles."${cycle.to.name}"."${cycle.to.version}"
246-
or [];
247-
in
248-
# if edge or reverse edge already in cycles, do nothing
249-
if
250-
l.elem cycle.from reverse
251-
|| l.elem cycle.to existing
252-
then cycles
253-
else
254-
lib.recursiveUpdate
255-
cycles
256-
{
257-
"${cycle.from.name}"."${cycle.from.version}" =
258-
existing ++ [cycle.to];
259-
}
260-
))
261-
{}
262-
cyclesList;
181+
cyclicDependencies = import ./getCycles.nix {inherit lib dependencyGraph;};
263182

264183
data =
265184
{

src/utils/translator.nix

Lines changed: 1 addition & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -167,75 +167,7 @@
167167
allSources =
168168
lib.recursiveUpdate sources generatedSources;
169169

170-
cyclicDependencies =
171-
# TODO: inefficient! Implement some kind of early cutoff
172-
let
173-
findCycles = node: prevNodes: cycles: let
174-
children = dependencyGraph."${node.name}"."${node.version}";
175-
176-
cyclicChildren =
177-
lib.filter
178-
(child: prevNodes ? "${child.name}#${child.version}")
179-
children;
180-
181-
nonCyclicChildren =
182-
lib.filter
183-
(child: ! prevNodes ? "${child.name}#${child.version}")
184-
children;
185-
186-
cycles' =
187-
cycles
188-
++ (b.map (child: {
189-
from = node;
190-
to = child;
191-
})
192-
cyclicChildren);
193-
194-
# use set for efficient lookups
195-
prevNodes' =
196-
prevNodes
197-
// {"${node.name}#${node.version}" = null;};
198-
in
199-
if nonCyclicChildren == []
200-
then cycles'
201-
else
202-
lib.flatten
203-
(b.map
204-
(child: findCycles child prevNodes' cycles')
205-
nonCyclicChildren);
206-
207-
cyclesList =
208-
findCycles
209-
(dlib.nameVersionPair defaultPackage packages."${defaultPackage}")
210-
{}
211-
[];
212-
in
213-
b.foldl'
214-
(cycles: cycle: (
215-
let
216-
existing =
217-
cycles."${cycle.from.name}"."${cycle.from.version}"
218-
or [];
219-
220-
reverse =
221-
cycles."${cycle.to.name}"."${cycle.to.version}"
222-
or [];
223-
in
224-
# if edge or reverse edge already in cycles, do nothing
225-
if
226-
b.elem cycle.from reverse
227-
|| b.elem cycle.to existing
228-
then cycles
229-
else
230-
lib.recursiveUpdate
231-
cycles
232-
{
233-
"${cycle.from.name}"."${cycle.from.version}" =
234-
existing ++ [cycle.to];
235-
}
236-
))
237-
{}
238-
cyclesList;
170+
cyclicDependencies = import ../lib/getCycles.nix {inherit lib dependencyGraph;};
239171
in
240172
{
241173
decompressed = true;

0 commit comments

Comments
 (0)