diff --git a/rbinstall/distro_info.py b/rbinstall/distro_info.py
new file mode 100644
index 0000000000000000000000000000000000000000..21f498137c4b4163738c05a62992c0700f4e0ae7
--- /dev/null
+++ b/rbinstall/distro_info.py
@@ -0,0 +1,795 @@
+"""Information on operating systems/distributions and their packages.
+
+Version Added:
+    1.0
+"""
+
+# NOTE: This file must be syntactically compatible with Python 3.7+.
+from __future__ import annotations
+
+import operator
+from typing import Dict, List, Set
+
+from typing_extensions import NotRequired, TypeAlias, TypedDict
+
+from rbinstall.install_methods import InstallMethodType
+from rbinstall.versioning import VersionMatchFunc, match_version
+
+
+class _PackageCandidateMatch(TypedDict):
+    """A match for selecting a candidate package for installation.
+
+    Version Added:
+        1.0
+    """
+
+    #: Any architectures that must be matched.
+    archs: NotRequired[Set[str]]
+
+    #: Any Linux distribution families that must be matched.
+    distro_families: NotRequired[Set[str]]
+
+    #: Any Linux distribution IDs that must be matched.
+    distro_ids: NotRequired[Set[str]]
+
+    #: A callable for determining if a Linux distribution version matches.
+    distro_version: NotRequired[VersionMatchFunc]
+
+    #: A set of flags that must be set from previous matched packages.
+    has_flags: NotRequired[Dict[str, bool]]
+
+    #: A callable for determining if a Review Board version matches.
+    rb_version: NotRequired[VersionMatchFunc]
+
+    #: Any operating systems that must be matched.
+    systems: NotRequired[Set[str]]
+
+
+class _PackageCandidate(TypedDict):
+    """A candidate package for installation.
+
+    Version Added:
+        1.0
+    """
+
+    #: A list of external commands that would be run.
+    commands: NotRequired[List[List[str]]]
+
+    #: The installation method used for installing the package.
+    #:
+    #: If not specified, the system-default installer will be used.
+    install_method: NotRequired[InstallMethodType]
+
+    #: Whether this installation step is allowed to fail.
+    #:
+    #: If allowed to fail, an unsuccessful package installation will not
+    #: cause the overall Review Board installation to fail.
+    allow_fail: NotRequired[bool]
+
+    #: Any conditions that must be matched for installation.
+    match: NotRequired[_PackageCandidateMatch]
+
+    #: A list of packages that would be installed.
+    packages: NotRequired[List[str]]
+
+    #: Flags to set for future package matches.
+    set_flags: NotRequired[Dict[str, bool]]
+
+    #: A list of packages to skip from prior matched candidates.
+    #:
+    #: This is used to discard/skip a package that was selected for
+    #: installation using this installation method from a previous candidate.
+    #: It can be used to specify a corrected package for a more specific
+    #: version of an operating system/distribution.
+    skip_packages: NotRequired[List[str]]
+
+
+_PackageTypeDict: TypeAlias = List[_PackageCandidate]
+_PackageBundleDict: TypeAlias = Dict[str, _PackageTypeDict]
+_Packages: TypeAlias = Dict[str, _PackageBundleDict]
+
+
+#: All packages available for installation across all supported systems.
+#:
+#: Version Added:
+#:     1.0
+PACKAGES: _Packages = {
+    # System packages for installation.
+    #
+    # Some of these (such as xmlsec support) are installed unconditionally,
+    # regardless of any selected featuresets, in order to ease usage of those
+    # modules down the road.
+    'common': {
+        'system': [
+            #############################################
+            # Package management bootstrapping commands #
+            #############################################
+
+            # Amazon Linux 2
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {'amzn'},
+                    'distro_version': match_version(2),
+                },
+                'commands': [
+                    [
+                        # Needed for g++ and friends.
+                        'yum', 'groupinstall', '-y', 'Development Tools',
+                    ],
+                ],
+            },
+
+            # CentOS
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {'centos'},
+                },
+                'commands': [
+                    [
+                        'dnf', 'install', '-y', 'dnf-plugins-core',
+                    ],
+                    [
+                        'dnf', 'config-manager', '--set-enabled', 'crb',
+                    ],
+                    [
+                        'yum', 'install', '-y', 'epel-release',
+                        'epel-next-release',
+                    ],
+                ],
+            },
+
+            # openSUSE
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_families': {'opensuse'},
+                },
+                'commands': [
+                    [
+                        'zypper', 'install', '-y', '-t', 'pattern',
+                        'devel_basis',
+                    ],
+                ],
+            },
+
+            # Red Hat Enterprise Linux 8 (x86)
+            {
+                'match': {
+                    'archs': {'x86_64'},
+                    'systems': {'Linux'},
+                    'distro_ids': {'rhel'},
+                    'distro_version': match_version(8),
+                },
+                'commands': [
+                    [
+                        'subscription-manager', 'repos', '--enable',
+                        'codeready-builder-for-rhel-8-x86_64-rpms',
+                    ],
+                    [
+                        'dnf', 'install', '-y',
+                        ('https://dl.fedoraproject.org/pub/epel/'
+                         'epel-release-latest-8.noarch.rpm'),
+                    ],
+                ],
+            },
+
+            # Red Hat Enterprise Linux 8 (ARM)
+            {
+                'match': {
+                    'archs': {'aarch64'},
+                    'systems': {'Linux'},
+                    'distro_ids': {'rhel'},
+                    'distro_version': match_version(8),
+                },
+                'commands': [
+                    [
+                        'subscription-manager', 'repos', '--enable',
+                        'codeready-builder-for-rhel-8-aarch64-rpms',
+                    ],
+                    [
+                        'dnf', 'install', '-y',
+                        ('https://dl.fedoraproject.org/pub/epel/'
+                         'epel-release-latest-8.noarch.rpm'),
+                    ],
+                ],
+            },
+
+            # Red Hat Enterprise Linux 9 (x86)
+            {
+                'match': {
+                    'archs': {'x86_64'},
+                    'systems': {'Linux'},
+                    'distro_ids': {'rhel'},
+                    'distro_version': match_version(9, op=operator.ge),
+                },
+                'commands': [
+                    [
+                        'subscription-manager', 'repos', '--enable',
+                        'codeready-builder-for-rhel-9-x86_64-rpms',
+                    ],
+                    [
+                        'dnf', 'install', '-y',
+                        ('https://dl.fedoraproject.org/pub/epel/'
+                         'epel-release-latest-9.noarch.rpm'),
+                    ],
+                ],
+            },
+
+            # Red Hat Enterprise Linux 9 (ARM)
+            {
+                'match': {
+                    'archs': {'aarch64'},
+                    'systems': {'Linux'},
+                    'distro_ids': {'rhel'},
+                    'distro_version': match_version(9, op=operator.ge),
+                },
+                'commands': [
+                    [
+                        'subscription-manager', 'repos', '--enable',
+                        'codeready-builder-for-rhel-9-aarch64-rpms',
+                    ],
+                    [
+                        'dnf', 'install', '-y',
+                        ('https://dl.fedoraproject.org/pub/epel/'
+                         'epel-release-latest-9.noarch.rpm'),
+                    ],
+                ],
+            },
+
+            # Rocky Linux 8
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {'rocky'},
+                    'distro_version': match_version(9, op=operator.le),
+                },
+                'commands': [
+                    [
+                        'dnf', 'install', '-y', 'dnf-plugins-core',
+                    ],
+                    [
+                        'yum', 'install', '-y', 'epel-release',
+                    ],
+                ],
+            },
+
+            # Rocky Linux 9
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {'rocky'},
+                    'distro_version': match_version(9, op=operator.ge),
+                },
+                'commands': [
+                    [
+                        'dnf', 'install', '-y', 'dnf-plugins-core',
+                    ],
+                    [
+                        'dnf', 'config-manager', '--set-enabled', 'crb',
+                    ],
+                    [
+                        'yum', 'install', '-y', 'epel-release',
+                    ],
+                ],
+            },
+
+            ############
+            # Packages #
+            ############
+
+            # Arch Linux
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {'arch'},
+                },
+                'install_method': InstallMethodType.PACMAN,
+                'packages': [
+                    'base-devel',
+                    'libffi',
+                    'libxml2',
+                    'openssl',
+                    'perl',
+                    'xmlsec',
+                ],
+                'set_flags': {
+                    'has_xmlsec': True,
+                },
+            },
+
+            # CentOS/Fedora/RHEL/Rocky Linux
+            {
+                # Common rules for all RPM-based distros.
+                #
+                # More specific rules for given distros will follow, as they
+                # don't all share the same packages or naming conventions.
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_families': {
+                        'centos',
+                        'fedora',
+                        'rhel',
+                    },
+                },
+                'install_method': InstallMethodType.YUM,
+                'packages': [
+                    'gcc',
+                    'gcc-c++',
+                    'libffi-devel',
+                    'libxml2-devel',
+                    'make',
+                    'openssl-devel',
+                    'patch',
+                    'perl',
+                    'python3-devel',
+                    'libtool-ltdl-devel',
+                ],
+            },
+
+            # CentOS 9+
+            {
+                'match': {
+                    'systems': {'linux'},
+                    'distro_ids': {'centos'},
+                    'distro_version': match_version(9, op=operator.ge),
+                },
+                'packages': [
+                    'xmlsec1-devel',
+                    'xmlsec1-openssl-devel',
+                ],
+                'set_flags': {
+                    'has_xmlsec': True,
+                },
+            },
+
+            # Fedora
+            {
+                'match': {
+                    'systems': {'linux'},
+                    'distro_ids': {'fedora'},
+                },
+                'packages': [
+                    'xmlsec1-devel',
+                    'xmlsec1-openssl-devel',
+                ],
+                'set_flags': {
+                    'has_xmlsec': True,
+                },
+            },
+
+            # Red Hat Enterprise Linux 9+
+            {
+                'match': {
+                    'systems': {'linux'},
+                    'distro_ids': {'rhel'},
+                    'distro_version': match_version(9, op=operator.ge),
+                },
+                'packages': [
+                    'xmlsec1-devel',
+                    'xmlsec1-openssl-devel',
+                ],
+                'set_flags': {
+                    'has_xmlsec': True,
+                },
+            },
+
+            # Rocky Linux 9+
+            {
+                'match': {
+                    'systems': {'linux'},
+                    'distro_ids': {'rocky'},
+                    'distro_version': match_version(9, op=operator.ge),
+                },
+                'packages': [
+                    'xmlsec1-devel',
+                    'xmlsec1-openssl-devel',
+                ],
+                'set_flags': {
+                    'has_xmlsec': True,
+                },
+            },
+
+            # Debian/Ubuntu
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_families': {
+                        'debian',
+                        'ubuntu',
+                    },
+                },
+                'install_method': InstallMethodType.APT,
+                'packages': [
+                    'build-essential',
+                    'libffi-dev',
+                    'libjpeg-dev',
+                    'libssl-dev',
+                    'libxml2-dev',
+                    'libxmlsec1-dev',
+                    'libxmlsec1-openssl',
+                    'patch',
+                    'pkg-config',
+                    'python3-dev',
+                    'python3-pip',
+                ],
+                'set_flags': {
+                    'has_xmlsec': True,
+                },
+            },
+
+            # openSUSE
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_families': {
+                        'opensuse',
+                    },
+                },
+                'install_method': InstallMethodType.ZYPPER,
+                'packages': [
+                    'gcc-c++',
+                    'libffi-devel',
+                    'libopenssl-devel',
+                    'libxml2-devel',
+                    'python3-devel',
+                    'xmlsec1-devel',
+                    'xmlsec1-openssl-devel',
+                ],
+                'set_flags': {
+                    'has_xmlsec': True,
+                },
+            },
+        ],
+
+        'virtualenv': [
+            {
+                'install_method': InstallMethodType.PIP,
+                'packages': [
+                    'pip',
+                    'setuptools',
+                    'wheel',
+                ],
+            },
+        ],
+    },
+
+    # Django Storages support
+    'django-storages': {
+        'service-integrations': [
+            {
+                'install_method': InstallMethodType.REVIEWBOARD_EXTRA,
+                'packages': [
+                    's3',
+                    'swift',
+                ],
+            },
+        ],
+    },
+
+    # CVS packages.
+    'cvs': {
+        'system': [
+            # Arch Linux
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {
+                        'amzn',
+                        'arch',
+                        'centos',
+                        'debian',
+                        'fedora',
+                        'opensuse',
+                        'rocky',
+                        'ubuntu',
+                    },
+                },
+                'install_method': InstallMethodType.SYSTEM_DEFAULT,
+                'packages': ['cvs'],
+            },
+
+            # Red Hat Enterprise Linux 9+
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {'rhel'},
+                    'distro_version': match_version(9, op=operator.ge),
+                },
+                'install_method': InstallMethodType.SYSTEM_DEFAULT,
+                'packages': ['cvs'],
+            },
+
+            # macOS
+            {
+                'match': {
+                    'systems': {'Darwin'},
+                },
+                'install_method': InstallMethodType.BREW,
+                'packages': ['cvs'],
+            },
+        ],
+    },
+
+    # Git packages.
+    'git': {
+        'system': [
+            {
+                'install_method': InstallMethodType.SYSTEM_DEFAULT,
+                'packages': ['git'],
+            },
+        ],
+    },
+
+    # LDAP packages.
+    'ldap': {
+        'service-integrations': [
+            {
+                'install_method': InstallMethodType.REVIEWBOARD_EXTRA,
+                'packages': ['ldap'],
+            },
+        ],
+    },
+
+    # Memcached packages.
+    'memcached': {
+        'system': [
+            # Common
+            {
+                'match': {
+                    'systems': {
+                        'Darwin',
+                        'Linux',
+                    },
+                },
+                'install_method': InstallMethodType.SYSTEM_DEFAULT,
+                'packages': ['memcached'],
+            },
+        ],
+    },
+
+    # Mercurial packages.
+    'mercurial': {
+        'service-integrations': [
+            {
+                'install_method': InstallMethodType.REVIEWBOARD_EXTRA,
+                'packages': ['mercurial'],
+            },
+        ],
+    },
+
+    # MySQL packages.
+    'mysql': {
+        'system': [
+            # Amazon Linux/CentOS/Fedora/RHEL
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_families': {
+                        'centos',
+                        'fedora',
+                        'rhel',
+                        'rocky',
+                    },
+                },
+                'install_method': InstallMethodType.YUM,
+                'packages': [
+                    'mariadb-connector-c-devel',
+                ],
+            },
+
+            # Amazon Linux 2
+            {
+                # This version uses a different package for the MySQL devel
+                # support.
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {'amzn'},
+                    'distro_version': match_version(2),
+                },
+                'install_method': InstallMethodType.YUM,
+                'skip_packages': [
+                    'mariadb-connector-c-devel',
+                ],
+                'packages': [
+                    'mariadb-devel',
+                ],
+            },
+
+            # Arch Linux
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {'arch'},
+                },
+                'install_method': InstallMethodType.PACMAN,
+                'packages': [
+                    'mariadb-libs',
+                ],
+            },
+
+            # Debian
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {'debian'},
+                },
+                'install_method': InstallMethodType.APT,
+                'packages': [
+                    'libmariadb-dev',
+                ],
+            },
+
+            # openSUSE
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_families': {'opensuse'},
+                },
+                'install_method': InstallMethodType.ZYPPER,
+                'packages': [
+                    'libmariadb-devel',
+                ],
+            },
+
+            # Ubuntu
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {'ubuntu'},
+                },
+                'install_method': InstallMethodType.APT,
+                'packages': [
+                    'libmysqlclient-dev',
+                ],
+            },
+
+            # macOS
+            {
+                'match': {
+                    'systems': {'Darwin'},
+                },
+                'install_method': InstallMethodType.BREW,
+                'packages': [
+                    'mysql',
+                ],
+            },
+        ],
+
+        'service-integrations': [
+            {
+                'install_method': InstallMethodType.REVIEWBOARD_EXTRA,
+                'packages': ['mysql'],
+            },
+        ],
+    },
+
+    # Perforce packages.
+    'perforce': {
+        'service-integrations': [
+            {
+                # Common
+                'allow_fail': True,
+                'match': {
+                    'archs': {'x86_64'},
+                },
+                'install_method': InstallMethodType.REVIEWBOARD_EXTRA,
+                'packages': ['p4'],
+            },
+        ],
+    },
+
+    # Postgres packages.
+    'postgres': {
+        'service-integrations': [
+            {
+                # Common
+                'install_method': InstallMethodType.REVIEWBOARD_EXTRA,
+                'packages': ['postgres']
+            },
+        ],
+    },
+
+    # SAML packages.
+    'saml': {
+        'service-integrations': [
+            {
+                'match': {
+                    'has_flags': {
+                        'has_xmlsec': True,
+                    },
+                    'rb_version': match_version(6, 0, op=operator.ge),
+                },
+                'install_method': InstallMethodType.REVIEWBOARD_EXTRA,
+                'packages': ['saml'],
+            },
+        ],
+    },
+
+    # Subversion packages.
+    'subversion': {
+        'system': [
+            # Arch Linux
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_ids': {'arch'},
+                },
+                'install_method': InstallMethodType.PACMAN,
+                'packages': [
+                    'subversion',
+                ],
+            },
+
+            # Centos/Fedora/RHEL
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_families': {
+                        'centos',
+                        'fedora',
+                        'rhel',
+                    },
+                },
+                'install_method': InstallMethodType.YUM,
+                'packages': [
+                    'subversion',
+                    'subversion-devel',
+                ],
+            },
+
+            # Debian/Ubuntu
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_families': {
+                        'debian',
+                        'ubuntu',
+                    },
+                },
+                'install_method': InstallMethodType.APT,
+                'packages': [
+                    'subversion',
+                    'libsvn-dev',
+                ],
+            },
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_families': {
+                        'debian',
+                        'ubuntu',
+                    },
+                },
+                'install_method': InstallMethodType.APT_BUILD_DEP,
+                'packages': [
+                    'python3-svn',
+                ],
+            },
+
+            # openSUSE
+            {
+                'match': {
+                    'systems': {'Linux'},
+                    'distro_families': {'opensuse'},
+                },
+                'install_method': InstallMethodType.ZYPPER,
+                'packages': [
+                    'subversion',
+                    'subversion-devel',
+                ],
+            },
+        ],
+
+        'service-integrations': [
+            # Common
+            {
+                'install_method': InstallMethodType.REMOTE_PYSCRIPT,
+                'packages': [
+                    'https://pysvn.reviewboard.org',
+                ],
+            },
+        ],
+    },
+}
diff --git a/rbinstall/install_steps.py b/rbinstall/install_steps.py
new file mode 100644
index 0000000000000000000000000000000000000000..a005ec08024c6f72994a52f3511ecbd73622d6c0
--- /dev/null
+++ b/rbinstall/install_steps.py
@@ -0,0 +1,368 @@
+"""Steps for installing Review Board.
+
+Version Added:
+    1.0
+"""
+
+# NOTE: This file must be syntactically compatible with Python 3.7+.
+from __future__ import annotations
+
+from gettext import gettext as _
+from itertools import chain
+from typing import Dict, Generic, List, Set, TYPE_CHECKING, TypeVar, Tuple
+
+from typing_extensions import NotRequired, TypeAlias, TypedDict
+
+from rbinstall.distro_info import PACKAGES
+from rbinstall.install_methods import (COMMON_INSTALL_METHODS,
+                                       InstallMethodType)
+from rbinstall.versioning import parse_version
+
+if TYPE_CHECKING:
+    from rbinstall.state import InstallState
+
+
+_T = TypeVar('_T')
+
+
+class _InstallStep(Generic[_T], TypedDict):
+    """An installation step to execute.
+
+    Version Added:
+        1.0
+    """
+
+    #: The installation method used.
+    install_method: InstallMethodType
+
+    #: The name of this step, for display purposes.
+    name: str
+
+    #: Whether this installation step is allowed to fail.
+    #:
+    #: If allowed to fail, an unsuccessful package installation will not
+    #: cause the overall Review Board installation to fail.
+    allow_fail: NotRequired[bool]
+
+    #: The method-specific state to execute for this step.
+    state: NotRequired[_T]
+
+
+InstallSteps: TypeAlias = List[_InstallStep]
+
+
+def get_install_package_steps(
+    install_state: InstallState,
+    *,
+    categories: List[str],
+    package_type: str,
+    description: str = '',
+) -> InstallSteps:
+    """Return steps for installing some packages for the target system.
+
+    This will go through the packages available for installation, returning
+    installation steps for any entries that match the criteria for the target
+    system.
+
+    Version Added:
+        1.0
+
+    Args:
+        install_state (rbinstall.state.InstallState):
+            The state for the installation.
+
+        categories (list of str):
+            The list of top-level package categories to consider.
+
+        package_type (str):
+            The package type nested within the category.
+
+        description (str, optional):
+            An optional description for the steps.
+
+    Returns:
+        list of _InstallStep:
+        The resulting list of steps.
+    """
+    packages: Dict[Tuple[InstallMethodType, int], List[str]] = {}
+    packages_set: Dict[Tuple[InstallMethodType, int], Set[str]] = {}
+    allow_fail_map: Dict[Tuple[InstallMethodType, int], bool] = {}
+    setup_commands: List[List[str]] = []
+    flags: Set[str] = set()
+
+    system_info = install_state['system_info']
+    arch = system_info['arch']
+    distro_families = system_info.get('distro_families')
+    distro_id = system_info.get('distro_id')
+    system = system_info['system']
+    system_install_method = system_info['system_install_method']
+
+    supported_install_methods = \
+        COMMON_INSTALL_METHODS | {system_install_method}
+
+    # Normalize the Review Board version to install.
+    rb_version_info = parse_version(
+        install_state['reviewboard_version_info']['version'])
+
+    # Normalize the distro version in use.
+    distro_version_info = parse_version(system_info['version'])
+
+    package_candidates = chain.from_iterable(
+        PACKAGES[category].get(package_type, [])
+        for category in categories
+    )
+
+    for i, candidate in enumerate(package_candidates):
+        match_info = candidate.get('match', {})
+
+        # Check for a system match.
+        if system not in match_info.get('systems', {system}):
+            continue
+
+        # Check for an architecture match.
+        if arch not in match_info.get('archs', {arch}):
+            continue
+
+        # Check that the install method is compatible.
+        install_method = candidate.get('install_method',
+                                       InstallMethodType.SYSTEM_DEFAULT)
+
+        if install_method == InstallMethodType.SYSTEM_DEFAULT:
+            install_method = system_install_method
+
+        if install_method not in supported_install_methods:
+            continue
+
+        # Check that at least one of the distro families match.
+        match_distro_families = match_info.get('distro_families', set())
+
+        if (distro_families and
+            match_distro_families and
+            not (match_distro_families & distro_families)):
+            continue
+
+        # Check for a distro ID match.
+        if (distro_id and
+            distro_id not in match_info.get('distro_ids', {distro_id})):
+            continue
+
+        # Check for a distro version match.
+        version_func = match_info.get('distro_version')
+
+        if version_func is not None and not version_func(distro_version_info):
+            continue
+
+        # Check for a Review Board version match.
+        version_func = match_info.get('rb_version')
+
+        if version_func is not None and not version_func(rb_version_info):
+            continue
+
+        # Check for any flags that must be set.
+        has_flags = match_info.get('has_flags', {})
+
+        if has_flags:
+            flags_match = True
+
+            for flag_name, flag_value in has_flags.items():
+                if ((flag_value and flag_name not in flags) or
+                    (not flag_value and flag_name in flags)):
+                    flags_match = False
+                    break
+
+            if not flags_match:
+                continue
+
+        # This is a match. Add any packages.
+        setup_commands += candidate.get('commands', [])
+
+        added_packages = candidate.get('packages', [])
+        skipped_packages = candidate.get('skip_packages', [])
+        allow_fail = candidate.get('allow_fail', False)
+        set_flags = candidate.get('set_flags', {})
+
+        if allow_fail:
+            block_i = i
+        else:
+            block_i = -1
+
+        key = (install_method, block_i)
+        packages_for_type = packages.setdefault(key, [])
+        packages_set_for_type = packages_set.setdefault(key, set())
+        allow_fail_map[key] = allow_fail
+
+        for flag_name, flag_value in set_flags.items():
+            if flag_value:
+                flags.add(flag_name)
+            else:
+                flags.discard(flag_name)
+
+        if added_packages:
+            # We have packages to add to the final list.
+            packages_for_type += added_packages
+            packages_set_for_type.update(added_packages)
+
+        if skipped_packages:
+            # We have packages to skip from the final list. These are ones
+            # that were added in a previous candidate but no longer apply in
+            # a more specific one.
+            for package in skipped_packages:
+                if package in packages_set_for_type:
+                    packages_for_type.remove(package)
+                    packages_set_for_type.remove(package)
+
+    install_steps: InstallSteps = [
+        {
+            'install_method': InstallMethodType.SHELL,
+            'name': 'Setting up support for packages',
+            'state': setup_command,
+        }
+        for setup_command in setup_commands
+    ]
+
+    for key, method_packages in packages.items():
+        install_method = key[0]
+
+        install_steps.append({
+            'allow_fail': allow_fail_map[key],
+            'install_method': install_method,
+            'name': description or _('Installing packages'),
+            'state': method_packages,
+        })
+
+    return install_steps
+
+
+def get_setup_virtualenv_steps(
+    install_state: InstallState,
+) -> InstallSteps:
+    """Return steps for setting up a virtual environment.
+
+    Version Added:
+        1.0
+
+    Args
+        install_state (rbinstall.state.InstallState):
+            The state for the installation.
+
+    Returns:
+        list of _InstallStep:
+        The resulting list of steps.
+    """
+    system_info = install_state['system_info']
+    bootstrap_python_exe = system_info['bootstrap_python_exe']
+    system_python_exe = system_info['system_python_exe']
+
+    return [
+        {
+            'install_method': InstallMethodType.SHELL,
+            'name': _('Creating Python virtual environment'),
+            'state': [
+                bootstrap_python_exe,
+                '-m',
+                'virtualenv',
+                '--download',
+                '-p',
+                system_python_exe,
+                install_state['venv_path'],
+            ],
+        },
+    ]
+
+
+def get_install_rb_packages_steps(
+    install_state: InstallState,
+) -> InstallSteps:
+    """Return steps for setting up Review Board packages.
+
+    Version Added:
+        1.0
+
+    Args
+        install_state (rbinstall.state.InstallState):
+            The state for the installation.
+
+    Returns:
+        list of _InstallStep:
+        The resulting list of steps.
+    """
+    packages: List[str] = []
+
+    for package_name, version_key in (('ReviewBoard',
+                                       'reviewboard_version_info'),
+                                      ('ReviewBoardPowerPack',
+                                       'powerpack_version_info'),
+                                      ('reviewbot-extension',
+                                       'reviewbot_extension_version_info'),
+                                      ('reviewbot-worker',
+                                       'reviewbot_worker_version_info')):
+        version_info = install_state[version_key]  # type: ignore
+
+        if version_info:
+            version = version_info['version']
+            packages.append(f'{package_name}=={version}')
+
+    return [
+        {
+            'install_method': InstallMethodType.PIP,
+            'name': _('Installing Review Board packages'),
+            'state': packages,
+        }
+    ]
+
+
+def get_install_steps(
+    *,
+    install_state: InstallState,
+) -> InstallSteps:
+    """Return all steps for installing Review Board.
+
+    Version Added:
+        1.0
+
+    Args
+        install_state (rbinstall.state.InstallState):
+            The state for the installation.
+
+    Returns:
+        list of _InstallStep:
+        The resulting list of steps.
+    """
+    categories: List[str] = [
+        'common',
+        'cvs',
+        'django-storages',
+        'git',
+        'memcached',
+        'mercurial',
+        'mysql',
+        'perforce',
+        'postgres',
+        'subversion',
+        'saml',
+    ]
+
+    return [
+        *get_install_package_steps(
+            install_state,
+            categories=categories,
+            description=_('Installing system packages'),
+            package_type='system'),
+        *get_setup_virtualenv_steps(install_state),
+        *get_install_package_steps(
+            install_state,
+            categories=categories,
+            description=_('Installing Python packaging support'),
+            package_type='virtualenv'),
+        *get_install_rb_packages_steps(install_state),
+        *get_install_package_steps(
+            install_state,
+            categories=categories,
+            description=_('Installing extra Review Board integrations'),
+            package_type='rb-extras'),
+        *get_install_package_steps(
+            install_state,
+            categories=categories,
+            description=_('Installing service integrations'),
+            package_type='service-integrations'),
+    ]
diff --git a/rbinstall/tests/test_install_steps.py b/rbinstall/tests/test_install_steps.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f63b68051c17fec9d26036b6595530536a4ae84
--- /dev/null
+++ b/rbinstall/tests/test_install_steps.py
@@ -0,0 +1,2164 @@
+"""Unit tests for rbinstall.install_steps.
+
+Version Added:
+    1.0
+"""
+
+from __future__ import annotations
+
+from typing import Set, TYPE_CHECKING
+from unittest import TestCase
+
+from rbinstall.install_methods import InstallMethodType
+from rbinstall.install_steps import get_install_steps
+from rbinstall.state import get_default_linux_install_method
+
+if TYPE_CHECKING:
+    from rbinstall.state import InstallState
+
+
+class GetInstallSteps(TestCase):
+    """Unit tests for get_install_steps().
+
+    Version Added:
+        1.0
+    """
+
+    maxDiff = None
+
+    VIRTUALENV_STEPS = [
+        {
+            'install_method': InstallMethodType.SHELL,
+            'name': 'Creating Python virtual environment',
+            'state': [
+                '/path/to/bootstrap/python',
+                '-m',
+                'virtualenv',
+                '--download',
+                '-p',
+                '/usr/bin/python',
+                '/path/to/venv',
+            ],
+        },
+        {
+            'allow_fail': False,
+            'install_method': InstallMethodType.PIP,
+            'name': 'Installing Python packaging support',
+            'state': ['pip', 'setuptools', 'wheel'],
+        },
+    ]
+
+    PYSVN_STEPS = [
+        {
+            'allow_fail': False,
+            'install_method': InstallMethodType.REMOTE_PYSCRIPT,
+            'name': 'Installing service integrations',
+            'state': ['https://pysvn.reviewboard.org'],
+        },
+    ]
+
+    RB_STEPS = [
+        {
+            'install_method': InstallMethodType.PIP,
+            'name': 'Installing Review Board packages',
+            'state': [
+                'ReviewBoard==6.0',
+                'ReviewBoardPowerPack==5.2.2',
+                'reviewbot-extension==4.0',
+                'reviewbot-worker==4.0',
+            ],
+        },
+        {
+            'allow_fail': False,
+            'install_method': InstallMethodType.REVIEWBOARD_EXTRA,
+            'name': 'Installing service integrations',
+            'state': [
+                's3',
+                'swift',
+                'mercurial',
+                'mysql',
+                'postgres',
+            ],
+        },
+    ]
+
+    COMMON_X86_STEPS = [
+        *VIRTUALENV_STEPS,
+        *RB_STEPS,
+        *PYSVN_STEPS,
+    ]
+
+    COMMON_ARM64_STEPS = [
+        *VIRTUALENV_STEPS,
+        *RB_STEPS,
+        {
+            'allow_fail': True,
+            'install_method': InstallMethodType.REVIEWBOARD_EXTRA,
+            'name': 'Installing service integrations',
+            'state': [
+                'p4',
+            ],
+        },
+        *PYSVN_STEPS,
+    ]
+
+    def test_with_amazon_linux_2_x86_64(self) -> None:
+        """Testing get_install_steps with Amazon Linux 2 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='amzn',
+            distro_families={
+                'amzn',
+                'centos',
+                'fedora',
+                'rhel',
+            },
+            version='2')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'yum', 'groupinstall', '-y', 'Development Tools',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_amazon_linux_2_aarch64(self) -> None:
+        """Testing get_install_steps with Amazon Linux 2 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='amzn',
+            distro_families={
+                'amzn',
+                'centos',
+                'fedora',
+                'rhel',
+            },
+            version='2')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'yum', 'groupinstall', '-y', 'Development Tools',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_amazon_linux_2023_x86_64(self) -> None:
+        """Testing get_install_steps with Amazon Linux 2023 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='amzn',
+            distro_families={
+                'amzn',
+                'fedora',
+            },
+            version='2023')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_amazon_linux_2023_aarch64(self) -> None:
+        """Testing get_install_steps with Amazon Linux 2023 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='amzn',
+            distro_families={
+                'amzn',
+                'fedora',
+            },
+            version='2023')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_archlinux_2023_x86_64(self) -> None:
+        """Testing get_install_steps with Arch Linux (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='arch',
+            distro_families={
+                'arch',
+            },
+            version='20231112.0.191179')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.PACMAN,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'base-devel',
+                        'libffi',
+                        'libxml2',
+                        'openssl',
+                        'perl',
+                        'xmlsec',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-libs',
+                        'subversion',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_archlinux_2023_aarch64(self) -> None:
+        """Testing get_install_steps with Arch Linux (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='arch',
+            distro_families={
+                'arch',
+            },
+            version='20231112.0.191179')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.PACMAN,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'base-devel',
+                        'libffi',
+                        'libxml2',
+                        'openssl',
+                        'perl',
+                        'xmlsec',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-libs',
+                        'subversion',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_centos_stream_8_x86_64(self) -> None:
+        """Testing get_install_steps with CentOS Stream 8 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='centos',
+            distro_families={
+                'centos',
+                'fedora',
+                'rhel',
+            },
+            version='8')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'install', '-y', 'dnf-plugins-core',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'config-manager', '--set-enabled', 'crb',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'yum', 'install', '-y', 'epel-release',
+                        'epel-next-release',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_centos_stream_8_aarch64(self) -> None:
+        """Testing get_install_steps with CentOS Stream 8 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='centos',
+            distro_families={
+                'centos',
+                'fedora',
+                'rhel',
+            },
+            version='8')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'install', '-y', 'dnf-plugins-core',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'config-manager', '--set-enabled', 'crb',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'yum', 'install', '-y', 'epel-release',
+                        'epel-next-release',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_centos_stream_9_x86_64(self) -> None:
+        """Testing get_install_steps with CentOS Stream 9 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='centos',
+            distro_families={
+                'centos',
+                'fedora',
+                'rhel',
+            },
+            version='9')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'install', '-y', 'dnf-plugins-core',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'config-manager', '--set-enabled', 'crb',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'yum', 'install', '-y', 'epel-release',
+                        'epel-next-release',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_centos_stream_9_aarch64(self) -> None:
+        """Testing get_install_steps with CentOS Stream 9 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='centos',
+            distro_families={
+                'centos',
+                'fedora',
+                'rhel',
+            },
+            version='9')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'install', '-y', 'dnf-plugins-core',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'config-manager', '--set-enabled', 'crb',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'yum', 'install', '-y', 'epel-release',
+                        'epel-next-release',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_debian_11_x86_64(self) -> None:
+        """Testing get_install_steps with Debian 11 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='debian',
+            distro_families={
+                'debian',
+            },
+            version='11 (bullseye)')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmariadb-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_debian_11_aarch64(self) -> None:
+        """Testing get_install_steps with Debian 11 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='debian',
+            distro_families={
+                'debian',
+            },
+            version='11 (bullseye)')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmariadb-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_debian_12_x86_64(self) -> None:
+        """Testing get_install_steps with Debian 12 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='debian',
+            distro_families={
+                'debian',
+            },
+            version='12 (bookworm)')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmariadb-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_debian_12_aarch64(self) -> None:
+        """Testing get_install_steps with Debian 12 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='debian',
+            distro_families={
+                'debian',
+            },
+            version='12 (bookworm)')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmariadb-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_fedora_36_x86_64(self) -> None:
+        """Testing get_install_steps with Fedora 36 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='fedora',
+            distro_families={
+                'fedora',
+            },
+            version='36')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_fedora_36_aarch64(self) -> None:
+        """Testing get_install_steps with Fedora 36 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='fedora',
+            distro_families={
+                'fedora',
+            },
+            version='36')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_fedora_37_x86_64(self) -> None:
+        """Testing get_install_steps with Fedora 37 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='fedora',
+            distro_families={
+                'fedora',
+            },
+            version='37')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_fedora_37_aarch64(self) -> None:
+        """Testing get_install_steps with Fedora 37 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='fedora',
+            distro_families={
+                'fedora',
+            },
+            version='37')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_fedora_38_x86_64(self) -> None:
+        """Testing get_install_steps with Fedora 38 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='fedora',
+            distro_families={
+                'fedora',
+            },
+            version='38')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_fedora_38_aarch64(self) -> None:
+        """Testing get_install_steps with Fedora 38 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='fedora',
+            distro_families={
+                'fedora',
+            },
+            version='38')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_fedora_39_x86_64(self) -> None:
+        """Testing get_install_steps with Fedora 39 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='fedora',
+            distro_families={
+                'fedora',
+            },
+            version='39')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_fedora_39_aarch64(self) -> None:
+        """Testing get_install_steps with Fedora 39 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='fedora',
+            distro_families={
+                'fedora',
+            },
+            version='39')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_fedora_40_x86_64(self) -> None:
+        """Testing get_install_steps with Fedora 40 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='fedora',
+            distro_families={
+                'fedora',
+            },
+            version='40')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_fedora_40_aarch64(self) -> None:
+        """Testing get_install_steps with Fedora 40 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='fedora',
+            distro_families={
+                'fedora',
+            },
+            version='40')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_opensuse_leap_15_x86_64(self) -> None:
+        """Testing get_install_steps with openSUSE Leap 15 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='opensuse-leap',
+            distro_families={
+                'opensuse',
+                'opensuse-leap',
+                'suse',
+            },
+            version='15.5')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'zypper', 'install', '-y', '-t', 'pattern',
+                        'devel_basis',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.ZYPPER,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libopenssl-devel',
+                        'libxml2-devel',
+                        'python3-devel',
+                        'xmlsec1-devel',
+                        'xmlsec1-openssl-devel',
+                        'git',
+                        'memcached',
+                        'libmariadb-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_opensuse_leap_15_aarch64(self) -> None:
+        """Testing get_install_steps with openSUSE Leap 15 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='opensuse-leap',
+            distro_families={
+                'opensuse',
+                'opensuse-leap',
+                'suse',
+            },
+            version='15.5')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'zypper', 'install', '-y', '-t', 'pattern',
+                        'devel_basis',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.ZYPPER,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libopenssl-devel',
+                        'libxml2-devel',
+                        'python3-devel',
+                        'xmlsec1-devel',
+                        'xmlsec1-openssl-devel',
+                        'git',
+                        'memcached',
+                        'libmariadb-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_opensuse_tumbleweed_x86_64(self) -> None:
+        """Testing get_install_steps with openSUSE Tumbleweed (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='opensuse-tumbleweed',
+            distro_families={
+                'opensuse',
+                'opensuse-tumbleweed',
+                'suse',
+            },
+            version='20231215')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'zypper', 'install', '-y', '-t', 'pattern',
+                        'devel_basis',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.ZYPPER,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libopenssl-devel',
+                        'libxml2-devel',
+                        'python3-devel',
+                        'xmlsec1-devel',
+                        'xmlsec1-openssl-devel',
+                        'git',
+                        'memcached',
+                        'libmariadb-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_opensuse_tumbleweed_aarch64(self) -> None:
+        """Testing get_install_steps with openSUSE Tumbleweed (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='opensuse-tumbleweed',
+            distro_families={
+                'opensuse',
+                'opensuse-tumbleweed',
+                'suse',
+            },
+            version='20231215')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'zypper', 'install', '-y', '-t', 'pattern',
+                        'devel_basis',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.ZYPPER,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libopenssl-devel',
+                        'libxml2-devel',
+                        'python3-devel',
+                        'xmlsec1-devel',
+                        'xmlsec1-openssl-devel',
+                        'git',
+                        'memcached',
+                        'libmariadb-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_rhel_8_x86_64(self) -> None:
+        """Testing get_install_steps with RHEL 8 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='rhel',
+            distro_families={
+                'fedora',
+                'rhel',
+            },
+            version='8.9')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_rhel_8_aarch64(self) -> None:
+        """Testing get_install_steps with RHEL 8 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='rhel',
+            distro_families={
+                'fedora',
+                'rhel',
+            },
+            version='8.9')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_rhel_9_x86_64(self) -> None:
+        """Testing get_install_steps with RHEL 9 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='rhel',
+            distro_families={
+                'fedora',
+                'rhel',
+            },
+            version='9.3')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'subscription-manager',
+                        'repos',
+                        '--enable',
+                        'codeready-builder-for-rhel-9-x86_64-rpms',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'install', '-y',
+                        ('https://dl.fedoraproject.org/pub/epel/'
+                         'epel-release-latest-9.noarch.rpm'),
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_rhel_9_aarch64(self) -> None:
+        """Testing get_install_steps with RHEL 9 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='rhel',
+            distro_families={
+                'fedora',
+                'rhel',
+            },
+            version='9.3')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'subscription-manager',
+                        'repos',
+                        '--enable',
+                        'codeready-builder-for-rhel-9-aarch64-rpms',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'install', '-y',
+                        ('https://dl.fedoraproject.org/pub/epel/'
+                         'epel-release-latest-9.noarch.rpm'),
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_rocky_linux_8_x86_64(self) -> None:
+        """Testing get_install_steps with Rocky Linux 8 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='rocky',
+            distro_families={
+                'centos',
+                'fedora',
+                'rhel',
+                'rocky',
+            },
+            version='8.9')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'install', '-y', 'dnf-plugins-core',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'yum', 'install', '-y', 'epel-release',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_rocky_linux_8_aarch64(self) -> None:
+        """Testing get_install_steps with Rocky Linux 8 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='rocky',
+            distro_families={
+                'centos',
+                'fedora',
+                'rhel',
+                'rocky',
+            },
+            version='8.9')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'install', '-y', 'dnf-plugins-core',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'yum', 'install', '-y', 'epel-release',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_rocky_linux_9_x86_64(self) -> None:
+        """Testing get_install_steps with Rocky Linux 9 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='rocky',
+            distro_families={
+                'centos',
+                'fedora',
+                'rhel',
+                'rocky',
+            },
+            version='9.3')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'install', '-y', 'dnf-plugins-core',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'config-manager', '--set-enabled', 'crb',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'yum', 'install', '-y', 'epel-release',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_rocky_linux_9_aarch64(self) -> None:
+        """Testing get_install_steps with Rocky Linux 9 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='rocky',
+            distro_families={
+                'centos',
+                'fedora',
+                'rhel',
+                'rocky',
+            },
+            version='9.3')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'install', '-y', 'dnf-plugins-core',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'dnf', 'config-manager', '--set-enabled', 'crb',
+                    ],
+                },
+                {
+                    'install_method': InstallMethodType.SHELL,
+                    'name': 'Setting up support for packages',
+                    'state': [
+                        'yum', 'install', '-y', 'epel-release',
+                    ],
+                },
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.YUM,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'gcc',
+                        'gcc-c++',
+                        'libffi-devel',
+                        'libxml2-devel',
+                        'make',
+                        'openssl-devel',
+                        'patch',
+                        'perl',
+                        'python3-devel',
+                        'libtool-ltdl-devel',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'mariadb-connector-c-devel',
+                        'subversion',
+                        'subversion-devel',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_ubuntu_18_04_x86_64(self) -> None:
+        """Testing get_install_steps with Ubuntu 18.04 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='ubuntu',
+            distro_families={
+                'debian',
+                'ubuntu',
+            },
+            version='18.04')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmysqlclient-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_ubuntu_18_04_aarch64(self) -> None:
+        """Testing get_install_steps with Ubuntu 18.04 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='ubuntu',
+            distro_families={
+                'debian',
+                'ubuntu',
+            },
+            version='18.04')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmysqlclient-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_ubuntu_20_04_x86_64(self) -> None:
+        """Testing get_install_steps with Ubuntu 20.04 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='ubuntu',
+            distro_families={
+                'debian',
+                'ubuntu',
+            },
+            version='20.04')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmysqlclient-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_ubuntu_20_04_aarch64(self) -> None:
+        """Testing get_install_steps with Ubuntu 20.04 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='ubuntu',
+            distro_families={
+                'debian',
+                'ubuntu',
+            },
+            version='20.04')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmysqlclient-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_ubuntu_22_04_x86_64(self) -> None:
+        """Testing get_install_steps with Ubuntu 22.04 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='ubuntu',
+            distro_families={
+                'debian',
+                'ubuntu',
+            },
+            version='22.04')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmysqlclient-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_ubuntu_22_04_aarch64(self) -> None:
+        """Testing get_install_steps with Ubuntu 22.04 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='ubuntu',
+            distro_families={
+                'debian',
+                'ubuntu',
+            },
+            version='22.04')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmysqlclient-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def test_with_ubuntu_23_10_x86_64(self) -> None:
+        """Testing get_install_steps with Ubuntu 23.10 (x86_64)"""
+        install_state = self.create_install_state(
+            arch='x86_64',
+            distro_id='ubuntu',
+            distro_families={
+                'debian',
+                'ubuntu',
+            },
+            version='23.10')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmysqlclient-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_ARM64_STEPS,
+            ])
+
+    def test_with_ubuntu_23_10_aarch64(self) -> None:
+        """Testing get_install_steps with Ubuntu 23.10 (aarch64)"""
+        install_state = self.create_install_state(
+            arch='aarch64',
+            distro_id='ubuntu',
+            distro_families={
+                'debian',
+                'ubuntu',
+            },
+            version='23.10')
+
+        self.assertEqual(
+            get_install_steps(install_state=install_state),
+            [
+                {
+                    'allow_fail': False,
+                    'install_method': InstallMethodType.APT,
+                    'name': 'Installing system packages',
+                    'state': [
+                        'build-essential',
+                        'libffi-dev',
+                        'libjpeg-dev',
+                        'libssl-dev',
+                        'libxml2-dev',
+                        'libxmlsec1-dev',
+                        'libxmlsec1-openssl',
+                        'patch',
+                        'pkg-config',
+                        'python3-dev',
+                        'python3-pip',
+                        'cvs',
+                        'git',
+                        'memcached',
+                        'libmysqlclient-dev',
+                        'subversion',
+                        'libsvn-dev',
+                    ],
+                },
+                *self.COMMON_X86_STEPS,
+            ])
+
+    def create_install_state(
+        self,
+        *,
+        arch: str = 'x86_64',
+        system: str = 'Linux',
+        version: str = '',
+        distro_id: str = '',
+        distro_families: Set[str] = set(),
+    ) -> InstallState:
+        install_method = get_default_linux_install_method(
+            families=distro_families)
+        assert install_method
+
+        return {
+            'create_sitedir': False,
+            'dry_run': False,
+            'install_reviewbot_extension': True,
+            'install_reviewbot_worker': True,
+            'install_powerpack': True,
+            'powerpack_version_info': {
+                'is_latest': True,
+                'is_requested': True,
+                'latest_version': '5.2.2',
+                'package_name': 'ReviewBoardPowerPack',
+                'requires_python': '>=3.7',
+                'version': '5.2.2',
+            },
+            'reviewboard_version_info': {
+                'is_latest': True,
+                'is_requested': True,
+                'latest_version': '6.0',
+                'package_name': 'ReviewBoard',
+                'requires_python': '>=3.8',
+                'version': '6.0',
+            },
+            'reviewbot_extension_version_info': {
+                'is_latest': True,
+                'is_requested': True,
+                'latest_version': '4.0',
+                'package_name': 'reviewbot-extension',
+                'requires_python': '>=3.8',
+                'version': '4.0',
+            },
+            'reviewbot_worker_version_info': {
+                'is_latest': True,
+                'is_requested': True,
+                'latest_version': '4.0',
+                'package_name': 'reviewbot-worker',
+                'requires_python': '>=3.8',
+                'version': '4.0',
+            },
+            'sitedir_path': '/var/www/reviewboard',
+            'steps': [],
+            'system_info': {
+                'arch': arch,
+                'bootstrap_python_exe': '/path/to/bootstrap/python',
+                'distro_id': distro_id,
+                'distro_families': distro_families,
+                'paths': {},
+                'system_install_method': install_method,
+                'system': system,
+                'system_python_exe': '/usr/bin/python',
+                'system_python_version': (3, 11, 0, '', 0),
+                'version': version,
+            },
+            'unattended_install': False,
+            'venv_path': '/path/to/venv',
+            'venv_pip_exe': '/path/to/venv/bin/pip',
+            'venv_python_exe': '/path/to/venv/bin/python',
+        }
diff --git a/rbinstall/tests/test_versioning.py b/rbinstall/tests/test_versioning.py
new file mode 100644
index 0000000000000000000000000000000000000000..784612cecf626f98b07d05898b004d00e64d5f3c
--- /dev/null
+++ b/rbinstall/tests/test_versioning.py
@@ -0,0 +1,31 @@
+"""Unit tests for rbinstall.versioning.
+
+Version Added:
+    1.0
+"""
+
+from __future__ import annotations
+
+from unittest import TestCase
+
+from rbinstall.versioning import parse_version
+
+
+class ParseVersionTests(TestCase):
+    """Unit tests for parse_version().
+
+    Version Added:
+        1.0
+    """
+
+    def test_with_ints(self) -> None:
+        """Testing parse_version with all integers"""
+        self.assertEqual(parse_version('1.2.3'), (1, 2, 3))
+
+    def test_with_strings(self) -> None:
+        """Testing parse_version with all strings"""
+        self.assertEqual(parse_version('a.b.c'), ('a', 'b', 'c'))
+
+    def test_with_mixed(self) -> None:
+        """Testing parse_version with mixed integers and strings"""
+        self.assertEqual(parse_version('1.2.abc'), (1, 2, 'abc'))
diff --git a/rbinstall/versioning.py b/rbinstall/versioning.py
new file mode 100644
index 0000000000000000000000000000000000000000..98b1a1d0ba42aa6db962c41e8e7e5649ded33c19
--- /dev/null
+++ b/rbinstall/versioning.py
@@ -0,0 +1,184 @@
+"""Utilities for working with version information.
+
+Version Added:
+    1.0
+"""
+
+# NOTE: This file must be syntactically compatible with Python 3.7+.
+from __future__ import annotations
+
+import operator
+from functools import partialmethod
+from typing import Any, Callable, List, Tuple, TypeVar, Union
+
+from typing_extensions import TypeAlias
+
+
+_ParsedVersionPart: TypeAlias = Union[int, str]
+_ParsedVersionPartT = TypeVar('_ParsedVersionPartT', int, str)
+
+
+class _ComparedVersionPart:
+    """A wrapper for version parts, to aid in comparison.
+
+    This will compare integers to integers, and otherwise compare two values
+    as strings.
+
+    Version Added:
+        1.0
+    """
+
+    ######################
+    # Instance variables #
+    ######################
+
+    part: _ParsedVersionPart
+
+    def __init__(
+        self,
+        part: _ParsedVersionPart,
+    ) -> None:
+        """Initialize the wrapper.
+
+        Args:
+            part (int or str):
+                The version part for comparison.
+        """
+        self.part = part
+
+    def _compare(
+        self,
+        other: object,
+        *,
+        op: Callable[[Any, Any], bool],
+    ) -> bool:
+        """Compare the wrapped value with another.
+
+        If both are integers, they will be compared as-is with the given
+        operator. Otherwise, they'll first be normalized to strings and then
+        compared.
+
+        Args:
+            other (object):
+                The other object for comparison.
+
+            op (callable):
+                The operator used to compare the values.
+
+        Returns:
+            bool:
+            The results of the operator on the normalized values.
+        """
+        if not isinstance(other, _ComparedVersionPart):
+            return False
+
+        a = self.part
+        b = other.part
+        is_a_int = isinstance(a, int)
+        is_b_int = isinstance(b, int)
+
+        if is_a_int != is_b_int:
+            a = str(a)
+            b = str(b)
+
+        return op(a, b)
+
+    __ge__ = partialmethod(_compare, op=operator.ge)
+    __gt__ = partialmethod(_compare, op=operator.gt)
+    __le__ = partialmethod(_compare, op=operator.le)
+    __lt__ = partialmethod(_compare, op=operator.lt)
+
+    # Note that we have to ignore the type on operator.eq, because instead
+    # of checking for comparable types, it hard-codes "object" specifically.
+    __eq__ = partialmethod(_compare, op=operator.eq)  # type: ignore
+
+
+def parse_version(
+    version: str,
+) -> ParsedVersion:
+    """Return a tuple representing a parsed version string.
+
+    The string is expected to be ``.``-delimited. It will be converted to
+    a tuple, with any numbers converted to integers.
+
+    Version Added:
+        1.0
+
+    Args:
+        version (str):
+            The version to parse.
+
+    Returns:
+        tuple:
+        The parsed version tuple.
+    """
+    part: _ParsedVersionPart
+
+    info: List[_ParsedVersionPart] = []
+
+    for part in version.split('.'):
+        try:
+            part = int(part)
+        except ValueError:
+            pass
+
+        info.append(part)
+
+    return tuple(info)
+
+
+def match_version(
+    *matched_version_info: _ParsedVersionPart,
+    op: Callable[[Tuple[_ComparedVersionPart, ...],
+                  Tuple[_ComparedVersionPart, ...]], bool] = operator.eq,
+) -> VersionMatchFunc:
+    """Match a computed version to an expected version.
+
+    This will check for equality by default, but can take an operator to
+    perform other comparisons.
+
+    Version Added:
+        1.0
+
+    Args:
+        *matched_version_info (tuple of int):
+            The version parts to compare against.
+
+        op (callable, optional):
+            The operator used for comparison.
+
+    Returns:
+        callable:
+        The version comparator.
+    """
+    def _match(
+        version_info: Tuple[_ParsedVersionPart, ...],
+    ) -> bool:
+        return op(
+            tuple(
+                _ComparedVersionPart(part)
+                for part in version_info
+            ),
+            tuple(
+                _ComparedVersionPart(part)
+                for part in matched_version_info
+            ))
+
+    return _match
+
+
+#: A parsed version tuple.
+#:
+#: This may consist of both integers and strings.
+#:
+#: Version Added:
+#:     1.0
+ParsedVersion: TypeAlias = Tuple[_ParsedVersionPart, ...]
+
+
+#: A function that can match a version.
+#:
+#: Version Added:
+#:     1.0
+VersionMatchFunc: TypeAlias = Callable[[Tuple[_ParsedVersionPart, ...]],
+                                       bool]
