jw-pkg/src/python/jw/pkg/cmds/projects/BaseCmdPkgRelations.py
Jan Lindemann aa7275f426 App.distro_xxx: Move properties to Distro.xxx
Commit a19679fec reverted the first attempt to make AsyncSSH reuse
one connection during an instance lifetime. That failed because a lot
of distribution-specific properties were filled in a new event loop
thread started by AsyncRunner, and AsyncSSH didn't like that.

This commit is the first part of the solution: Move those properties
from the App class to the Distro class, and load the Distro class
in an async loader. As soon as it's instantiated, it can provide all
its properties without cluttering the code with async keywords.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-19 21:00:21 +02:00

207 lines
10 KiB
Python

# -*- 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 <modules> 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)