diff --git a/docs/manual/admin/integrations/circle-ci.rst b/docs/manual/admin/integrations/circle-ci.rst
index 154b6c7a5f7159d29a01641d5fccbe79244940c2..88e9394bed90cc72b5a7a59f88b63cf30021853b 100644
--- a/docs/manual/admin/integrations/circle-ci.rst
+++ b/docs/manual/admin/integrations/circle-ci.rst
@@ -143,7 +143,7 @@ using :ref:`rbt patch <rbtools:rbt-patch>`.
           - checkout
           - run:
             name: Install dependencies
-            command: sudo python setup.py develop
+            command: sudo pip install -e .
           - run:
             name: Run tests
             command: python ./tests/runtests.py
@@ -159,7 +159,7 @@ using :ref:`rbt patch <rbtools:rbt-patch>`.
               rbt patch --api-token "$REVIEWBOARD_API_TOKEN" --server "$REVIEWBOARD_SERVER" --diff-revision "$REVIEWBOARD_DIFF_REVISION" "$REVIEWBOARD_REVIEW_REQUEST"
           - run:
             name: Install dependencies
-            command: sudo python setup.py develop
+            command: sudo pip install -e .
           - run:
             name: Run tests
             command: python ./tests/runtests.py
diff --git a/docs/manual/extending/auth-backends.rst b/docs/manual/extending/auth-backends.rst
index 543470b63c2882350d7188208461ba5ae35776a1..97f18fcf10a827155d76fb45a4a8aa1dd2f9c972 100644
--- a/docs/manual/extending/auth-backends.rst
+++ b/docs/manual/extending/auth-backends.rst
@@ -393,15 +393,12 @@ Using Entry Points
 ------------------
 
 When extensions are, for some reason, not an ideal option, you can instead
-fall back on using Python entry point registration. This is required
-if your authentication backend needs to work on versions of Review Board
-prior to 2.0.
+fall back on using Python entry point registration.
 
 For entry point registration, your authentication backends will need to be
-packaged as a standard Python egg module. Generally, this looks something
-like::
+packaged as a standard Python package. Generally, this looks something like::
 
-    setup.py
+    pyproject.toml
     myauth/__init__.py
 
 The :file:`__init__.py` would contain your authentication backend's classes
@@ -412,17 +409,13 @@ You can of course split this up into separate files (such as
 settings form). This is entirely up to you. However, to be a proper Python
 module, you must have a :file:`__init__.py`, though it can be blank.
 
-:file:`setup.py` must define an "entry point" for your module in order for
-Review Board to find it. This is done through the ``entry_points`` parameter
-passed to ``setup``. For example::
+:ref:`pyproject.toml` must define a :term:`Python Entry Point` for your module
+in order for Review Board to find it. For example:
 
-    setup(...,
-          entry_points={
-              'reviewboard.auth_backends': [
-                  'myauth = myauth:MyAuthBackend',
-              ],
-          }
-    )
+.. code-block:: console
+
+    [project.entry-points."reviewboard.auth_backends"]
+    myauth = 'myauth:MyAuthBackend'
 
 Review Board will look in ``reviewboard.auth_backends`` for every module and
 attempt to load it. The module path specified must be the full Python module
@@ -430,6 +423,5 @@ path for your class. The ID (``myauth`` in the example above) can be anything,
 but generally should be consistent with your settings prefix for the settings
 form, and must not conflict with any other authentication modules.
 
-The authentication module can then be installed by typing (as root)::
-
-    $ python setup.py install
+The authentication module can then be installed by building and installing
+your package.
diff --git a/docs/manual/extending/coderef/index.rst b/docs/manual/extending/coderef/index.rst
index 440afcc680df9249fa6b8d3ea499b9b0572823c9..3f26cb44e0d368d6ec48ce7b21ed51f5c9dbeb6a 100644
--- a/docs/manual/extending/coderef/index.rst
+++ b/docs/manual/extending/coderef/index.rst
@@ -191,6 +191,7 @@ Extensions
    reviewboard.extensions.base
    reviewboard.extensions.hooks
    reviewboard.extensions.packaging
+   reviewboard.extensions.packaging.backend
    reviewboard.extensions.packaging.setuptools_backend
    reviewboard.extensions.packaging.static_media
    reviewboard.extensions.testing
diff --git a/docs/manual/extending/extensions/distribution.rst b/docs/manual/extending/extensions/distribution.rst
index 28013d9a80fd755a4702df81711d25f6019f41e9..9279567b759351c998a3aa1149c301011a71d535 100644
--- a/docs/manual/extending/extensions/distribution.rst
+++ b/docs/manual/extending/extensions/distribution.rst
@@ -11,14 +11,16 @@ Python Packages
 ===============
 
 Extensions are packaged and distributed as Python packages (:term:`Python
-Eggs` or :term:`Python Wheels`). This allows for automatic detection of
-installed extensions, packaging of static files, and dependency checking.
+Wheels`). This allows for automatic detection of installed extensions,
+packaging of static files, and dependency checking.
 
 
 Extension packages are pretty much like any other Python package. It uses
-:py:mod:`setuptools` and a :file:`setup.py` file to define the package
-contents and to build the package. There are a few additional features that
-are provided by Review Board, which will be covered in this guide.
+a :ref:`pyproject.toml` file to define your package contents, along with
+Review Board's extension build backend.
+
+There are a few additional features that are provided by Review Board, which
+will be covered in this guide.
 
 
 .. _extension-entry-point:
@@ -29,15 +31,12 @@ Defining an Entry Point
 To facilitate the auto-detection of installed extensions, a
 ``reviewboard.extensions`` :term:`Python Entry Point` must be defined for each
 :ref:`extension class <extension-class>` in your package. These are defined
-like so:
+in your :ref:`pyproject.toml` like so:
 
-.. code-block:: python
+.. code-block:: toml
 
-      entry_points={
-          'reviewboard.extensions': [
-              'sample_extension = sample_extension.extension:SampleExtension',
-          ],
-      },
+   [project.entry-points."reviewboard.extensions"]
+   sample_extension = 'sample_extension.extension:SampleExtension'
 
 This tells the Python packaging system that there's a Review Board extension
 named ``sample_extension`` that points to
@@ -45,26 +44,53 @@ named ``sample_extension`` that points to
 installed, Review Board will be able to use this registered entry point to
 locate your extension.
 
-Here is an example of a full :file:`setup.py` file defining this entry point:
+Here is an example of a full :ref:`pyproject.toml` file defining this entry
+point:
 
-.. code-block:: python
+.. code-block:: toml
+
+   [build-system]
+   requires = [
+       # Update this for the target version of Review Board.
+       'reviewboard~=7.1',
+
+       'reviewboard[extension-packaging]',
+   ]
+   build-backend = 'reviewboard.extensions.packaging.backend'
+
+
+   [project]
+   name = 'sample_extension'
+   version = '1.0'
+   description = 'Description of your extension package.'
+   authors = [
+       {name = 'Your Name', email = 'your-email@example.com'}
+   ]
+
+   dependencies = [
+       # Your package dependencies go here.
+       # Don't include "ReviewBoard" in this list.
+   ]
+
+   classifiers = [
+       # For a full list of package classifiers, see
+       # https://pypi.python.org/pypi?%3Aaction=list_classifiers
 
-   from reviewboard.extensions.packaging import setup
-   from setuptools import find_packages
+       'Development Status :: 3 - Alpha',
+       'Environment :: Web Framework',
+       'Framework :: Review Board',
+       'Operating System :: OS Independent',
+       'Programming Language :: Python',
+   ]
 
 
-   setup(
-       name='sample_extension',
-       version='0.1',
-       description='Description of extension',
-       author='Your Name',
-       packages=find_packages(),
-       entry_points={
-           'reviewboard.extensions': [
-               'sample_extension = sample_extension.extension:SampleExtension',
-           ],
-       },
-   )
+   [project.entry-points."reviewboard.extensions"]
+   sample_extension = 'sample_extension.extension:SampleExtension'
+
+
+   [tool.setuptools.packages.find]
+   where = ['.']
+   namespaces = false
 
 
 .. _extension-package-static-files:
@@ -84,7 +110,7 @@ See :ref:`extension-static-files` for more information on bundles.
 .. _extension-package-data-files:
 
 Packaging Templates/Data Files
------------------------------------
+------------------------------
 
 If your package needs to ship templates or other data files, you'll need
 to include these in your package's :file:`MANIFEST.in` file. Please see
@@ -92,7 +118,7 @@ the `MANIFEST.in documentation
 <https://docs.python.org/2/distutils/sourcedist.html#manifest-template>`_ for
 the format of this file.
 
-This file will live in the same directory as your :file:`setup.py`.
+This file will live in the same directory as your :ref:`pyproject.toml`.
 
 Your :file:`MANIFEST.in` might look something like this::
 
@@ -108,11 +134,8 @@ Dependencies
 ------------
 
 Your package can specify a list of dependencies, which are other packages that
-will be installed when your package is installed. This is specified as an
-``install_requires`` parameter to
-:py:func:`~reviewboard.extensions.packaging.setup`. See the `official
-documentation <https://packaging.python.org/requirements/#install-requires>`_
-for how to specify dependencies.
+will be installed when your package is installed. This is specified as
+``dependencies`` in :ref:`pyproject.toml`.
 
 .. warning::
 
@@ -122,29 +145,16 @@ for how to specify dependencies.
    cases) of accidentally upgrading all or part of your Review Board install
    when installing your package.
 
-Your :file:`setup.py` might look like:
+For example, your :ref:`pyproject.toml` may include:
 
-.. code-block:: python
+.. code-block:: toml
 
-   from reviewboard.extensions.packaging import setup
-   from setuptools import find_packages
-
-
-   setup(
-       name='sample_extension',
-       version='0.1',
-       description='Description of extension',
-       author='Your Name',
-       packages=find_packages(),
-       entry_points={
-           'reviewboard.extensions': [
-               'sample_extension = sample_extension.extension:SampleExtension',
-           ],
-       },
-       install_requires=[
-           'PythonPackageIDependOn>=0.1',
-       ],
-   )
+   [project]
+   ...
+
+   dependencies = [
+      'PythonPackageIDependOn>=0.1'
+   ]
 
 In addition, extensions can have a run-time dependency on another extension,
 forcing that extension to be enabled when yours is enabled. This is done by
@@ -165,41 +175,24 @@ example:
 Building a Package
 ------------------
 
-You're now ready to build your package! Before you do, let's talk setup and
-deployment options.
-
-.. note::
-
-   If you're running Review Board 2.5.7 or older, and you're working with
-   static media files, you'll need to install a couple of modules using
-   `npm <https://docs.npmjs.com/getting-started/installing-node>`_::
-
-       $ sudo npm install -g less uglifyjs
-
-If you're looking to distribute your package publicly (such as on the `Python
-Package Index`_, you'll want to build this as a Wheel, Egg, and maybe a Source
-Distribution ("sdist"). You can build all three with one command::
-
-    $ python setup.py bdist_wheel bdist_egg sdist
+You're now ready to build your package! Just follow these steps:
 
-That will produce builds in the :file:`dist/` directory.
+1. Make sure you have Python's :pypi:`build` package installed:
 
-If this is for internal use, you can get away with just one package format.
-We recommend Wheels, as these are the new standard for Python packaging. You
-can build just the Wheel by running::
+   .. code-block:: console
 
-    $ python setup.py bdist_wheel
+      $ pip3 install build
 
-.. note::
+   You only have to do this once.
 
-   If you get an error about ``bdist_wheel`` not being a valid command, you
-   will need to update your ``pip`` package and install ``wheel``::
+2. Build your source distribution and wheel package from the top of your
+   extension's source tree:
 
-       $ pip install -U pip
-       $ pip install wheel
+   .. code-block:: console
 
+      $ python3 -m build .
 
-.. _Python Package Index: https://pypi.python.org/pypi/
+   That will produce builds in the :file:`dist/` directory.
 
 
 .. _extension-package-developing:
@@ -209,17 +202,15 @@ Developing Against Your Package
 
 If you're actively testing your package against Review Board, you don't want
 to keep rebuilding the package every time you make a change. Instead, you'll
-want to install your package in development mode::
+want to install your package in "editable" mode:
 
-    $ python setup.py develop
+.. code-block:: console
 
-This basically tells the Python packaging system that the installed package
-lives in your source tree. The entry points will be registered and you'll be
-able to enable the extension in Review Board. It's the recommended way to
-iterate on your package while you test.
+   $ pip3 install -e .
 
-.. note::
+This allows you to make changes to your extension and test it without building
+and installing new packages.  It's the recommended way to iterate on your
+package while you test.
 
-   Due to some differences in how the package is prepared, this will require
-   testing against a Review Board development server, instead of a production
-   install.
+We recommend only testing editable packages against a Review Board development
+server, and not against a production server.
diff --git a/docs/manual/extending/extensions/file-layout.rst b/docs/manual/extending/extensions/file-layout.rst
index 6dd7e5bd2306f30e86bc22242565cad8acde3e21..7b7bd4634ca537b711c8f6d7475456d2372cf819 100644
--- a/docs/manual/extending/extensions/file-layout.rst
+++ b/docs/manual/extending/extensions/file-layout.rst
@@ -25,41 +25,87 @@ Required Files
 
 At minimum, an extension requires the following files:
 
-*  :ref:`setup.py <extension-example-files-setup.py>`
+*  :ref:`pyproject.toml <extension-example-files-pyproject.toml>`
 *  *extensiondir*/:ref:`__init__.py <extension-example-files-__init__.py>`
 *  *extensiondir*/:ref:`extension.py <extension-example-files-extension.py>`
 
 Let's go into each of these files and show some examples.
 
 
-.. _extension-example-files-setup.py:
+.. _pyproject.toml:
+.. _extension-example-files-pyproject.toml:
 
-**setup.py**
-   This is the file used to create the Python package. It defines the
+**pyproject.toml**
+   This file defines your Python package, its metadata, and the
    :ref:`Entry Point <extension-entry-point>` used to allow Review Board to
-   find the extension, and contains other metadata. This is covered in detail
-   in :ref:`extension-distribution`.
+   find the extension. This is covered in detail in
+   :ref:`extension-distribution`.
 
-   Here's an example :file:`setup.py`:
+   You can learn more about :file:`pyproject.toml` by reading:
 
-   .. code-block:: python
+   * `Writing your pyproject.toml
+     <https://packaging.python.org/en/latest/guides/writing-pyproject-toml/>`_
+   * `Configuring setuptools using pyproject.toml files
+     <https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html>`_
+
+   Here's an example :file:`pyproject.toml`:
+
+   .. code-block:: toml
+
+      [build-system]
+      requires = [
+          # Update this for the target version of Review Board.
+          'reviewboard~=7.1',
+
+          'reviewboard[extension-packaging]',
+      ]
+      build-backend = 'reviewboard.extensions.packaging.backend'
+
+
+      [project]
+      name = 'sample_extension'
+      version = '1.0'
+      description = 'Description of your extension package.'
+      authors = [
+          {name = 'Your Name', email = 'your-email@example.com'}
+      ]
+
+      dependencies = [
+          # Your package dependencies go here.
+          # Don't include "ReviewBoard" in this list.
+      ]
+
+      classifiers = [
+          # For a full list of package classifiers, see
+          # https://pypi.python.org/pypi?%3Aaction=list_classifiers
+
+          'Development Status :: 3 - Alpha',
+          'Environment :: Web Framework',
+          'Framework :: Review Board',
+          'Operating System :: OS Independent',
+          'Programming Language :: Python',
+      ]
+
+
+      [project.entry-points."reviewboard.extensions"]
+      sample_extension = 'sample_extension.extension:SampleExtension'
+
+
+      [tool.setuptools.packages.find]
+      where = ['.']
+      namespaces = false
+
+   .. important::
+
+      Prior to Review Board 7.1, new extensions would provide a
+      :file:`setup.py` file instead, and may have only included some basic
+      information in :file:`pyproject.toml`.
 
-      from reviewboard.extensions.packaging import setup
-      from setuptools import find_packages
-
-
-      setup(
-          name='sample_extension',
-          version='0.1',
-          description='Description of extension package.',
-          author='Your Name',
-          packages=find_packages(),
-          entry_points={
-              'reviewboard.extensions': [
-                  'sample_extension = sample_extension.extension:SampleExtension',
-              ]
-          },
-      )
+      Modern Python packaging requires :file:`pyproject.toml`. You can keep
+      your existing :file:`setup.py` or fully convert to
+      :file:`pyproject.toml`. If you keep your :file:`setup.py`, make sure
+      to at least add the ``[build-system]`` section from above in your
+      :file:`pyproject.toml`.
 
 
 .. _extension-example-files-__init__.py:
diff --git a/docs/manual/extending/extensions/rbext.rst b/docs/manual/extending/extensions/rbext.rst
index 0ac118124d2a66effaec06d3b7283f72521b48ab..ea03e538352f80d2254a7c64d7d5ae2b93d240eb 100644
--- a/docs/manual/extending/extensions/rbext.rst
+++ b/docs/manual/extending/extensions/rbext.rst
@@ -58,12 +58,13 @@ initial extension:
 .. option:: --package-version
 
    Sets a specific version to use for the package and extension. This will
-   be stored in the :file:`setup.py` in your package. It defaults to "1.0".
+   be stored in the :ref:`pyproject.toml` in your package. It defaults to
+   "1.0".
 
 .. option:: --summary
 
-   Sets a summary for the package's :file:`setup.py`, :file:`README.rst` and
-   for the extension information.
+   Sets a summary for the package's :ref:`pyproject.toml`, :file:`README.rst`
+   and for the extension information.
 
 .. option:: --description
 
@@ -71,14 +72,14 @@ initial extension:
 
 .. option:: --author-name
 
-   Sets the name of the author for the package's :file:`setup.py`. This can
-   be an individual or the name of an organization/company.
+   Sets the name of the author for the package's :ref:`pyproject.toml`. This
+   can be an individual or the name of an organization/company.
 
 .. option:: --author-email
 
-   Sets the e-mail address of the author for the package's :file:`setup.py`.
-   This can be any e-mail address suitable for contacting the developers of
-   the package.
+   Sets the e-mail address of the author for the package's
+   :ref:`pyproject.toml`.  This can be any e-mail address suitable for
+   contacting the developers of the package.
 
 .. option:: --enable-configuration
 
@@ -156,7 +157,7 @@ There's a few special arguments you may want to use:
 .. option:: --tree-root
 
    The path to the root of your extension's source tree (where
-   :file:`setup.py` lives). This defaults to the current directory.
+   :ref:`pyproject.toml` lives). This defaults to the current directory.
 
 .. option:: --with-coverage
 
diff --git a/docs/manual/extending/legacy-auth-backends.rst b/docs/manual/extending/legacy-auth-backends.rst
index 10a39d2762bf77d528e15a35c5ea133880aa4fc3..8d5fdac7fe754fd591bff454c277ce49dcb908e9 100644
--- a/docs/manual/extending/legacy-auth-backends.rst
+++ b/docs/manual/extending/legacy-auth-backends.rst
@@ -124,9 +124,9 @@ get_user
 Installing the Authentication Backend
 =====================================
 
-The authentication backend should be packaged as a standard Python egg.
-This includes creating a :file:`setup.py` and making a proper Python module
-that includes your authentication backend.
+The authentication backend should be packaged as a standard Python package.
+This includes creating a :ref:`pyproject.toml` and making a proper Python
+module that includes your authentication backend.
 
 Once this is Python package is installed on the system running Review Board,
 you can change the Authentication type in Review Board to :guilabel:`Custom`
diff --git a/reviewboard/cmdline/rbext.py b/reviewboard/cmdline/rbext.py
index 47f3e8ffb2ca2e475ffd23eec03243bb0e6bd3e8..93d67575c71ba1564c569e01ce7de626b959021a 100644
--- a/reviewboard/cmdline/rbext.py
+++ b/reviewboard/cmdline/rbext.py
@@ -27,7 +27,10 @@ sys.path.insert(0, os.path.join(rbext_dir, 'conf', 'rbext'))
 from django.utils.encoding import force_str
 from importlib_metadata import Prepared
 
