From 857e9a6d82e5c5ebb36a10179b180761546fcf71 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sat, 15 Aug 2026 22:21:07 +0200 Subject: [PATCH 1/2] App.__read_dep_graph(): Remove redundant loop __read_dep_graph() iterates over the given sections (flavours), but the loop body does not use the loop variable: it passes the entire sections list to get_project_refs() on every iteration, so the same lookup is repeated for each section, and the recursion into the found dependencies is re-triggered (and skipped) for each of them. Remove the loop and do the lookup once per project. Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/App.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index 7abc3040..1e1a5d3b 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -232,18 +232,17 @@ class App(Base): for project in projects: if project in graph: continue - for section in sections: - deps = self.get_project_refs( - [project], - ['pkg.requires.jw'], - sections, - scope = Scope.One, - add_self = False, - names_only = True, - ) - graph[project] = set(deps) - for dep in deps: - self.__read_dep_graph([dep], sections, graph) + deps = self.get_project_refs( + [project], + ['pkg.requires.jw'], + sections, + scope = Scope.One, + add_self = False, + names_only = True, + ) + graph[project] = set(deps) + for dep in deps: + self.__read_dep_graph([dep], sections, graph) def __flip_dep_graph(self, graph: Graph) -> Graph: ret: Graph = {} -- 2.55.0 From bd7cfa4ff8ebe2800c2819ed5d1b524647d238c3 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sat, 15 Aug 2026 22:20:06 +0200 Subject: [PATCH 2/2] App.__find_circular_deps(): Fix cycle path __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 --- src/python/jw/pkg/App.py | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index 1e1a5d3b..c12bbaba 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -258,45 +258,43 @@ class App(Base): project: str, graph: Graph, unvisited: list[str], - temp: set[str], - path: list[str], - ) -> str | None: - if project in temp: + stack: list[str], + ) -> list[str] | None: + if project in stack: log(DEBUG, 'found circular dependency at project', project) - return project + idx = stack.index(project) + return stack[idx:] + [project] if project not in unvisited: return None - temp.add(project) + stack.append(project) if project in graph: for dep in graph[project]: - last = self.__find_circular_deps_recursive( - dep, graph, unvisited, temp, path + cycle = self.__find_circular_deps_recursive( + dep, graph, unvisited, stack ) - if last is not None: - path.insert(0, dep) - return last + if cycle is not None: + return cycle unvisited.remove(project) - temp.remove(project) + stack.pop() return None def __find_circular_deps(self, projects: list[str], flavours: list[str]) -> list[str]: graph: Graph = {} - ret: list[str] = [] self.__read_dep_graph(projects, flavours, graph) unvisited = list(graph.keys()) - temp: set[str] = set() flipped = self.__flip_dep_graph(graph) while unvisited: project = unvisited[0] log(DEBUG, 'Checking circular dependency of', project) - last = self.__find_circular_deps_recursive( - project, flipped, unvisited, temp, ret - ) - if last is not None: - log(DEBUG, f'Found circular dependency below {project}, last is {last}') - ret.append(last) - return ret + cycle = self.__find_circular_deps_recursive(project, flipped, unvisited, []) + if cycle is not None: + # An edge a -> b in the flipped graph means that b + # depends on a, so reverse to report the cycle in the + # original direction + cycle = list(reversed(cycle)) + log(DEBUG, f'Found circular dependency: {" -> ".join(cycle)}') + return cycle return [] def __init__(self, distro: Distro | None = None) -> None: -- 2.55.0