Skip to content

Commit

Permalink
utils/translator: produce merged cycles
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
wmertens committed Jul 30, 2022
1 parent 26dce97 commit f8c9d48
Showing 1 changed file with 96 additions and 62 deletions.
158 changes: 96 additions & 62 deletions src/utils/translator.nix
Original file line number Diff line number Diff line change
Expand Up @@ -167,75 +167,109 @@
allSources =
lib.recursiveUpdate sources generatedSources;

cyclicDependencies =
# TODO: inefficient! Implement some kind of early cutoff
let
findCycles = node: prevNodes: cycles: let
children = dependencyGraph."${node.name}"."${node.version}";
# discover cycles as sets with their members=true
# a member is pkgname#pkgversion (# should not be in version string)
# this walks dependencies depth-first
# It will eventually see parents as children => cycle
#
# To visit only new nodes, we pass around state in parentAcc:
# - visited: a set of already-visited packages
# - cycles: a list of cycle sets
getCycles = pkg: seen: parentAcc: let
deps = dependencyGraph."${pkg.name}"."${pkg.version}";
pkgTag = "${pkg.name}#${pkg.version}";
pkgTrue = lib.listToAttrs [(lib.nameValuePair pkgTag true)];

cyclicChildren =
lib.filter
(child: prevNodes ? "${child.name}#${child.version}")
children;
visitOne = acc: dep: let
depTag = "${dep.name}#${dep.version}";
depTrue = lib.listToAttrs [(lib.nameValuePair depTag true)];
in
if acc.visited ? "${depTag}"
then
# We will already have found all cycles it has, skip
acc
else if seen ? "${depTag}"
then
# We found a cycle
{
visited = acc.visited;
cycles = acc.cycles ++ [(pkgTrue // depTrue)];
}
else
# We need to check this dep
# Don't add pkg to visited until all deps were processed
getCycles dep (seen // pkgTrue) acc;
initialAcc = {
visited = parentAcc.visited;
cycles = [];
};

nonCyclicChildren =
lib.filter
(child: ! prevNodes ? "${child.name}#${child.version}")
children;
allVisited = b.foldl' visitOne initialAcc deps;
in {
visited = allVisited.visited // pkgTrue;
cycles =
if b.length allVisited.cycles != 0
then mergeCycles parentAcc.cycles allVisited.cycles
else parentAcc.cycles;
};

cycles' =
cycles
++ (b.map (child: {
from = node;
to = child;
})
cyclicChildren);
# merge cycles: We want a set of disjoined cycles
# meaning, for each cycle e.g. {a=true; b=true; c=true;...},
# there is no other cycle that has any member (a,b,c,...) of this set
# We maintain a set of already disjoint cycles and add a new cycle
# by merging all cycles of the set that have members in common with
# the cycle. The rest stays disjoint.
mergeCycles = djCycles: cycles: b.foldl' mergeCycle djCycles cycles;
mergeCycle = djCycles: cycle: let
cycleDeps = b.attrNames cycle;
includesDep = s: lib.any (n: s ? "${n}") cycleDeps;
partitions = lib.partition includesDep djCycles;
mergedCycle =
if b.length partitions.right != 0
then b.zipAttrsWith (n: v: true) ([cycle] ++ partitions.right)
else cycle;
in
[mergedCycle] ++ partitions.wrong;

# use set for efficient lookups
prevNodes' =
prevNodes
// {"${node.name}#${node.version}" = null;};
in
if nonCyclicChildren == []
then cycles'
else
lib.flatten
(b.map
(child: findCycles child prevNodes' cycles')
nonCyclicChildren);
allCycles = let
handleOne = acc: pkg:
getCycles (dlib.nameVersionPair (getName pkg) (getVersion pkg)) {} acc;
initalAcc = {
visited = {};
cycles = [];
};
allDone = b.foldl' handleOne initalAcc serializedPackagesList;
in
allDone.cycles;

cyclesList =
findCycles
(dlib.nameVersionPair defaultPackage packages."${defaultPackage}")
{}
[];
getCycleSets = cycles: b.foldl' lib.recursiveUpdate {} (b.map getCycleSetEntry cycles);
getCycleSetEntry = cycle: let
split = b.map toNameVersion (b.attrNames cycle);
toNameVersion = d: let
matches = b.match "^(.*)#([^#]*)$" d;
name = b.elemAt matches 0;
version = b.elemAt matches 1;
in {inherit name version;};
sorted = b.sort (x: y: let
lenX = b.stringLength x.name;
lenY = b.stringLength y.name;
in
b.foldl'
(cycles: cycle: (
let
existing =
cycles."${cycle.from.name}"."${cycle.from.version}"
or [];
if lenX < lenY
then true
else if lenX == lenY
then
if x.name < y.name
then true
else if x.name == y.name
then x.version > y.version
else false
else false)
split;
head = b.elemAt sorted 0;
cyclees = lib.drop 1 sorted;
in {${head.name}.${head.version} = cyclees;};

reverse =
cycles."${cycle.to.name}"."${cycle.to.version}"
or [];
in
# if edge or reverse edge already in cycles, do nothing
if
b.elem cycle.from reverse
|| b.elem cycle.to existing
then cycles
else
lib.recursiveUpdate
cycles
{
"${cycle.from.name}"."${cycle.from.version}" =
existing ++ [cycle.to];
}
))
{}
cyclesList;
cyclicDependencies = getCycleSets allCycles;
in
{
decompressed = true;
Expand Down

0 comments on commit f8c9d48

Please sign in to comment.