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 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