Unify the project dependency walks in App and pkg_relations() #118

Merged
Jan Lindemann merged 2 commits from jan/feature/20260922-unify-dep-walks into master 2026-09-22 05:47:35 +02:00 AGit
2 changed files with 111 additions and 79 deletions

View file

@ -198,7 +198,28 @@ class App(Base):
except FileNotFoundError: except FileNotFoundError:
return None 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, self,
buf: list[str], buf: list[str],
visited: set[str], visited: set[str],
@ -208,7 +229,21 @@ class App(Base):
add_self: bool, add_self: bool,
scope: Scope, scope: Scope,
names_only: bool, names_only: bool,
*,
check_installed: bool = True,
exclude: set[str] = set(),
recurse: bool = True,
) -> None: ) -> 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) dep = Dependency(spec)
name = dep.base_name name = dep.base_name
mod = dep.full_name mod = dep.full_name
@ -216,52 +251,44 @@ class App(Base):
spec = name spec = name
if spec in buf: if spec in buf:
return return
if spec in exclude:
return
if spec in visited: if spec in visited:
if add_self: if add_self:
buf.append(spec) buf.append(spec)
return return
visited.add(spec) visited.add(spec)
needed_subpackage = 'devel' if mod.endswith(('-dev', '-devel')) else 'run' if check_installed:
if not self.is_installed(name, devel = True): needed_subpackage = 'devel' if mod.endswith(('-dev', '-devel')) else 'run'
if not mod.endswith(('-dev', '-devel')): if not self.is_installed(name, devel = True):
if self.is_installed(name, devel = False): if not mod.endswith(('-dev', '-devel')):
return if self.is_installed(name, devel = False):
raise Exception( return
f'Unmet dependency on {mod}: the -{needed_subpackage} package ' raise Exception(
f'of project {name} is not installed' 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_list = self.read_dep_edges(name, sections, key)
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)
match scope: match scope:
case Scope.Self: case Scope.Self:
buf += vals_list buf += vals_list
case Scope.One | Scope.Subtree: case Scope.One | Scope.Subtree:
subscope = scope.Self if scope == Scope.One else scope if recurse:
for val in vals_list: subscope = scope.Self if scope == Scope.One else scope
self.__get_project_refs( for val in vals_list:
buf, self.walk_project_deps(
visited, buf,
val, visited,
sections, val,
key, sections,
add_self = True, key,
scope = subscope, add_self = True,
names_only = names_only, scope = subscope,
) names_only = names_only,
check_installed = check_installed,
exclude = exclude,
recurse = recurse,
)
if add_self: if add_self:
buf.append(spec) buf.append(spec)
@ -567,7 +594,7 @@ class App(Base):
visited: set[str] = set() visited: set[str] = set()
for name in projects: for name in projects:
rr: list[str] = [] rr: list[str] = []
self.__get_project_refs( self.walk_project_deps(
rr, visited, name, sections, key, add_self, scope, names_only rr, visited, name, sections, key, add_self, scope, names_only
) )
# TODO: this looks like a performance hogger # TODO: this looks like a performance hogger

View file

@ -52,47 +52,52 @@ def pkg_relations(
ret: list[str] = [] ret: list[str] = []
for flavour in flavours: # build / release / run / devel for flavour in flavours: # build / release / run / devel
cur_pkgs = seed_pkgs.copy() closure: list[str] = []
visited_pkgs: set[str] = set() visited: set[str] = set()
while len(cur_pkgs): for seed in seed_pkgs:
cur_pkg = cur_pkgs.pop(0) app.walk_project_deps(
if cur_pkg in visited_pkgs or cur_pkg in ignore: closure,
continue 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: for subsec in subsections:
section = 'pkg.' + rel_type + '.' + subsec section = 'pkg.' + rel_type + '.' + subsec
visited_pkgs.add(cur_pkg) for deps_spec in app.read_dep_edges(cur_pkg, [section], flavour):
deps_spec = app.get_value(cur_pkg, section, flavour) for dep in Dependency.parse_deps_spec(
if not deps_spec: deps_spec,
continue lookup_version = app.get_version,
for dep in Dependency.parse_deps_spec( dependent_package = cur_pkg,
deps_spec, ):
lookup_version = app.get_version, dep_name = dep.base_name
dependent_package = cur_pkg, if dep_name in ignore or dep.full_name in ignore:
):
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':
continue continue
expand_version_macros = subsec == 'jw' if subsec == 'jw':
if dont_expand_version_macros and dep_name in cur_pkgs: if hide_jw_pkg and dep_name == 'jw-pkg':
expand_version_macros = False continue
if hide_self and dep_name in seed_pkgs: expand_version_macros = subsec == 'jw'
continue if dont_expand_version_macros and dep_name in visited:
dep_str = dep.constraint_str( expand_version_macros = False
untemplated = expand_version_macros, if hide_self and dep_name in seed_pkgs:
include_revision = dont_strip_revision, continue
as_range = expand_semver_revision_range, dep_str = dep.constraint_str(
no_subpackages = no_subpackages, untemplated = expand_version_macros,
syntax = syntax, include_revision = dont_strip_revision,
quote = '"' if quote else None, as_range = expand_semver_revision_range,
) no_subpackages = no_subpackages,
if dep_str in ret: syntax = syntax,
continue quote = '"' if quote else None,
log(DEBUG, f'Appending dependency >{dep_str}<') )
ret.append(dep_str) if dep_str in ret:
continue
log(DEBUG, f'Appending dependency >{dep_str}<')
ret.append(dep_str)
return ret return ret