cmds.distro: Move all modules to lib

Functions abstracting the distribution are not only needed in the
context of the distro subcommand, but also by other code, so make the
bulk of the code abstracting the distribution available in some place
more universally useful than below cmds.distro.

This commit leaves the source files mostly unchanged. They are only
patched to fix import paths, so that functionality is preserved.
Refactoring the code from command-line API to library API will be
done by the next commit.

Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-03-05 11:38:29 +01:00
commit 7e7cee6d11
45 changed files with 44 additions and 42 deletions

View file

@ -2,13 +2,15 @@
import os, importlib
from ...lib.log import *
from ...lib.distros.Util import Util
from ..Cmd import Cmd as Base
from ..CmdDistro import CmdDistro
from .backend.Util import Util
class Cmd(Base): # export
from .backend.Backend import Backend
from ...lib.distros.Backend import Backend
def __init__(self, parent: CmdDistro, name: str, help: str) -> None:
super().__init__(parent, name, help)
@ -35,16 +37,16 @@ class Cmd(Base): # export
backend_id = 'redhat'
case 'opensuse' | 'suse':
backend_id = 'suse'
self.__backend_path = (
os.path.splitext(__name__)[0]
+ '.backend.'
+ backend_id
+ '.'
)
self.__backend_path = 'jw.pkg.lib.distros.' + backend_id + '.'
return self.__backend_path
def _instantiate(self, name: str, *args, **kwargs):
module = importlib.import_module(self._backend_path + name)
module_path = self._backend_path + name
try:
module = importlib.import_module(module_path)
except Exception as e:
log(ERR, f'Failed to import module {module_path} ({str(e)})')
raise
cls = getattr(module, name)
return cls(self, *args, **kwargs)

View file

@ -3,10 +3,10 @@
from argparse import Namespace, ArgumentParser
import re
from ...lib.Package import Package
from ...lib.distros.BeSelect import BeSelect
from ..CmdDistro import CmdDistro
from .lib.Package import Package
from .Cmd import Cmd
from .backend.BeSelect import BeSelect
class CmdSelect(Cmd): # export

View file

@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from ....lib.util import run_sudo
if TYPE_CHECKING:
from ..Cmd import Cmd
class Backend:
def __init__(self, parent: Cmd):
self.__parent = parent
async def _sudo(self, *args, **kwargs):
return await run_sudo(*args, interactive=self.interactive, **kwargs)
@property
def util(self):
return self.__parent.util
@property
def parent(self):
return self.__parent
@property
def interactive(self) -> bool:
return self.__parent.interactive

View file

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
import abc
from argparse import Namespace
from .Backend import Backend as Base
from ..CmdDelete import CmdDelete as Parent
class BeDelete(Base):
def __init__(self, parent: Parent):
super().__init__(parent)
@abc.abstractmethod
async def run(self, args: Namespace) -> None:
pass

View file

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
import abc
from argparse import Namespace
from .Backend import Backend as Base
from ..CmdDup import CmdDup as Parent
class BeDup(Base):
def __init__(self, parent: Parent):
super().__init__(parent)
@abc.abstractmethod
async def run(self, args: Namespace) -> None:
pass

View file

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
import abc
from argparse import Namespace
from .Backend import Backend as Base
from ..CmdInstall import CmdInstall as Parent
class BeInstall(Base):
def __init__(self, parent: Parent):
super().__init__(parent)
@abc.abstractmethod
async def run(self, args: Namespace) -> None:
pass

View file

@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import Iterable, TYPE_CHECKING
import abc
from ..lib.Package import Package
from .Backend import Backend as Base
if TYPE_CHECKING:
from ..CmdPkg import CmdPkg as Parent
class BePkg(Base):
def __init__(self, parent: Parent):
super().__init__(parent)
async def files(self, name: str) -> Iterable[str]:
return await self._files(name)
@abc.abstractmethod
async def _files(self, name: str) -> Iterable[str]:
pass
async def meta_data(self, names: Iterable[str]) -> Iterable[Package]:
return await self._meta_data(names)
@abc.abstractmethod
async def _meta_data(self, names: Iterable[str]) -> Iterable[Package]:
pass

View file

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
import abc
from argparse import Namespace
from .Backend import Backend as Base
from ..CmdRebootRequired import CmdRebootRequired as Parent
class BeRebootRequired(Base):
def __init__(self, parent: Parent):
super().__init__(parent)
@abc.abstractmethod
async def run(self, args: Namespace) -> None:
pass

