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

View file

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