App: Remove duplicate from dependency graph #77

Merged
Jan Lindemann merged 2 commits from jan/fix/20260822-app-remove-duplicate-from-dependency-graph into master 2026-08-22 16:11:34 +02:00 AGit
Showing only changes of commit bd7cfa4ff8 - Show all commits

App.__find_circular_deps(): Fix cycle path
All checks were successful
CI / Packaging - Kali Linux (pull_request) Successful in 8m56s
CI / Packaging - OpenSUSE Tumbleweed (pull_request) Successful in 4m5s
CI / Packaging test (pull_request) Successful in 0s

__find_circular_deps_recursive() builds the cycle path by inserting
each visited dependency at the front of the list on the way back up,
and __find_circular_deps() appends the project where the cycle was
detected at the end. The path therefore starts at the first child of
the DFS root instead of at the project that closes the cycle, and the
closing project appears twice: for a dependency cycle between projects
A and B, find_circular_deps() reports "B -> A -> A" instead of
"A -> B -> A".

Keep the DFS stack as an ordered list, and when a dependency is already
on the stack, return the part of the stack from that dependency to the
top, plus the dependency itself, which starts and ends at the same
project. Reverse the result before returning, because an edge
a -> b in the flipped graph means that b depends on a, so the cycle
is reported in the original dependency direction.

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2
Signed-off-by: Jan Lindemann <jan@janware.com>
Jan Lindemann 2026-08-15 22:20:06 +02:00
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61

View file

@ -258,45 +258,43 @@ class App(Base):
project: str, project: str,
graph: Graph, graph: Graph,
unvisited: list[str], unvisited: list[str],
temp: set[str], stack: list[str],
path: list[str], ) -> list[str] | None:
) -> str | None: if project in stack:
if project in temp:
log(DEBUG, 'found circular dependency at project', project) log(DEBUG, 'found circular dependency at project', project)
return project idx = stack.index(project)
return stack[idx:] + [project]
if project not in unvisited: if project not in unvisited:
return None return None
temp.add(project) stack.append(project)
if project in graph: if project in graph:
for dep in graph[project]: for dep in graph[project]:
last = self.__find_circular_deps_recursive( cycle = self.__find_circular_deps_recursive(
dep, graph, unvisited, temp, path dep, graph, unvisited, stack
) )
if last is not None: if cycle is not None:
path.insert(0, dep) return cycle
return last
unvisited.remove(project) unvisited.remove(project)
temp.remove(project) stack.pop()
return None return None
def __find_circular_deps(self, projects: list[str], def __find_circular_deps(self, projects: list[str],
flavours: list[str]) -> list[str]: flavours: list[str]) -> list[str]:
graph: Graph = {} graph: Graph = {}
ret: list[str] = []
self.__read_dep_graph(projects, flavours, graph) self.__read_dep_graph(projects, flavours, graph)
unvisited = list(graph.keys()) unvisited = list(graph.keys())
temp: set[str] = set()
flipped = self.__flip_dep_graph(graph) flipped = self.__flip_dep_graph(graph)
while unvisited: while unvisited:
project = unvisited[0] project = unvisited[0]
log(DEBUG, 'Checking circular dependency of', project) log(DEBUG, 'Checking circular dependency of', project)
last = self.__find_circular_deps_recursive( cycle = self.__find_circular_deps_recursive(project, flipped, unvisited, [])
project, flipped, unvisited, temp, ret if cycle is not None:
) # An edge a -> b in the flipped graph means that b
if last is not None: # depends on a, so reverse to report the cycle in the
log(DEBUG, f'Found circular dependency below {project}, last is {last}') # original direction
ret.append(last) cycle = list(reversed(cycle))
return ret log(DEBUG, f'Found circular dependency: {" -> ".join(cycle)}')
return cycle
return [] return []
def __init__(self, distro: Distro | None = None) -> None: def __init__(self, distro: Distro | None = None) -> None: