cmds.projects.lib.pkg_relations: Add module

BaseCmdPkgRelations contains pkg_relations(), a function doing
package graph analysis code. The function needs to be made available
to code outside BaseCmdPkgRelations, so move it to
cmds.projects.lib.pkg_relations.

The commit also applies style fixes to both BaseCmdPkgRelations and
pkg_relations which anticipate broader style changes to jw-pkg in
general.

Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-05-27 15:42:50 +02:00
commit d449472ceb
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
2 changed files with 269 additions and 175 deletions

View file

@ -0,0 +1,169 @@
import re
from enum import Enum, auto
from ....App import App, Scope
from ....lib.log import DEBUG, log
class VersionSyntax(Enum):
semver = auto()
debian = auto()
names_only = auto()
def pkg_relations(
app: App,
rel_type: str,
flavours: list[str],
seed_pkgs: list[str],
subsections: list[str] | None = None,
delimiter: str = ' ',
no_subpackages: bool = False,
dont_strip_revision: bool = False,
expand_semver_revision_range: bool = False,
syntax: VersionSyntax = VersionSyntax.semver,
recursive: bool = False,
dont_expand_version_macros: bool = False,
ignore: set[str] = set(),
quote: bool = False,
skip_excluded: bool = False,
hide_self = False,
hide_jw_pkg = False,
) -> list[str]:
if subsections is None:
subsections = app.distro.os_cascade
subsections.append('jw')
expand_semver_revision_range = expand_semver_revision_range
if syntax == VersionSyntax.debian:
expand_semver_revision_range = True
if skip_excluded:
excluded = app.get_project_refs(
seed_pkgs,
['build'],
'exclude',
scope = Scope.One,
add_self = False,
names_only = True,
)
ignore |= set(excluded)
log(
DEBUG,
(
f'flavours="{", ".join(flavours)}", '
f'subsections="{", ".join(subsections)}", '
f'"ignore="{", ".join(ignore)}"'
),
)
version_pattern = re.compile('[0-9-.]*')
ret: list[str] = []
for flavour in flavours: # build / release / run / devel
cur_pkgs = seed_pkgs.copy()
visited = set()
while len(cur_pkgs):
cur_pkg = cur_pkgs.pop(0)
if cur_pkg in visited or cur_pkg in ignore:
continue
for subsec in subsections:
section = 'pkg.' + rel_type + '.' + subsec
visited.add(cur_pkg)
value = app.get_value(cur_pkg, section, flavour)
if not value:
continue
deps = value.split(',')
for spec in deps:
dep = re.split('([=><]+)', spec)
if syntax == VersionSyntax.names_only:
dep = dep[:1]
dep = list(map(str.strip, dep))
dep_name = re.sub('-dev$|-devel$|-run$', '', dep[0])
if dep_name in ignore or dep[0] in ignore:
continue
if no_subpackages:
dep[0] = dep_name
for i, item in enumerate(dep):
dep[i] = item.strip()
if subsec == 'jw':
if (recursive and dep_name not in visited
and dep_name not in cur_pkgs):
cur_pkgs.append(dep_name)
if hide_jw_pkg:
continue
if len(dep) == 3:
if dont_expand_version_macros and dep_name in cur_pkgs:
version = dep[2]
else:
version = app.get_version(dep_name)
if dep[2] == 'VERSION':
if dont_strip_revision:
dep[2] = version
else:
dep[2] = version.split('-')[0]
elif dep[2] == 'VERSION-REVISION':
dep[2] = version
elif version_pattern.match(dep[2]):
# dep[2] = dep[2]
pass
else:
raise Exception('Unknown version specifier in ' + spec)
if len(dep) != 3 or not expand_semver_revision_range:
expanded_deps = [dep]
else:
expanded_deps = []
semver = re.split(r'[.-]', version)
if len(semver) != 4:
expanded_deps = [dep]
else:
release = int(semver[2])
major_minor = f'{semver[0]}.{semver[1]}'
match dep[1]:
case '>' | '>=':
expanded_deps.append([dep[0], dep[1], dep[2]])
expanded_deps.append(
[dep[0], '<', f'{major_minor}.{release + 1}']
)
case '<' | '<=':
expanded_deps.append([dep[0], dep[1], dep[2]])
case '=':
expanded_deps.append(
[dep[0], '>=', f'{major_minor}.{release}']
)
expanded_deps.append(
[dep[0], '<', f'{major_minor}.{release + 1}']
)
case _:
raise NotImplementedError(
(
'Expanding SemVer range '
f'"{dep[0]} {dep[1]} {dep[3]}" '
'is not yet implemented'
)
)
for expanded_dep in expanded_deps:
if hide_self and dep_name in seed_pkgs:
continue
match syntax:
case VersionSyntax.semver:
pass
case VersionSyntax.debian:
if len(expanded_dep) == 3:
match expanded_dep[1]:
case '<':
expanded_dep[1] = '<<'
case '>':
expanded_dep[1] = '>>'
case '_':
raise NotImplementedError(
f'Unknown dependency syntax "{syntax}" for '
f'dependency "{dep[0]} {dep[1]} {dep[3]}"'
)
dep_str = ' '.join(expanded_dep)
if quote:
dep_str = '"' + dep_str + '"'
if dep_str not in ret:
log(DEBUG, f'Appending dependency >{dep_str}<')
ret.append(dep_str)
return ret