# -*- coding: utf-8 -*- 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 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, flavours = args.flavours.split(','), seed_pkgs = args.modules, 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('-', '_')], recursive = args.recursive, dont_expand_version_macros = args.dont_expand_version_macros, ignore = set(args.ignore.split(',')), quote = args.quote, skip_excluded = args.skip_excluded, hide_self = args.hide_self, hide_jw_pkg = args.hide_jw_pkg, ) ) 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) 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') async def _run(self, args: Namespace) -> None: return self.print_pkg_relations(self.relation, args)