-from reviewboard import get_manual_url
+from reviewboard import (
+    __version__ as reviewboard_version,
+    get_manual_url,
+)
 from reviewboard.cmdline.utils.argparsing import (HelpFormatter,
                                                   RBProgVersionAction)
 from reviewboard.cmdline.utils.console import init_console
@@ -432,19 +435,14 @@ class CreateCommand(BaseCommand):
             self._create_manifest(static_dir=static_dir,
                                   templates_dir=templates_dir))
 
-        self._write_file(
-            os.path.join(root_dir, 'setup.py'),
-            self._create_setup_py(package_name=package_name,
-                                  version=options.package_version,
-                                  summary=summary,
-                                  author=options.author_name,
-                                  author_email=options.author_email,
-                                  class_name=class_name),
-            mode=0o755)
-
         self._write_file(
             os.path.join(root_dir, 'pyproject.toml'),
-            self._create_pyproject_toml(package_name=package_name))
+            self._create_pyproject_toml(author=options.author_name,
+                                        author_email=options.author_email,
+                                        class_name=class_name,
+                                        package_name=package_name,
+                                        summary=summary,
+                                        version=options.package_version))
 
         self._write_file(
             os.path.join(root_dir, 'conftest.py'),
@@ -733,19 +731,116 @@ class CreateCommand(BaseCommand):
 
     def _create_pyproject_toml(
         self,
+        *,
+        author: str,
+        author_email: str,
+        class_name: str,
         package_name: str,
+        summary: str,
+        version: str,
     ) -> str:
         """Create the content for a pyproject.toml file.
 
+        Version Changed:
+            7.1:
+            Added ``author``, ``author_email``, ``class_name``, ``summary``,
+            and ``version`` arguments.
+
         Args:
+            author (str):
+                The name of the author of the extension.
+
+                Version Added:
+                    7.1
+
+            author_email (str):
+                The e-mail address of the author of the extension.
+
+                Version Added:
+                    7.1
+
+            class_name (str):
+                The name of the extension class.
+
+                Version Added:
+                    7.1
+
             package_name (str):
                 The name of the package.
 
+                Version Added:
+                    7.1
+
+            summary (str):
+                A summary of the package.
+
+                Version Added:
+                    7.1
+
+            version (str):
+                The version of the package.
+
+                Version Added:
+                    7.1
+
         Returns:
             str:
             The resulting content for the file.
         """
+        if not author:
+            author = '<REPLACE ME>'
+
+        if not author_email:
+            author_email = '<REPLACE ME>'
+
+        if not summary:
+            summary = '<REPLACE ME>'
+
         return f"""
+            [build-system]
+            requires = [
+                # Update this for the target version of Review Board.
+                'reviewboard~={reviewboard_version}',
+
+                'reviewboard[extension-packaging]',
+            ]
+            build-backend = 'reviewboard.extensions.packaging.backend'
+
+
+            [project]
+            name = '{package_name}'
+            version = '{version}'
+            description = '{summary}'
+            authors = [
+                {{name = '{author}', email = '{author_email}'}}
+            ]
+
+            dependencies = [
+                # Your package dependencies go here.
+                # Don't include "ReviewBoard" in this list.
+            ]
+
+            classifiers = [
+                # For a full list of package classifiers, see
+                # https://pypi.python.org/pypi?%3Aaction=list_classifiers
+
+                'Development Status :: 3 - Alpha',
+                'Environment :: Web Framework',
+                'Framework :: Review Board',
+                'Operating System :: OS Independent',
+                'Programming Language :: Python',
+            ]
+
+
+            [project.entry-points."reviewboard.extensions"]
+            {package_name} = '{package_name}.extension:{class_name}'
+
+
+            [tool.setuptools.packages.find]
+            where = ['.']
+            namespaces = false
+
+
             [tool.pytest.ini_options]
             DJANGO_SETTINGS_MODULE = "reviewboard.settings"
             django_debug_mode = false
diff --git a/reviewboard/extensions/packaging/backend.py b/reviewboard/extensions/packaging/backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..e7f21ae4c83269a17d2fd1a9d7a5863048e405e5
--- /dev/null
+++ b/reviewboard/extensions/packaging/backend.py
@@ -0,0 +1,115 @@
+"""Build backend for Review Board extension packages.
+
+Version Added:
+    7.1
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+from contextlib import contextmanager
+from typing import Iterator, Optional
+
+from setuptools import build_meta
+
+
+@contextmanager
+def _prepare_package_files() -> Iterator[None]:
+    """Prepare files for the package.
+
+    This will create a :file:`setup.py` if one doesn't already exist and
+    ensure the extension's module can be found in the search path.
+
+    Context:
+        The package files and environment will be ready for building.
+    """
+    setup_py_created: bool = False
+
+    if not os.path.exists('setup.py'):
+        # There's no setup.py here, so we'll want to create one that calls
+        # our version.
+        #
+        # This is the same approach that setuptools (at least as of v74)
+        # uses. The build backend is still a wrapper around calling setup().
+        setup_py_created = True
+
+        with open('setup.py', 'w') as fp:
+            fp.write(
+                'from reviewboard.extensions.packaging.setuptools_backend'
+                ' import setup\n'
+                '\n'
+                'setup()\n'
+            )
+
+    # Make sure the extension's module can be found in this directory.
+    sys.path.insert(0, os.getcwd())
+
+    try:
+        yield
+    finally:
+        if setup_py_created:
+            os.unlink('setup.py')
+
+        sys.path.pop(0)
+
+
+def build_sdist(
+    sdist_directory: str,
+    config_settings: Optional[dict] = None,
+) -> str:
+    """Build a source distribution.
+
+    This will prepare a setup.py to run our extension-building logic, and
+    then build the distribution.
+
+    Args:
+        sdist_directory (str):
+            The directory where the source distribution will be placed.
+
+        config_settings (dict, optional):
+            Configuration settings to pass to Setuptools.
+
+    Returns:
+        str:
+        The basename for the generated source distribution file.
+    """
+    with _prepare_package_files():
+        return build_meta.build_sdist(sdist_directory,
+                                      config_settings)
+
+
+def build_wheel(
+    wheel_directory: str,
+    config_settings: Optional[dict] = None,
+    metadata_directory: Optional[str] = None,
+) -> str:
+    """Build a wheel.
+
+    This will prepare a setup.py to run our extension-building logic, and
+    then build the distribution.
+
+    Args:
+        wheel_directory (str):
+            The directory where the wheel will be placed.
+
+        config_settings (dict, optional):
+            Configuration settings to pass to Setuptools.
+
+    Returns:
+        str:
+        The basename for the generated wheel file.
+    """
+    with _prepare_package_files():
+        return build_meta.build_wheel(wheel_directory,
+                                      config_settings,
+                                      metadata_directory)
+
+
+get_requires_for_build_editable = build_meta.get_requires_for_build_editable
+get_requires_for_build_sdist = build_meta.get_requires_for_build_sdist
+get_requires_for_build_wheel = build_meta.get_requires_for_build_wheel
+build_editable = build_meta.build_editable
+prepare_metadata_for_build_editable = \
+    build_meta.prepare_metadata_for_build_editable
+prepare_metadata_for_build_wheel = build_meta.prepare_metadata_for_build_wheel
diff --git a/setup.py b/setup.py
index de6975bde2ed8c4e3d49616de7cbc5aa762cf09d..09a05b49e3328fd276b1fcb332dd454be1a3aa7f 100755
--- a/setup.py
+++ b/setup.py
@@ -485,6 +485,7 @@ setup(
         'elasticsearch2': ['elasticsearch~=2.0'],
         'elasticsearch5': ['elasticsearch~=5.0'],
         'elasticsearch7': ['elasticsearch~=7.0'],
+        'extension-packaging': ['setuptools>=74'],
         'ldap': ['python-ldap>=3.3.1'],
         'mercurial': ['mercurial'],
         'mysql': ['mysqlclient>=1.4,<=2.1.999'],
