From 11fb8f9f744d8286b9760d65b36db248ba2d565f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 10:31:53 +0000 Subject: [PATCH] script/build-with-container: refactor LinuxDistro to use instances instead of subclasses - Add LinuxDistro.PackageManager enum replacing PKG_DNF/PKG_APT/PKG_ZYPPER string constants - Add LinuxDistro.Release class with full comparison operator support - Change LinuxDistro to support instance-based concrete distros - Add _make_name/_make_image/_make_aliases to family classes for auto-guessing - Introduce CentOSLinuxDistro, RockyLinuxDistro, UbuntuLinuxDistro family classes - Update DebianLinuxDistro with codename-based auto-guessing - Replace all concrete subclasses with instance instantiation calls - Remove global concrete distro class objects - Remove unsupported decorator and related globals - Update parse_cli to use LinuxDistro["centos9"].name instead of CentOS9.name - Add comparison operators to LinuxDistro for release-based ordering - Remove unnecessary comments per problem statement Co-authored-by: epuertat <37327689+epuertat@users.noreply.github.com> --- src/script/build-with-container.py | 379 ++++++++++++----------------- 1 file changed, 162 insertions(+), 217 deletions(-) diff --git a/src/script/build-with-container.py b/src/script/build-with-container.py index 09d31230bd0..d1744feee60 100755 --- a/src/script/build-with-container.py +++ b/src/script/build-with-container.py @@ -100,120 +100,97 @@ except ImportError: ftcache = lambda f: f -_UNSUPPORTED_DISTROS: set = set() -_UNSUPPORTED_COMBINATIONS: dict = {} - - -def unsupported(cls=None, *, ceph_versions=()): - """Decorator to mark a distro (or distro + Ceph-version combination) as unsupported. - - Without arguments the distro is considered entirely unsupported:: - - @unsupported - class OldDistro(ELLinuxDistro, name="old8", ...): pass - - With *ceph_versions* the distro is unsupported only for those releases:: - - @unsupported(ceph_versions=("reef", "squid")) - class SomeDistro(ELLinuxDistro, name="some9", ...): pass - - Use ``LinuxDistro["name"].is_supported()`` to query support status. - """ - def decorator(c): - if ceph_versions: - _UNSUPPORTED_COMBINATIONS.setdefault(c, set()).update(ceph_versions) - else: - _UNSUPPORTED_DISTROS.add(c) - return c - - if cls is not None: - # called as @unsupported (no parentheses) - return decorator(cls) - # called as @unsupported(...) (with parentheses) - return decorator - - class LinuxDistro: - """Base class for Linux distro definitions used by the build system. - - Each concrete distro version is defined as a subclass that passes - metadata through class parameters. The subclass init hook automatically - registers every concrete distro (one that supplies a *name*) so that it - can be looked up later via the ``LinuxDistro["name"]`` syntax. - - Intermediate/family classes (e.g. ``ELLinuxDistro``) are defined without - a *name* and are therefore **not** registered; they only propagate shared - attributes (such as *pkg_manager*) to their concrete descendants. - - Example – adding a new distro:: + class PackageManager(enum.Enum): + DNF = "dnf" + APT = "apt" + ZYPPER = "zypper" - class CentOS99(ELLinuxDistro, - name="centos99", - default_image="quay.io/centos/centos:stream99", - aliases=("centos99stream",)): - pass - - Lookup examples:: + class Release: + def __init__(self, major, minor=0, build=0): + self.major = major + self.minor = minor + self.build = build - LinuxDistro["centos9"] # -> CentOS9 class - LinuxDistro["centos9stream"] # -> CentOS9 class (alias) - LinuxDistro["centos9"].default_image - LinuxDistro["centos9"].uses_dnf() - """ + def __str__(self): + if self.minor == 0 and self.build == 0: + return str(self.major) + if self.build == 0: + return f"{self.major}.{self.minor}" + return f"{self.major}.{self.minor}.{self.build}" + + def __repr__(self): + return f"Release({self.major}, {self.minor}, {self.build})" + + def _tuple(self): + return (self.major, self.minor, self.build) + + def __eq__(self, other): + if not isinstance(other, LinuxDistro.Release): + return NotImplemented + return self._tuple() == other._tuple() + + def __lt__(self, other): + if not isinstance(other, LinuxDistro.Release): + return NotImplemented + return self._tuple() < other._tuple() + + def __le__(self, other): + if not isinstance(other, LinuxDistro.Release): + return NotImplemented + return self._tuple() <= other._tuple() + + def __gt__(self, other): + if not isinstance(other, LinuxDistro.Release): + return NotImplemented + return self._tuple() > other._tuple() + + def __ge__(self, other): + if not isinstance(other, LinuxDistro.Release): + return NotImplemented + return self._tuple() >= other._tuple() + + def __hash__(self): + return hash(self._tuple()) - # Shared registry populated by __init_subclass__ for every concrete distro. _registry: dict = {} - _concrete: set = set() + _all: set = set() - # Package-manager sentinel constants – use these instead of bare strings. - PKG_DNF = "dnf" - PKG_APT = "apt" - PKG_ZYPPER = "zypper" - - # Class-level defaults; concrete distros override these via __init_subclass__. - name: str = "" - default_image: str = "" - pkg_manager: str = "" + pkg_manager = None python: str = "python3" - aliases: tuple = () - - def __init_subclass__( - cls, - name=None, - default_image=None, - aliases=(), - pkg_manager=None, - python=None, - **kwargs, - ): + + def __init_subclass__(cls, pkg_manager=None, python=None, **kwargs): super().__init_subclass__(**kwargs) - # Propagate family-level traits so intermediate classes can set them - # for all their descendants without requiring repetition. if pkg_manager is not None: cls.pkg_manager = pkg_manager if python is not None: cls.python = python - # Register only concrete distros (those that declare a canonical name). - if name is not None: - if not cls.pkg_manager: - raise TypeError( - f"Concrete distro {cls.__name__!r} has no pkg_manager set." - " Inherit from a family class (e.g. ELLinuxDistro) or" - " pass pkg_manager= explicitly." - ) - cls.name = name - if default_image is not None: - cls.default_image = default_image - cls.aliases = tuple(aliases) - for alias in (name,) + cls.aliases: - LinuxDistro._registry[alias] = cls - LinuxDistro._concrete.add(cls) - def __class_getitem__(cls, name): - """Look up a concrete distro class by canonical name or alias. + def __init__(self, release, name=None, default_image=None, aliases=None): + if not self.pkg_manager: + raise TypeError( + f"{type(self).__name__!r} has no pkg_manager set." + ) + if isinstance(release, tuple): + release = LinuxDistro.Release(*release) + self.release = release + self.name = name or self._make_name(release) + self.default_image = default_image or self._make_image(release) + self.aliases = tuple(aliases) if aliases is not None else self._make_aliases(release) + for alias in (self.name,) + self.aliases: + LinuxDistro._registry[alias] = self + LinuxDistro._all.add(self) - Raises ``KeyError`` for unknown names. - """ + def _make_name(self, release): + raise NotImplementedError + + def _make_image(self, release): + raise NotImplementedError + + def _make_aliases(self, release): + return () + + def __class_getitem__(cls, name): try: return LinuxDistro._registry[name] except KeyError: @@ -224,168 +201,136 @@ class LinuxDistro: @classmethod def all_distros(cls): - """Return a frozenset of all registered concrete distro classes.""" - return frozenset(LinuxDistro._concrete) + return frozenset(LinuxDistro._all) @classmethod def all_aliases(cls): - """Return a dict mapping every alias/name to its distro class.""" return dict(LinuxDistro._registry) @classmethod def from_arg(cls, value): - """Resolve a raw alias string to its canonical distro name. - - Intended for use as ``type=LinuxDistro.from_arg`` with argparse; - raises ``argparse.ArgumentTypeError`` for unknown aliases. - """ try: return LinuxDistro._registry[value].name except KeyError: valid = ", ".join(sorted(LinuxDistro._registry)) - msg = f"unknown distro: {value!r} not in {valid}" - raise argparse.ArgumentTypeError(msg) - - @classmethod - def uses_dnf(cls): - """Return True if this distro uses DNF as its package manager.""" - return cls.pkg_manager == cls.PKG_DNF - - @classmethod - def uses_rpmbuild(cls): - """Return True if this distro uses rpmbuild for package builds.""" - return cls.uses_dnf() - - @classmethod - def is_supported(cls, ceph_version=None): - """Return True if this distro is considered supported. - - When *ceph_version* is provided the check also considers distros that - are only unsupported for specific Ceph release names. - """ - if cls in _UNSUPPORTED_DISTROS: - return False - if ceph_version is not None: - blocked = _UNSUPPORTED_COMBINATIONS.get(cls, set()) - if ceph_version in blocked: - return False - return True + raise argparse.ArgumentTypeError( + f"unknown distro: {value!r} not in {valid}" + ) + def uses_dnf(self): + return self.pkg_manager == LinuxDistro.PackageManager.DNF -# --- Family (intermediate) classes --- + def uses_rpmbuild(self): + return self.uses_dnf() -class ELLinuxDistro(LinuxDistro, pkg_manager=LinuxDistro.PKG_DNF): - """Enterprise Linux distro family. + def __lt__(self, other): + if type(self) is not type(other): + return NotImplemented + return self.release < other.release - Uses DNF for package management and rpmbuild for building RPMs. - Add new EL-family distros by subclassing this and supplying *name*, - *default_image*, and optional *aliases*. - """ + def __eq__(self, other): + if type(self) is not type(other): + return NotImplemented + return self.release == other.release + def __le__(self, other): + if type(self) is not type(other): + return NotImplemented + return self.release <= other.release -class FedoraLinuxDistro(ELLinuxDistro): - """Fedora distro family (a specialisation of ELLinuxDistro).""" - + def __gt__(self, other): + if type(self) is not type(other): + return NotImplemented + return self.release > other.release -class DebianLinuxDistro(LinuxDistro, pkg_manager=LinuxDistro.PKG_APT): - """Debian/Ubuntu distro family. + def __ge__(self, other): + if type(self) is not type(other): + return NotImplemented + return self.release >= other.release - Uses APT for package management and dpkg/debhelper for building packages. - Add new Debian-family distros by subclassing this. - """ + def __hash__(self): + return hash((type(self), self.release)) -# --- Concrete distro classes --- -# Each class registers itself in LinuxDistro._registry when it is defined. - -class CentOS8(ELLinuxDistro, - name="centos8", - default_image="quay.io/centos/centos:stream8"): +class ELLinuxDistro(LinuxDistro, pkg_manager=LinuxDistro.PackageManager.DNF): pass -class CentOS9(ELLinuxDistro, - name="centos9", - default_image="quay.io/centos/centos:stream9", - aliases=("centos9stream",)): - pass +class CentOSLinuxDistro(ELLinuxDistro): + def _make_name(self, release): + return f"centos{release.major}" + def _make_image(self, release): + return f"quay.io/centos/centos:stream{release.major}" -class CentOS10(ELLinuxDistro, - name="centos10", - default_image="quay.io/centos/centos:stream10", - aliases=("centos10stream",)): - pass + def _make_aliases(self, release): + return (f"centos{release.major}stream",) -class Rocky9(ELLinuxDistro, - name="rocky9", - default_image="docker.io/rockylinux/rockylinux:9", - aliases=("rockylinux9",)): - pass +class RockyLinuxDistro(ELLinuxDistro): + def _make_name(self, release): + return f"rocky{release.major}" + def _make_image(self, release): + return f"docker.io/rockylinux/rockylinux:{release.major}" -class Rocky10(ELLinuxDistro, - name="rocky10", - default_image="docker.io/rockylinux/rockylinux:10", - aliases=("rockylinux10",)): - pass + def _make_aliases(self, release): + return (f"rockylinux{release.major}",) -class Fedora41(FedoraLinuxDistro, - name="fedora41", - default_image="registry.fedoraproject.org/fedora:41", - aliases=("fc41",)): - pass +class FedoraLinuxDistro(ELLinuxDistro): + def _make_name(self, release): + return f"fedora{release.major}" + def _make_image(self, release): + return f"registry.fedoraproject.org/fedora:{release.major}" -class Fedora42(FedoraLinuxDistro, - name="fedora42", - default_image="registry.fedoraproject.org/fedora:42", - aliases=("fc42",)): - pass + def _make_aliases(self, release): + return (f"fc{release.major}",) -class Fedora43(FedoraLinuxDistro, - name="fedora43", - default_image="registry.fedoraproject.org/fedora:43", - aliases=("fc43",)): - pass +class DebianLinuxDistro(LinuxDistro, pkg_manager=LinuxDistro.PackageManager.APT): + _codenames: dict = {12: "bookworm", 13: "trixie"} + def _make_name(self, release): + return f"debian{release.major}" -class Ubuntu2004(DebianLinuxDistro, - name="ubuntu20.04", - default_image="docker.io/ubuntu:20.04", - aliases=("ubuntu-focal", "focal")): - pass + def _make_image(self, release): + codename = self._codenames.get(release.major, str(release.major)) + return f"docker.io/debian:{codename}" + def _make_aliases(self, release): + codename = self._codenames.get(release.major, str(release.major)) + return (f"debian-{codename}", codename) -class Ubuntu2204(DebianLinuxDistro, - name="ubuntu22.04", - default_image="docker.io/ubuntu:22.04", - aliases=("ubuntu-jammy", "jammy")): - pass +class UbuntuLinuxDistro(DebianLinuxDistro): + _codenames = {(20, 4): "focal", (22, 4): "jammy", (24, 4): "noble"} -class Ubuntu2404(DebianLinuxDistro, - name="ubuntu24.04", - default_image="docker.io/ubuntu:24.04", - aliases=("ubuntu-noble", "noble")): - pass + def _make_name(self, release): + return f"ubuntu{release.major}.{release.minor:02d}" + def _make_image(self, release): + return f"docker.io/ubuntu:{release.major}.{release.minor:02d}" -class Debian12(DebianLinuxDistro, - name="debian12", - default_image="docker.io/debian:bookworm", - aliases=("debian-bookworm", "bookworm")): - pass + def _make_aliases(self, release): + codename = self._codenames.get((release.major, release.minor)) + return (f"ubuntu-{codename}", codename) if codename else () -class Debian13(DebianLinuxDistro, - name="debian13", - default_image="docker.io/debian:trixie", - aliases=("debian-trixie", "trixie")): - pass +CentOSLinuxDistro(release=(8,)) +CentOSLinuxDistro(release=(9,)) +CentOSLinuxDistro(release=(10,)) +RockyLinuxDistro(release=(9,)) +RockyLinuxDistro(release=(10,)) +FedoraLinuxDistro(release=(41,)) +FedoraLinuxDistro(release=(42,)) +FedoraLinuxDistro(release=(43,)) +UbuntuLinuxDistro(release=(20, 4)) +UbuntuLinuxDistro(release=(22, 4)) +UbuntuLinuxDistro(release=(24, 4)) +DebianLinuxDistro(release=(12,)) +DebianLinuxDistro(release=(13,)) class CommandFailed(Exception): @@ -1314,7 +1259,7 @@ def parse_cli(build_step_names): "-d", choices=LinuxDistro.all_aliases().keys(), type=LinuxDistro.from_arg, - default=CentOS9.name, + default=LinuxDistro["centos9"].name, help="Specify a distro short name", ) g_basic.add_argument( -- 2.47.3