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

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from ..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

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
import abc
from argparse import Namespace
from .Backend import Backend as Base
from ..Cmd import Cmd 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

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
import abc
from argparse import Namespace
from .Backend import Backend as Base
from ..Cmd import Cmd 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

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
import abc
from argparse import Namespace
from .Backend import Backend as Base
from ..Cmd import Cmd 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

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import Iterable, TYPE_CHECKING
import abc
from ..Package import Package
from .Backend import Backend as Base
if TYPE_CHECKING:
from ..Cmd import Cmd 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

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
import abc
from argparse import Namespace
from .Backend import Backend as Base
from ..Cmd import Cmd 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

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
import abc
from argparse import Namespace
from .Backend import Backend as Base
from ..Cmd import Cmd 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

@ -0,0 +1,26 @@
# -*- 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 ..Cmd import Cmd 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

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

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from ..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

@ -0,0 +1,17 @@
# -*- 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

@ -0,0 +1,18 @@
# -*- 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

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

View file

@ -0,0 +1,14 @@
# -*- 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

@ -0,0 +1,16 @@
# -*- 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

@ -0,0 +1,14 @@
# -*- 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

@ -0,0 +1,18 @@
# -*- 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

@ -0,0 +1,19 @@
# -*- 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

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

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from typing import Iterable
from argparse import Namespace
from ...Package import Package
from ...pm.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

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
import os
from argparse import Namespace
from ...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

@ -0,0 +1,14 @@
# -*- 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

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from typing import Iterable
from argparse import Namespace
from ...Package import Package
from ...pm.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

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from ...Cmd import Cmd
from ...pm.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

@ -0,0 +1,17 @@
# -*- 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

@ -0,0 +1,18 @@
# -*- 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

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

View file

@ -0,0 +1,15 @@
# -*- 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

@ -0,0 +1,14 @@
# -*- 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

@ -0,0 +1,14 @@
# -*- 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

@ -0,0 +1,17 @@
# -*- 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

@ -0,0 +1,15 @@
# -*- 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

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

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from typing import Iterable
from argparse import Namespace
from ...Package import Package
from ...pm.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

@ -0,0 +1,22 @@
# -*- 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

@ -0,0 +1,14 @@
# -*- 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

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from typing import Iterable
from argparse import Namespace
from ...Package import Package
from ...pm.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

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from ...util import run_cmd
from ...Cmd import Cmd
from ..Util import Util as Base
from ...pm.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)