diff --git a/bindings/python/MANIFEST.in b/bindings/python/MANIFEST.in
index c7124d4..0aa9079 100644
--- a/bindings/python/MANIFEST.in
+++ b/bindings/python/MANIFEST.in
@@ -2,6 +2,7 @@
 # SPDX-FileCopyrightText: 2023 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
 
 include setup.py
+include gpiod-version.txt
 
 recursive-include gpiod *.py
 recursive-include tests *.py
@@ -11,3 +12,7 @@ recursive-include gpiod/ext *.h
 
 recursive-include tests/gpiosim *.c
 recursive-include tests/procname *.c
+
+recursive-include lib *.c
+recursive-include lib *.h
+recursive-include include *.h
diff --git a/bindings/python/setup.py b/bindings/python/setup.py
index df10e18..f0d5c1f 100644
--- a/bindings/python/setup.py
+++ b/bindings/python/setup.py
@@ -1,24 +1,136 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
 
-from os import environ, path
-from setuptools import setup, Extension, find_packages
+import tarfile
+from os import getenv, path, unlink
+from shutil import copytree, rmtree
+from urllib.request import urlretrieve
+
+from setuptools import Extension, find_packages, setup
 from setuptools.command.build_ext import build_ext as orig_build_ext
-from shutil import rmtree
+from setuptools.command.sdist import sdist as orig_sdist
+
+LINK_SYSTEM_LIBGPIOD = getenv("LINK_SYSTEM_LIBGPIOD") == "1"
+GPIOD_VERSION = getenv("GPIOD_VERSION")
+GPIOD_WITH_TESTS = getenv("GPIOD_WITH_TESTS") == "1"
+SRC_BASE_URL = "https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/snapshot/"
+SRC_FILENAME = "libgpiod-{version}.tar.gz"
+
+# __version__
+with open("gpiod/version.py", "r") as fd:
+    exec(fd.read())
+
+
+def fetch_tarball(func):
+    # If no GPIOD_VERSION is specified in env, just run the task
+    if GPIOD_VERSION is None:
+        return func
+
+    # If GPIOD_VERSION is specified, fetch the requested version tarball
+    # and create gpiod-version.txt so the sdist package knows what version
+    # it's building.
+    def wrapper(self):
+        TARBALL = SRC_FILENAME.format(version=GPIOD_VERSION)
+
+        print(f"fetching: {SRC_BASE_URL + TARBALL}")
+
+        filename, headers = urlretrieve(SRC_BASE_URL + TARBALL, TARBALL)
+
+        if not tarfile.is_tarfile(filename):
+            print(f"error: refusing to build sdist (invalid tarball {TARBALL})")
+            return
+
+        # Unpack the downloaded tarball
+        print(f"unpacking: {filename}")
+        file = tarfile.open(filename)
+        file.extractall(".")
+        file.close()
+        unlink(filename)
+
+        # Copy the include and lib directories we need to build libgpiod
+        copytree(f"libgpiod-{GPIOD_VERSION}/include/", "./include")
+        copytree(f"libgpiod-{GPIOD_VERSION}/lib/", "./lib")
+        rmtree(f"libgpiod-{GPIOD_VERSION}")
+
+        # Save the gpiod version for sdist
+        open("gpiod-version.txt", "w").write(GPIOD_VERSION)
+
+        func(self)
+
+        rmtree("./lib", ignore_errors=True)
+        rmtree("./include", ignore_errors=True)
+        unlink("gpiod-version.txt")
+
+    return wrapper
 
 
 class build_ext(orig_build_ext):
     """
-    setuptools install all C extentions even if they're excluded in setup().
-    As a workaround - remove the tests directory right after all extensions
-    were built (and possibly copied to the source directory if inplace is set).
+    Wrap build_ext to amend the module sources and settings to build
+    the bindings and gpiod into a combined module when a version is
+    specified and LINK_SYSTEM_LIBGPIOD=1 is not present in env.
+
+    run is wrapped with @fetch_tarball in order to fetch the sources
+    needed to build binary wheels when GPIOD_VERSION is specified, eg:
+
+    GPIOD_VERSION="2.0.2" python3 -m build .
     """
 
+    @fetch_tarball
     def run(self):
+        # Try to get the gpiod version from the .txt file included in sdist
+        try:
+            gpiod_version = open("gpiod-version.txt", "r").read()
+        except OSError:
+            gpiod_version = GPIOD_VERSION
+
+        if gpiod_version and not LINK_SYSTEM_LIBGPIOD:
+            # When building the extension from an sdist with a vendored
+            # amend gpiod._ext sources and settings accordingly.
+            gpiod_ext = self.ext_map["gpiod._ext"]
+            gpiod_ext.sources += [
+                "lib/chip.c",
+                "lib/chip-info.c",
+                "lib/edge-event.c",
+                "lib/info-event.c",
+                "lib/internal.c",
+                "lib/line-config.c",
+                "lib/line-info.c",
+                "lib/line-request.c",
+                "lib/line-settings.c",
+                "lib/misc.c",
+                "lib/request-config.c",
+            ]
+            gpiod_ext.libraries = []
+            gpiod_ext.include_dirs = ["include", "lib", "gpiod/ext"]
+            gpiod_ext.extra_compile_args.append(
+                f'-DGPIOD_VERSION_STR="{gpiod_version}"',
+            )
+
         super().run()
+
+        # We don't ever want the module tests directory in our package
+        # since this might include gpiosim._ext or procname._ext from a
+        # previous dirty build tree.
         rmtree(path.join(self.build_lib, "tests"), ignore_errors=True)
 
 
+class sdist(orig_sdist):
+    """
+    Wrap sdist in order to fetch the libgpiod source files for vendoring
+    into a source distribution.
+
+    run is wrapped with @fetch_tarball in order to fetch the sources
+    needed to build binary wheels when GPIOD_VERSION is specified, eg:
+
+    GPIOD_VERSION="2.0.2" python3 -m build . --sdist
+    """
+
+    @fetch_tarball
+    def run(self):
+        super().run()
+
+
 gpiod_ext = Extension(
     "gpiod._ext",
     sources=[
@@ -50,19 +162,16 @@ procname_ext = Extension(
 )
 
 extensions = [gpiod_ext]
-if environ.get("GPIOD_WITH_TESTS") == "1":
+if GPIOD_WITH_TESTS:
     extensions.append(gpiosim_ext)
     extensions.append(procname_ext)
 
-with open("gpiod/version.py", "r") as fd:
-    exec(fd.read())
-
 setup(
     name="libgpiod",
     packages=find_packages(exclude=["tests", "tests.*"]),
     python_requires=">=3.9.0",
     ext_modules=extensions,
-    cmdclass={"build_ext": build_ext},
+    cmdclass={"build_ext": build_ext, "sdist": sdist},
     version=__version__,
     author="Bartosz Golaszewski",
     author_email="brgl@bgdev.pl",
