]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
script/build-with-container: refactor LinuxDistro to use instances instead of subclasses copilot/refactor-distrokind-structure 67775/head
authorcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sat, 14 Mar 2026 10:31:53 +0000 (10:31 +0000)
committercopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sat, 14 Mar 2026 10:31:53 +0000 (10:31 +0000)
- 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

index 09d31230bd00261c0bd3bdfb2ff0c9933331525c..d1744feee6079bfd3d1269cf5eedd0fac4482a76 100755 (executable)
@@ -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(