diff --git a/.gitignore b/.gitignore
index 7cf3c475128f9335766d26f669da6f15086392de..4378c71224d7b7d03ac102738248d855b9d4f1be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,8 +21,6 @@ lextab.py
 yacctab.py
 .pytest_cache/*
 
-loopy/_git_rev.py
-
 .cache
 .env
 virtualenv-[0-9]*[0-9]
diff --git a/loopy/version.py b/loopy/version.py
index 09d8442a2ba00d38b96d53558e20e8d5f994856b..609e6c179352f702f55f4f7cbe25d09426f456e0 100644
--- a/loopy/version.py
+++ b/loopy/version.py
@@ -21,32 +21,16 @@ THE SOFTWARE.
 """
 
 
-# {{{ find install- or run-time git revision
+import re
+from importlib import metadata
 
-import os
 
+VERSION_TEXT = metadata.version("loopy")
+_match = re.match("^([0-9.]+)([a-z0-9]*?)$", VERSION_TEXT)
+assert _match is not None
+VERSION_STATUS = _match.group(2)
+VERSION = tuple(int(nr) for nr in _match.group(1).split("."))
 
-if os.environ.get("AKPYTHON_EXEC_IMPORT_UNAVAILABLE") is not None:
-    # We're just being exec'd by setup.py. We can't import anything.
-    _git_rev = None
-
-else:
-    import loopy._git_rev as _git_rev_mod  # pylint: disable=no-name-in-module,import-error  # noqa: E501
-    _git_rev = _git_rev_mod.GIT_REVISION
-
-    # If we're running from a dev tree, the last install (and hence the most
-    # recent update of the above git rev) could have taken place very long ago.
-    from pytools import find_module_git_revision
-    _runtime_git_rev = find_module_git_revision(__file__, n_levels_up=1)
-    if _runtime_git_rev is not None:
-        _git_rev = _runtime_git_rev
-
-# }}}
-
-
-VERSION = (2024, 1)
-VERSION_STATUS = ""
-VERSION_TEXT = ".".join(str(x) for x in VERSION) + VERSION_STATUS
 
 try:
     import islpy.version
@@ -62,8 +46,7 @@ except ImportError:
 else:
     _cgen_version = cgen.version.VERSION_TEXT
 
-DATA_MODEL_VERSION = "{}-islpy{}-cgen{}-{}-v1".format(
-        VERSION_TEXT, _islpy_version, _cgen_version, _git_rev)
+DATA_MODEL_VERSION = f"{VERSION_TEXT}-islpy{_islpy_version}-cgen{_cgen_version}-v1"
 
 
 FALLBACK_LANGUAGE_VERSION = (2018, 2)
diff --git a/pyproject.toml b/pyproject.toml
index 9dadd57f557f66b42e8044fad9a6114e39184823..7f0a38ba430094b4874de6cb5251adaea36c5a0d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,76 @@
+[build-system]
+build-backend = "setuptools.build_meta"
+requires = [
+    "setuptools>=63",
+]
+
+[project]
+name = "loopy"
+version = "2024.1"
+description = "A code generator for array-based code on CPUs and GPUs"
+readme = "README.rst"
+license = { text = "MIT" }
+authors = [
+    { name = "Andreas Kloeckner", email = "inform@tiker.net" },
+]
+requires-python = ">=3.8"
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "Intended Audience :: Developers",
+    "Intended Audience :: Other Audience",
+    "Intended Audience :: Science/Research",
+    "License :: OSI Approved :: MIT License",
+    "Natural Language :: English",
+    "Programming Language :: Python",
+    "Programming Language :: Python :: 3",
+    "Topic :: Scientific/Engineering",
+    "Topic :: Scientific/Engineering :: Information Analysis",
+    "Topic :: Scientific/Engineering :: Mathematics",
+    "Topic :: Scientific/Engineering :: Visualization",
+    "Topic :: Software Development :: Libraries",
+    "Topic :: Utilities",
+]
+dependencies = [
+    "pytools>=2024.1.5",
+    "pymbolic>=2022.1",
+    "genpy>=2016.1.2",
+
+    # https://github.com/inducer/loopy/pull/419
+    "numpy>=1.19",
+
+    "cgen>=2016.1",
+    "islpy>=2019.1",
+    "codepy>=2017.1",
+    "colorama",
+    "Mako",
+    "pyrsistent",
+    "immutables",
+    "typing_extensions",
+]
+[project.optional-dependencies]
+pyopencl = [
+    "pyopencl>=2022.3",
+]
+fortran = [
+    # Note that this is *not* regular 'f2py2e', this is
+    # the Fortran parser from the (unfinished) third-edition
+    # f2py, as linked below. This package is not on the package index, AFAIK.
+    # -AK, 2024-08-02
+    "f2py @ git+https://github.com/pearu/f2py.git",
+    "ply>=3.6",
+]
+
+[project.scripts]
+
+[project.urls]
+Documentation = "https://documen.tician.de/loopy"
+Homepage = "https://github.com/inducer/loopy"
+
+[tool.setuptools.packages.find]
+include = [
+    "loopy*",
+]
+
 
 [tool.ruff]
 preview = true
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 0cf58f83a0363ac1ba2f5b5cc130b75ca6c4e908..0000000000000000000000000000000000000000
--- a/setup.py
+++ /dev/null
@@ -1,130 +0,0 @@
-#!/usr/bin/env python
-
-import os
-
-from setuptools import find_packages, setup
-
-
-ver_dic = {}
-version_file = open("loopy/version.py")
-try:
-    version_file_contents = version_file.read()
-finally:
-    version_file.close()
-
-os.environ["AKPYTHON_EXEC_IMPORT_UNAVAILABLE"] = "1"
-exec(compile(version_file_contents, "loopy/version.py", "exec"), ver_dic)
-
-
-# {{{ capture git revision at install time
-
-# authoritative version in pytools/__init__.py
-def find_git_revision(tree_root):
-    # Keep this routine self-contained so that it can be copy-pasted into
-    # setup.py.
-
-    from os.path import abspath, exists, join
-    tree_root = abspath(tree_root)
-
-    if not exists(join(tree_root, ".git")):
-        return None
-
-    from subprocess import PIPE, STDOUT, Popen
-    p = Popen(["git", "rev-parse", "HEAD"], shell=False,
-              stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True,
-              cwd=tree_root)
-    (git_rev, _) = p.communicate()
-
-    git_rev = git_rev.decode()
-
-    git_rev = git_rev.rstrip()
-
-    retcode = p.returncode
-    assert retcode is not None
-    if retcode != 0:
-        from warnings import warn
-        warn("unable to find git revision", stacklevel=1)
-        return None
-
-    return git_rev
-
-
-def write_git_revision(package_name):
-    from os.path import dirname, join
-    dn = dirname(__file__)
-    git_rev = find_git_revision(dn)
-
-    with open(join(dn, package_name, "_git_rev.py"), "w") as outf:
-        outf.write('GIT_REVISION = "%s"\n' % git_rev)
-
-
-write_git_revision("loopy")
-
-# }}}
-
-
-setup(name="loopy",
-      version=ver_dic["VERSION_TEXT"],
-      description="A code generator for array-based code on CPUs and GPUs",
-      long_description=open("README.rst").read(),
-      classifiers=[
-          "Development Status :: 4 - Beta",
-          "Intended Audience :: Developers",
-          "Intended Audience :: Other Audience",
-          "Intended Audience :: Science/Research",
-          "License :: OSI Approved :: MIT License",
-          "Natural Language :: English",
-          "Programming Language :: Python",
-          "Programming Language :: Python :: 3",
-          "Topic :: Scientific/Engineering",
-          "Topic :: Scientific/Engineering :: Information Analysis",
-          "Topic :: Scientific/Engineering :: Mathematics",
-          "Topic :: Scientific/Engineering :: Visualization",
-          "Topic :: Software Development :: Libraries",
-          "Topic :: Utilities",
-          ],
-
-      python_requires="~=3.8",
-      install_requires=[
-          "pytools>=2024.1.5",
-          "pymbolic>=2022.1",
-          "genpy>=2016.1.2",
-
-          # https://github.com/inducer/loopy/pull/419
-          "numpy>=1.19",
-
-          "cgen>=2016.1",
-          "islpy>=2019.1",
-          "codepy>=2017.1",
-          "colorama",
-          "Mako",
-          "pyrsistent",
-          "immutables",
-          "typing_extensions",
-          ],
-
-      extras_require={
-          "pyopencl":  [
-              "pyopencl>=2022.3",
-              ],
-          "fortran":  [
-              # Note that this is *not* regular 'f2py2e', this is
-              # the Fortran parser from the (unfinished) third-edition
-              # f2py, as linked below.
-              "f2py>=0.3.1",
-              "ply>=3.6",
-              ],
-          },
-
-      dependency_links=[
-          "git+https://github.com/pearu/f2py.git"
-          ],
-
-      scripts=["bin/loopy"],
-
-      author="Andreas Kloeckner",
-      url="https://mathema.tician.de/software/loopy",
-      author_email="inform@tiker.net",
-      license="MIT",
-      packages=find_packages(),
-      )