From d449472ceb96e73e896922dfb787975f557ea3f3 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Wed, 27 May 2026 15:42:50 +0200 Subject: [PATCH] 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 --- .../pkg/cmds/projects/BaseCmdPkgRelations.py | 275 +++++++----------- .../jw/pkg/cmds/projects/lib/pkg_relations.py | 169 +++++++++++ 2 files changed, 269 insertions(+), 175 deletions(-) create mode 100644 src/python/jw/pkg/cmds/projects/lib/pkg_relations.py diff --git a/src/python/jw/pkg/cmds/projects/BaseCmdPkgRelations.py b/src/python/jw/pkg/cmds/projects/BaseCmdPkgRelations.py index f7ecdaef..9f87af7e 100644 --- a/src/python/jw/pkg/cmds/projects/BaseCmdPkgRelations.py +++ b/src/python/jw/pkg/cmds/projects/BaseCmdPkgRelations.py @@ -1,162 +1,25 @@ -# -*- coding: utf-8 -*- +from argparse import ArgumentParser, Namespace -import re -from argparse import Namespace, ArgumentParser -from enum import Enum, auto - -from ...lib.log import * -from ...App import Scope -from ..Cmd import Cmd -from ..CmdProjects import CmdProjects +from .Cmd import Cmd, Parent +from .lib.pkg_relations import VersionSyntax +from .lib.pkg_relations import pkg_relations as pkg_relations_list class BaseCmdPkgRelations(Cmd): - class Syntax(Enum): - semver = auto() - debian = auto() - names_only = auto() - - def pkg_relations_list( - self, - 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: Syntax=Syntax.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 = self.app.distro.os_cascade - subsections.append('jw') - - expand_semver_revision_range = expand_semver_revision_range - if syntax == self.Syntax.debian: - expand_semver_revision_range = True - - if skip_excluded: - excluded = self.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)}", subsections="{", ".join(subsections)}", "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 = self.app.get_value(cur_pkg, section, flavour) - if not value: - continue - deps = value.split(',') - for spec in deps: - dep = re.split('([=><]+)', spec) - if syntax == self.Syntax.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 not dep_name in visited and not dep_name 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 = self.app.get_value(dep_name, 'version', '') - 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(f'Expanding SemVer range "{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 self.Syntax.semver: - pass - case self.Syntax.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 dependency "{dep[0]} {dep[1]} {dep[3]}"') - dep_str = ' '.join(expanded_dep) - if quote: - dep_str = '"' + dep_str + '"' - if not dep_str in ret: - log(DEBUG, f'Appending dependency >{dep_str}<') - ret.append(dep_str) - return ret - def pkg_relations(self, rel_type: str, args: Namespace) -> str: return args.delimiter.join( - self.pkg_relations_list( - rel_type=rel_type, + pkg_relations_list( + self.app, + rel_type = rel_type, flavours = args.flavours.split(','), seed_pkgs = args.modules, - subsections = None if args.subsections is None else args.subsections.split(','), + subsections = None + if args.subsections is None else args.subsections.split(','), no_subpackages = args.no_subpackages, dont_strip_revision = args.dont_strip_revision, expand_semver_revision_range = args.expand_semver_revision_range, - syntax = self.Syntax[args.syntax.replace('-', '_')], + syntax = VersionSyntax[args.syntax.replace('-', '_')], recursive = args.recursive, dont_expand_version_macros = args.dont_expand_version_macros, ignore = set(args.ignore.split(',')), @@ -170,38 +33,100 @@ class BaseCmdPkgRelations(Cmd): def print_pkg_relations(self, rel_type: str, args: Namespace) -> None: print(self.pkg_relations(rel_type, args)) - def __init__(self, parent: CmdProjects, relation: str, help: str) -> None: - super().__init__(parent, 'pkg-' + relation, help=help) + def __init__(self, parent: Parent, relation: str, help: str) -> None: + super().__init__(parent, 'pkg-' + relation, help = help) self.relation = relation def add_arguments(self, parser: ArgumentParser) -> None: super().add_arguments(parser) - parser.add_argument('-S', '--subsections', nargs='?', default=None, help='Subsections to consider, comma-separated') - parser.add_argument('-d', '--delimiter', nargs='?', default=', ', help='Output words delimiter') - parser.add_argument('flavours', help='Dependency flavours (run, build, devel, release)') - parser.add_argument('modules', nargs='*', help='Modules') - parser.add_argument('-p', '--no-subpackages', action='store_true', - default=False, help='Cut -run and -devel from package names') - parser.add_argument('--dont-strip-revision', action='store_true', - default=False, help='Always treat VERSION macro as VERSION-REVISION') - parser.add_argument('--expand-semver-revision-range', action='store_true', - default=False, help='Always treat =VERSION macro as >= VERSION-0 and < (VERSION+1)-0') - parser.add_argument('--syntax', choices=['semver', 'debian', 'names-only'], - default='semver', help='Output syntax') - parser.add_argument('--recursive', action='store_true', - default=False, help='Find dependencies recursively') - parser.add_argument('--dont-expand-version-macros', action='store_true', - default=False, help='Don\'t expand VERSION and REVISION macros') - parser.add_argument('--ignore', nargs='?', default='', help='Packages that ' - 'should be ignored together with their dependencies') - parser.add_argument('--skip-excluded', action='store_true', default=False, - help='Don\'t consider or output modules matching the os cascade in their [build].exclude config') - parser.add_argument('--hide-self', action='store_true', default=False, - help='Don\'t include any of the projects listed in in the output') - parser.add_argument('--hide-jw-pkg', action='store_true', default=False, - help='Don\'t include packages from requires.jw in the output') - parser.add_argument('--quote', action='store_true', default=False, - help='Put double quotes around each listed dependency') + parser.add_argument( + '-S', + '--subsections', + nargs = '?', + default = None, + help = 'Subsections to consider, comma-separated', + ) + parser.add_argument( + '-d', + '--delimiter', + nargs = '?', + default = ', ', + help = 'Output words delimiter' + ) + parser.add_argument( + 'flavours', help = 'Dependency flavours (run, build, devel, release)' + ) + parser.add_argument('modules', nargs = '*', help = 'Modules') + parser.add_argument( + '-p', + '--no-subpackages', + action = 'store_true', + default = False, + help = 'Cut -run and -devel from package names', + ) + parser.add_argument( + '--dont-strip-revision', + action = 'store_true', + default = False, + help = 'Always treat VERSION macro as VERSION-REVISION', + ) + parser.add_argument( + '--expand-semver-revision-range', + action = 'store_true', + default = False, + help = 'Always treat =VERSION macro as >= VERSION-0 and < (VERSION+1)-0', + ) + parser.add_argument( + '--syntax', + choices = ['semver', 'debian', 'names-only'], + default = 'semver', + help = 'Output syntax', + ) + parser.add_argument( + '--recursive', + action = 'store_true', + default = False, + help = 'Find dependencies recursively', + ) + parser.add_argument( + '--dont-expand-version-macros', + action = 'store_true', + default = False, + help = "Don't expand VERSION and REVISION macros", + ) + parser.add_argument( + '--ignore', + nargs = '?', + default = '', + help = 'Packages that should be ignored together with their dependencies', + ) + parser.add_argument( + '--skip-excluded', + action = 'store_true', + default = False, + help = ( + "Don't consider or output modules matching the os cascade in their " + "[build].exclude config" + ) + ) + parser.add_argument( + '--hide-self', + action = 'store_true', + default = False, + help = "Don't include projects listed in in the output", + ) + parser.add_argument( + '--hide-jw-pkg', + action = 'store_true', + default = False, + help = "Don't include packages from requires.jw in the output", + ) + parser.add_argument( + '--quote', + action = 'store_true', + default = False, + help = 'Put double quotes around each listed dependency', + ) async def _run(self, args: Namespace) -> None: return self.print_pkg_relations(self.relation, args) diff --git a/src/python/jw/pkg/cmds/projects/lib/pkg_relations.py b/src/python/jw/pkg/cmds/projects/lib/pkg_relations.py new file mode 100644 index 00000000..20990632 --- /dev/null +++ b/src/python/jw/pkg/cmds/projects/lib/pkg_relations.py @@ -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