From 676fd7583987ca3f8876202fd25d3870ccabcfe1 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Thu, 28 Jul 2022 15:03:37 +0200 Subject: [PATCH] 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 --- src/utils/translator.nix | 146 ++++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 63 deletions(-) diff --git a/src/utils/translator.nix b/src/utils/translator.nix index 2297519229..062d2c507f 100644 --- a/src/utils/translator.nix +++ b/src/utils/translator.nix @@ -167,75 +167,95 @@ 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}") - {} - []; - in - b.foldl' - (cycles: cycle: ( - let - existing = - cycles."${cycle.from.name}"."${cycle.from.version}" - or []; + 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: b.stringLength x.name < b.stringLength y.name || x.name < y.name || x.version > y.version) 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;