View file

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
import abc
from argparse import Namespace
from .Backend import Backend as Base
from ..CmdRefresh import CmdRefresh as Parent
class BeRefresh(Base):
def __init__(self, parent: Parent):
super().__init__(parent)
@abc.abstractmethod
async def run(self, args: Namespace) -> None:
pass

View file

@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
import abc
from typing import Iterable
from .Backend import Backend as Base
if TYPE_CHECKING:
from ..CmdSelect import CmdSelect as Parent
from .Package import Package
class BeSelect(Base):
def __init__(self, parent: Parent):
super().__init__(parent)
@property
async def all_installed_packages(self) -> Iterable[Package]:
return await self._all_installed_packages()
@abc.abstractmethod
async def _all_installed_packages(self) -> Iterable[Package]:
pass

View file

@ -1,4 +0,0 @@
TOPDIR = ../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-mod.mk

View file

@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from ....lib.util import run_sudo
if TYPE_CHECKING:
from ..Cmd import Cmd
class Util:
def __init__(self, parent: Cmd):
self.__parent = parent
async def _sudo(self, *args, **kwargs):
return await run_sudo(*args, interactive=self.interactive, **kwargs)
@property
def parent(self):
return self.__parent
@property
def interactive(self) -> bool:
return self.__parent.interactive

View file

