From 83d6f4b2df8f579f97a91baeacdd2c250529c6e7 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Tue, 30 Jun 2026 07:07:03 +0200 Subject: [PATCH 1/8] App.__find_circular_deps(): Move __flip_dep_graph() out The __flip_dep_graph(graph) call sits inside the while loop and performs redundant graph flipping on every iteration. Hoist it outside to compute once and reuse the result. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev Signed-off-by: Jan Lindemann --- src/python/jw/pkg/App.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index 03fb3f7b..7d93af61 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -319,11 +319,12 @@ class App(Base): 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, self.__flip_dep_graph(graph), unvisited, temp, ret + project, flipped, unvisited, temp, ret ) if last is not None: log(DEBUG, f'Found circular dependency below {project}, last is {last}') -- 2.55.0 From a61a55c696f541257e20143c8d9d59625a644a17 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Mon, 29 Jun 2026 21:57:58 +0200 Subject: [PATCH 2/8] App: Remove dead None check in __read_dep_graph() get_project_refs() always returns a list[str], never None. The `if deps is None: continue` check in __read_dep_graph() is dead code, remove it. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev Signed-off-by: Jan Lindemann --- src/python/jw/pkg/App.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index 7d93af61..443b5d1d 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -272,8 +272,6 @@ class App(Base): add_self = False, names_only = True, ) - if deps is None: - continue graph[project] = set(deps) for dep in deps: self.__read_dep_graph([dep], sections, graph) -- 2.55.0 From 59a0e2ada866fbe3586689c6582633ee66bc80ae Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Tue, 30 Jun 2026 07:07:13 +0200 Subject: [PATCH 3/8] App.find_dir/__find_dir(): Fix mutable default args Mutable default arguments (list) cause unexpected shared state between calls. Use None as default and initialise to [] inside the function body. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev Signed-off-by: Jan Lindemann --- src/python/jw/pkg/App.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index 443b5d1d..894b62aa 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -126,10 +126,14 @@ class App(Base): def __find_dir( self, name: str, - search_subdirs: list[str] = [], - search_absdirs: list[str] = [], + search_subdirs: list[str] | None = None, + search_absdirs: list[str] | None = None, pretty: bool = True, ) -> str | None: + if search_subdirs is None: + search_subdirs = [] + if search_absdirs is None: + search_absdirs = [] def __format_relpath(path: str): if path.startswith('./'): @@ -488,8 +492,8 @@ class App(Base): def find_dir( self, name: str, - search_subdirs: list[str] = [], - search_absdirs: list[str] = [], + search_subdirs: list[str] | None = None, + search_absdirs: list[str] | None = None, pretty: bool = True, throw: bool = False, ) -> str | None: -- 2.55.0 From 09e41df0ede634095039fc8f9a51c13ec763f3c0 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Tue, 30 Jun 2026 07:07:24 +0200 Subject: [PATCH 4/8] App.__proj_dir(): Use configurable ___opt_root Replace the hardcoded /opt in __proj_dir() with a configurable ___opt_root member, consistent with how __projs_root is configured. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev Signed-off-by: Jan Lindemann --- src/python/jw/pkg/App.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index 894b62aa..aa11b102 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -114,7 +114,7 @@ class App(Base): if pretty: return self.__pretty_topdir return self.__topdir - for d in [self.__projs_root, '/opt']: + for d in [self.__projs_root, self.___opt_root]: ret = d + '/' + name if os.path.exists(ret): return ret @@ -350,6 +350,7 @@ class App(Base): # -- Members with default values self.__topdir_fmt = 'absolute' self.__projs_root = pwd.getpwuid(os.getuid()).pw_dir + '/local/src/jw.dev/proj' + self.___opt_root = '/opt' self.__pretty_projs_root = None async def __init_async(self) -> None: -- 2.55.0 From fdae5ad1221d4b790b225687c9a92eb96cea5689 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Tue, 30 Jun 2026 07:07:31 +0200 Subject: [PATCH 5/8] App.get_value(): Remove redundant fd.close() The fd.close() call inside the `with open(...) as fd:` block is redundant because the file handle is managed by the context manager. Remove it. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev Signed-off-by: Jan Lindemann --- src/python/jw/pkg/App.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index aa11b102..a3583459 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -545,7 +545,6 @@ class App(Base): try: with open(version_path) as fd: ret = fd.read().replace('\n', '').replace('-dev', '') - fd.close() return ret except EnvironmentError: log(DEBUG, f'Ignoring unreadable file "{version_path}"') -- 2.55.0 From 24d67a5ec0ecb32c37358034b836e79659b69b88 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Tue, 30 Jun 2026 11:26:52 +0200 Subject: [PATCH 6/8] App: Return cycle path from find_circular_deps() The __find_circular_deps() and find_circular_deps() methods now return list[str] instead of bool. On a cycle, the path builds up the dependency chain in __find_circular_deps_recursive(), and __find_circular_deps() appends the closing project to complete the cycle. An empty list means no cycle found. CmdDep prints the cycle as 'a -> b -> c -> a' instead of a generic message. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev Signed-off-by: Jan Lindemann --- src/python/jw/pkg/App.py | 10 ++++++---- src/python/jw/pkg/cmds/projects/check/CmdDep.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index a3583459..1cd974c8 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -315,7 +315,8 @@ class App(Base): temp.remove(project) return None - def __find_circular_deps(self, projects: list[str], flavours: list[str]) -> bool: + 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) @@ -330,8 +331,9 @@ class App(Base): ) if last is not None: log(DEBUG, f'Found circular dependency below {project}, last is {last}') - return True - return False + ret.append(last) + return ret + return [] def __init__(self, distro: Distro | None = None) -> None: @@ -627,5 +629,5 @@ class App(Base): return ', '.join(intersection) return None - def find_circular_deps(self, projects: list[str], flavours: list[str]) -> bool: + def find_circular_deps(self, projects: list[str], flavours: list[str]) -> list[str]: return self.__find_circular_deps(projects, flavours) diff --git a/src/python/jw/pkg/cmds/projects/check/CmdDep.py b/src/python/jw/pkg/cmds/projects/check/CmdDep.py index e6469b90..478b26ae 100644 --- a/src/python/jw/pkg/cmds/projects/check/CmdDep.py +++ b/src/python/jw/pkg/cmds/projects/check/CmdDep.py @@ -1,8 +1,9 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from ....lib.log import NOTICE, log from .Cmd import Cmd, Parent -from typing import TYPE_CHECKING if TYPE_CHECKING: from argparse import ArgumentParser, Namespace @@ -22,8 +23,13 @@ class CmdDep(Cmd): # export parser.add_argument('-f', '--flavour', nargs = '?', default = 'build') async def _run(self, args: Namespace) -> None: - if self.app.find_circular_deps(args.module, args.flavour): - log(NOTICE, f'Found circular dependency in flavour {args.flavour}') + cycle = self.app.find_circular_deps(args.module, args.flavour) + if cycle: + log( + NOTICE, + f'Found circular dependency in flavour {args.flavour}: ' + ' -> '.join(cycle) + ) exit(1) log( NOTICE, -- 2.55.0 From 42f064de8942e2648fbbce477e026fcfa2969239 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Mon, 29 Jun 2026 21:54:14 +0200 Subject: [PATCH 7/8] App.get_value(): Fix FileNotFoundError regression The refactored __get_project_conf() raises FileNotFoundError when project.conf does not exist, whereas the original read_value() returns None. This causes get_value() to crash for projects with missing or incomplete project.conf files. Catch FileNotFoundError in __get_project_conf() and return None to restore original behavior. Remove redundant @cache from __read_project_conf() since __get_project_conf() already provides caching. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev Signed-off-by: Jan Lindemann --- src/python/jw/pkg/App.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index 1cd974c8..c474ead0 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -185,16 +185,18 @@ class App(Base): return ret return None - @cache def __read_project_conf(self, project_dir: str) -> ProjectConf: return ProjectConf.read(project_dir + '/make/project.conf') @cache - def __get_project_conf(self, project: str) -> ProjectConf: + def __get_project_conf(self, project: str) -> ProjectConf | None: pd = self.__proj_dir(project, False) if pd is None: raise Exception(f'Failed to find directory of project {project}') - return self.__read_project_conf(pd) + try: + return self.__read_project_conf(pd) + except FileNotFoundError: + return None def __get_project_refs_cached( self, buf, visited, spec, section, key, add_self, scope, names_only @@ -552,7 +554,10 @@ class App(Base): log(DEBUG, f'Ignoring unreadable file "{version_path}"') continue raise Exception(f'No version file found for project "{project}"') - ret = self.__get_project_conf(project).get_str_or_none(section, key) + proj_conf = self.__get_project_conf(project) + if proj_conf is None: + return None + ret = proj_conf.get_str_or_none(section, key) log( DEBUG, 'Lookup %s -> %s / [%s%s] -> "%s"' % -- 2.55.0 From a2a4312f5b0dff4208b67fedb0ac9433a9f66762 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Mon, 29 Jun 2026 21:56:17 +0200 Subject: [PATCH 8/8] App.get_values(): Filter out empty split results get_values() splits comma-separated values and strips whitespace but does not filter out empty strings. A value like "a, b, " produces ['a', 'b', ''], with an empty string at the end. This empty string propagates to callers like CmdRequiredOsPkg.py and pollutes output. Add a filter for non-empty stripped values. Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev Signed-off-by: Jan Lindemann --- src/python/jw/pkg/App.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index c474ead0..3dfee93c 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -584,7 +584,10 @@ class App(Base): for key in keys: vals = self.get_value(p, section, key) if vals: - ret += [val.strip() for val in vals.split(',')] + for val in vals.split(','): + stripped = val.strip() + if stripped: + ret.append(stripped) return list(dict.fromkeys(ret)) # Remove duplicates, keep ordering def get_project_refs( -- 2.55.0