From c6d6987e548593ca1fa7bb48fe440ebfc1186ffe Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Mon, 21 Sep 2026 19:03:19 +0200 Subject: [PATCH 1/2] App.walk_project_deps() / read_dep_edges(): Add methods App.get_project_refs() uses __get_project_refs() to walk the [pkg.*.jw] sections and pkg_relations() walks the same edges with its own implementation. In an attempt to unify the walks, this commit tries to make the first walk implementation so generally useful that it can be used by the second. - Make __get_project_refs() public as walk_project_deps(). Name changed because it sounds less awkward - Add three parameters needed from the other call site: - check_installed applies the installed-package check that skips the dependencies of an installed run package and fails on an unmet dependency - exclude names specs that are neither traversed nor appended - recurse = False stops the walk at the starting spec - Wrap the per-project dependency reading into App.read_dep_edges(), because the other call site does the same thing Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.86.1 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/App.py | 105 ++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 39 deletions(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index 8ee0f687..dabb97ef 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -198,7 +198,28 @@ class App(Base): except FileNotFoundError: return None - def __get_project_refs( + def read_dep_edges( + self, + name: str, + sections: list[str], + key: str, + ) -> list[str]: + """The deduplicated dependency values of name for the given + sections and key (flavour), in section and value order. + """ + ret: list[str] = [] + for section in sections: + vals = self.get_value(name, section, key) + log(DEBUG, f'name={name}, section={section}, key={key}, deps={vals}') + if not vals: + continue + for val in vals.split(','): + val = val.strip() + if (len(val)) and (val not in ret): + ret.append(val) + return ret + + def walk_project_deps( self, buf: list[str], visited: set[str], @@ -208,7 +229,21 @@ class App(Base): add_self: bool, scope: Scope, names_only: bool, + *, + check_installed: bool = True, + exclude: set[str] = set(), + recurse: bool = True, ) -> None: + """Walk the dependency edges of sections and key (flavour) + starting at spec, appending the visited specs to buf in + postorder; buf and visited are updated in place. + + check_installed applies the installed-package check, which + skips the dependencies of an installed run package and fails + on an unmet dependency. exclude names specs that are neither + traversed nor appended. recurse = False stops the walk at the + starting spec. + """ dep = Dependency(spec) name = dep.base_name mod = dep.full_name @@ -216,52 +251,44 @@ class App(Base): spec = name if spec in buf: return + if spec in exclude: + return if spec in visited: if add_self: buf.append(spec) return visited.add(spec) - needed_subpackage = 'devel' if mod.endswith(('-dev', '-devel')) else 'run' - if not self.is_installed(name, devel = True): - if not mod.endswith(('-dev', '-devel')): - if self.is_installed(name, devel = False): - return - raise Exception( - f'Unmet dependency on {mod}: the -{needed_subpackage} package ' - f'of project {name} is not installed' - ) - vals_list: list[str] = [] - for section in sections: - vals = self.get_value(name, section, key) - log( - DEBUG, - ( - f'name={name}, section={section}, key={key}, deps={vals}, ' - f'scope={scope.name}, visited={visited}' - ), - ) - if not vals: - continue - for val in vals.split(','): - val = val.strip() - if (len(val)) and (val not in vals_list): - vals_list.append(val) + if check_installed: + needed_subpackage = 'devel' if mod.endswith(('-dev', '-devel')) else 'run' + if not self.is_installed(name, devel = True): + if not mod.endswith(('-dev', '-devel')): + if self.is_installed(name, devel = False): + return + raise Exception( + f'Unmet dependency on {mod}: the -{needed_subpackage} package ' + f'of project {name} is not installed' + ) + vals_list = self.read_dep_edges(name, sections, key) match scope: case Scope.Self: buf += vals_list case Scope.One | Scope.Subtree: - subscope = scope.Self if scope == Scope.One else scope - for val in vals_list: - self.__get_project_refs( - buf, - visited, - val, - sections, - key, - add_self = True, - scope = subscope, - names_only = names_only, - ) + if recurse: + subscope = scope.Self if scope == Scope.One else scope + for val in vals_list: + self.walk_project_deps( + buf, + visited, + val, + sections, + key, + add_self = True, + scope = subscope, + names_only = names_only, + check_installed = check_installed, + exclude = exclude, + recurse = recurse, + ) if add_self: buf.append(spec) @@ -567,7 +594,7 @@ class App(Base): visited: set[str] = set() for name in projects: rr: list[str] = [] - self.__get_project_refs( + self.walk_project_deps( rr, visited, name, sections, key, add_self, scope, names_only ) # TODO: this looks like a performance hogger -- 2.55.0 From 5eccef847da01ef68a304050971baf023b96a72c Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Mon, 21 Sep 2026 19:09:14 +0200 Subject: [PATCH 2/2] cmds.projects.lib.pkg_relations(): Use walk_project_deps() pkg_relations() walks the [pkg..jw] edges with its own queue, duplicating the walk that App.walk_project_deps() extracted from App.__get_project_refs() in the previous commit. Port it onto the shared walk: for each flavour, the walk computes the closure of the seed packages (with the installed-package check off, the ignore set as exclude, and recursion per the recursive flag), and the function then emits the constraint strings of the edges of the visited projects. The relation sets are unchanged. The order of recursive output follows the walk (postorder) instead of the old breadth-first order; non-recursive callers, such as the spec generation and the PREREQ_* queries, print byte-identical output. The dont_expand_version_macros condition now tests membership in the visited set instead of the old queue, which only matters when the flag is passed manually. Verified over all projects of the dev tree: pkg-requires, pkg-recommends, pkg-conflicts and the skip-excluded, names-only and debian variants are byte-identical for the non-recursive forms and set-identical for the recursive ones. Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.86.1 Signed-off-by: Jan Lindemann --- .../jw/pkg/cmds/projects/lib/pkg_relations.py | 85 ++++++++++--------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/src/python/jw/pkg/cmds/projects/lib/pkg_relations.py b/src/python/jw/pkg/cmds/projects/lib/pkg_relations.py index 8156ad7e..4a42b070 100644 --- a/src/python/jw/pkg/cmds/projects/lib/pkg_relations.py +++ b/src/python/jw/pkg/cmds/projects/lib/pkg_relations.py @@ -52,47 +52,52 @@ def pkg_relations( ret: list[str] = [] for flavour in flavours: # build / release / run / devel - cur_pkgs = seed_pkgs.copy() - visited_pkgs: set[str] = set() - while len(cur_pkgs): - cur_pkg = cur_pkgs.pop(0) - if cur_pkg in visited_pkgs or cur_pkg in ignore: - continue + closure: list[str] = [] + visited: set[str] = set() + for seed in seed_pkgs: + app.walk_project_deps( + closure, + visited, + seed, + ['pkg.' + rel_type + '.jw'], + flavour, + add_self = True, + scope = Scope.Subtree, + names_only = True, + check_installed = False, + exclude = ignore, + recurse = recursive, + ) + for cur_pkg in closure: for subsec in subsections: section = 'pkg.' + rel_type + '.' + subsec - visited_pkgs.add(cur_pkg) - deps_spec = app.get_value(cur_pkg, section, flavour) - if not deps_spec: - continue - for dep in Dependency.parse_deps_spec( - deps_spec, - lookup_version = app.get_version, - dependent_package = cur_pkg, - ): - dep_name = dep.base_name - if dep_name in ignore or dep.full_name in ignore: - continue - if subsec == 'jw': - if (recursive and dep_name not in visited_pkgs - and dep_name not in cur_pkgs): - cur_pkgs.append(dep_name) - if hide_jw_pkg and dep_name == 'jw-pkg': + for deps_spec in app.read_dep_edges(cur_pkg, [section], flavour): + for dep in Dependency.parse_deps_spec( + deps_spec, + lookup_version = app.get_version, + dependent_package = cur_pkg, + ): + dep_name = dep.base_name + if dep_name in ignore or dep.full_name in ignore: continue - expand_version_macros = subsec == 'jw' - if dont_expand_version_macros and dep_name in cur_pkgs: - expand_version_macros = False - if hide_self and dep_name in seed_pkgs: - continue - dep_str = dep.constraint_str( - untemplated = expand_version_macros, - include_revision = dont_strip_revision, - as_range = expand_semver_revision_range, - no_subpackages = no_subpackages, - syntax = syntax, - quote = '"' if quote else None, - ) - if dep_str in ret: - continue - log(DEBUG, f'Appending dependency >{dep_str}<') - ret.append(dep_str) + if subsec == 'jw': + if hide_jw_pkg and dep_name == 'jw-pkg': + continue + expand_version_macros = subsec == 'jw' + if dont_expand_version_macros and dep_name in visited: + expand_version_macros = False + if hide_self and dep_name in seed_pkgs: + continue + dep_str = dep.constraint_str( + untemplated = expand_version_macros, + include_revision = dont_strip_revision, + as_range = expand_semver_revision_range, + no_subpackages = no_subpackages, + syntax = syntax, + quote = '"' if quote else None, + ) + if dep_str in ret: + continue + log(DEBUG, f'Appending dependency >{dep_str}<') + ret.append(dep_str) return ret -- 2.55.0