@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeDup import BeDup as Base
class Dup(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
pm_args = ['-Su']
if args.download_only:
pm_args.append('-w')
return await self.util.pacman(pm_args)

View file

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeInstall import BeInstall as Base
class Install(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
if args.only_update:
raise NotImplementedError('--only-update is not yet implemented for pacman')
pacman_args = ['-S', '--needed']
pacman_args.extend(args.packages)
await self.util.pacman(*pacman_args)

View file

@ -1,4 +0,0 @@
TOPDIR = ../../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-mod.mk

View file

@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeRefresh import BeRefresh as Base
class Refresh(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
raise NotImplementedError('distro refresh is not yet implemented for Arch-like distributions')

View file

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
from ...Cmd import Cmd
from ..Util import Util as Base
class Util(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def pacman(self, *args):
cmd = ['/usr/bin/pacman']
if not self.interactive:
cmd.extend(['--noconfirm'])
cmd.extend(args)
return await self._sudo(cmd)

View file

@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeDelete import BeDelete as Base
class Delete(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
return await self.util.dpkg(['-P', *args.names], sudo=True)

View file

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeDup import BeDup as Base
class Dup(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
apt_get_args: list[str] = []
if args.download_only:
apt_get_args.append('--download-only')
apt_get_args.append('upgrade')
return await self.util.apt_get(apt_get_args)

View file

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeInstall import BeInstall as Base
class Install(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
apt_get_args = ['install']
if args.only_update:
apt_get_args.append('--only-upgrade')
apt_get_args.append('--no-install-recommends')
apt_get_args.extend(args.packages)
return await self.util.apt_get(apt_get_args)

View file

@ -1,4 +0,0 @@
TOPDIR = ../../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-mod.mk

View file

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Iterable
from argparse import Namespace
from ...lib.Package import Package
from ...lib.dpkg import list_files, query_packages
from ...Cmd import Cmd
from ..BePkg import BePkg as Base
class Pkg(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def _files(self, name: str) -> Iterable[str]:
return await list_files(name)
async def _meta_data(self, names: Iterable[str]) -> Iterable[Package]:
return await query_packages(names)

View file

@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
import os
from argparse import Namespace
from .....lib.log import *
from ...Cmd import Cmd
from ..BeRebootRequired import BeRebootRequired as Base
class RebootRequired(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
reboot_required = '/run/reboot_required'
if os.path.exists(reboot_required):
if args.verbose:
log(NOTICE, f'Yes. {reboot_required} exists.')
required_pkgs = '/run/reboot-required.pkgs'
if os.path.exists(required_pkgs):
with open(required_pkgs, 'r') as f:
content = f.read()
print(f'-- From {required_pkgs}:')
print(content.strip())
return 1
if args.verbose:
log(NOTICE, f'No. {reboot_required} doesn\'t exist.')
return 0

View file

@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeRefresh import BeRefresh as Base
class Refresh(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
return await self.util.apt_get(['update'])

View file

@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Iterable
from argparse import Namespace
from ...lib.Package import Package
from ...lib.dpkg import query_packages
from ...Cmd import Cmd
from ..BeSelect import BeSelect as Base
class Select(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def _all_installed_packages(self) -> Iterable[Package]:
return await query_packages()

View file

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
from ...Cmd import Cmd
from ...lib.dpkg import run_dpkg
from ..Util import Util as Base
class Util(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def apt_get(self, args: list[str]):
cmd = ['/usr/bin/apt-get']
mod_env = None
if not self.interactive:
cmd.extend(['--yes', '--quiet'])
mod_env = { 'DEBIAN_FRONTEND': 'noninteractive' }
cmd.extend(args)
return await self._sudo(cmd, mod_env=mod_env)
async def dpkg(self, *args, **kwargs):
return await run_dpkg(*args, **kwargs)

View file

@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeDup import BeDup as Base
class Dup(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
yum_args: list[str] = ['update']
if args.download_only:
yum_args.append('--downloadonly')
return await self.yum(yum_args)

View file

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeInstall import BeInstall as Base
class Install(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
yum_args = ['update' if args.only_update else 'install']
if not self.interactive:
yum_args.append['-y']
yum_args.extend(args.packages)
return await self.util.yum(*yum_args)

View file

@ -1,4 +0,0 @@
TOPDIR = ../../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-mod.mk

View file

@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeRefresh import BeRefresh as Base
class Refresh(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
await self.util.yum('clean', 'expire-cache')
await self.util.yum('makecache')

View file

@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
from ...Cmd import Cmd
from ..Util import Util as Base
class Util(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def yum(self, *args):
cmd = ['/usr/bin/yum']
cmd.extend(args)
return await self._sudo(cmd)

View file

@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeDelete import BeDelete as Base
class Delete(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
return await self.util.rpm(['-e', *args.names], sudo=True)

View file

@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeDup import BeDup as Base
class Dup(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
zypper_args = ['dup', '--force-resolution', '--auto-agree-with-licenses']
if args.download_only:
zypper_args.append('--download-only')
return await self.util.zypper(zypper_args)

View file

@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeInstall import BeInstall as Base
class Install(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
zypper_cmd = 'update' if args.only_update else 'install'
return await self.util.zypper([zypper_cmd, *args.packages])

View file

@ -1,4 +0,0 @@
TOPDIR = ../../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-mod.mk

View file

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Iterable
from argparse import Namespace
from ...lib.Package import Package
from ...lib.rpm import list_files, query_packages
from ...Cmd import Cmd
from ..BePkg import BePkg as Base
class Pkg(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def _files(self, name: str) -> Iterable[str]:
return await list_files(name)
async def _meta_data(self, names: Iterable[str]) -> Iterable[Package]:
return await query_packages(names)

View file

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeRebootRequired import BeRebootRequired as Base
class RebootRequired(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
opts = []
if not args.verbose:
pass
#opts.append('--quiet')
opts.append('needs-rebooting')
stdout, stderr, ret = await self.util.zypper(opts, sudo=False, verbose=args.verbose)
if ret != 0:
return 1
return 0

View file

@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
from argparse import Namespace
from ...Cmd import Cmd
from ..BeRefresh import BeRefresh as Base
class Refresh(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def run(self, args: Namespace):
return await self.util.zypper(['refresh'])

View file

@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Iterable
from argparse import Namespace
from ...lib.Package import Package
from ...lib.rpm import query_packages
from ...Cmd import Cmd
from ..BeSelect import BeSelect as Base
class Select(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def _all_installed_packages(self) -> Iterable[Package]:
return await query_packages()

View file

@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
from .....lib.util import run_cmd
from ...Cmd import Cmd
from ..Util import Util as Base
from ...lib.rpm import run_rpm
class Util(Base):
def __init__(self, parent: Cmd):
super().__init__(parent)
async def zypper(self, args: list[str], verbose: bool=False, sudo: bool=True):
cmd = ['/usr/bin/zypper']
if not self.interactive:
cmd.extend(['--non-interactive', '--gpg-auto-import-keys', '--no-gpg-checks'])
cmd.extend(args)
if sudo:
# Run sudo --login in case /etc/profile modifies ZYPP_CONF
return await self._sudo(cmd, opts=['--login'], verbose=verbose)
return await run_cmd(cmd, verbose=verbose)
async def rpm(self, *args, **kwargs):
return await run_rpm(*args, **kwargs)

View file

@ -1,4 +0,0 @@
TOPDIR = ../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-mod.mk

View file

@ -1,51 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Any
meta_tags = [
"name",
"vendor",
"packager",
"url",
"maintainer",
]
class Package:
name: str = None
vendor: str|None = None
packager: str|None = None
url: str|None = None
maintainer: str|None = None
@classmethod
def parse_spec_str(cls, spec: str, delimiter='|'):
tags = spec.split(delimiter)
if len(tags) != 5:
raise ValueError(f'Invalid package spec string \"{spec}\"')
return cls(name=tags[0], vendor=tags[1], packager=tags[2], url=tags[3], maintainer=tags[4])
@classmethod
def parse_specs_str(cls, specs: str, delimiter='|'):
ret: list[Package] = []
for spec in specs.splitlines():
ret.append(cls.parse_spec_str(spec))
return ret
@classmethod
def order_tags(cls, mapping: dict[str, Any]):
ret: dict[str, Any] = {}
for tag in meta_tags:
ret[tag] = mapping.get(tag, '')
return ret
def __init__(self, name: str, vendor: str|None=None, packager: str|None=None, url: str|None=None, maintainer: str|None=None):
self.name = name
self.vendor = vendor
self.packager = packager
self.url = url
self.maintainer = maintainer
def __repr__(self) -> str:
global meta_tags
return '\n'.join([f'{key:<15}: {getattr(self, key)}' for key in meta_tags])

View file

@ -1,45 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Iterable
from ....lib.util import run_cmd, run_sudo
from .Package import Package, meta_tags
_meta_map: dict[str, str]|None = None
def meta_map():
global _meta_map
if _meta_map is None:
_meta_map = Package.order_tags({
'name': 'binary:Package',
'vendor': None, # deb doesn't have vendor field
'packager': None, # -- packager --
'url': 'Homepage',
'maintainer': 'Maintainer',
})
return _meta_map
async def run_dpkg(args: list[str], sudo: bool=False): # export
cmd = ['/usr/bin/dpkg']
cmd.extend(args)
if sudo:
return await run_sudo(cmd)
return await run_cmd(cmd)
async def run_dpkg_query(args: list[str], sudo: bool=False): # export
cmd = ['/usr/bin/dpkg-query']
cmd.extend(args)
if sudo:
return await run_sudo(cmd)
return await run_cmd(cmd)
async def query_packages(names: Iterable[str] = []) -> Iterable[Package]: # export
fmt_str = '|'.join([(f'${{{tag}}}' if tag else '') for tag in meta_map().values()]) + r'\n'
# dpkg-query -W -f='${binary:Package}|${Maintainer}| ... \n'
specs, stderr, status = await run_dpkg_query(['-W', '-f=' + fmt_str, *names], sudo=False)
return Package.parse_specs_str(specs)
async def list_files(pkg: str) -> list[str]: # export
file_list_str, stderr, status = await run_dpkg(['-L', pkg], sudo=False)
return file_list_str.splitlines()

View file

@ -1,40 +0,0 @@
# -*- coding: utf-8 -*-
from typing import Iterable
from ....lib.util import run_cmd, run_sudo
from .Package import Package, meta_tags
_meta_map: dict[str, str]|None = None
def meta_map():
global _meta_map
if _meta_map is None:
_meta_map = Package.order_tags({
'name': 'Name',
'vendor': 'Vendor',
'packager': 'Packager',
'url': 'URL',
'maintainer': None, # RPM doesn't have a maintainer field
})
return _meta_map
async def run_rpm(args: list[str], sudo: bool=False, **kwargs): # export
cmd = ['/usr/bin/rpm']
cmd.extend(args)
if sudo:
return await run_sudo(cmd, **kwargs)
return await run_cmd(cmd, **kwargs)
async def query_packages(names: Iterable[str] = []) -> Iterable[Package]: # export
fmt_str = '|'.join([(f'%{{{tag}}}' if tag else '') for tag in meta_map().values()]) + r'\n'
opts = ['-q', '--queryformat', fmt_str]
if not names:
opts.append('-a')
specs, stderr, status = await run_rpm([*opts, *names], throw=True, sudo=False)
return Package.parse_specs_str(specs)
async def list_files(pkg: str) -> list[str]: # export
file_list_str, stderr, status = await run_rpm(['-ql', pkg], throw=True, sudo=False)
return file_list_str.splitlines()

View file

@ -7,7 +7,7 @@ from ..CmdPkg import CmdPkg as Parent
class Cmd(Base): # export
from ..backend.Backend import Backend
from ....lib.distros.Backend import Backend
def __init__(self, parent: Parent, name: str, help: str) -> None:
super().__init__(parent, name, help)