On Thu, 19 Oct 2023 at 10:41, Bartosz Golaszewski <brgl@xxxxxxxx> wrote: > > On Wed, Oct 18, 2023 at 10:57 PM Phil Howard <phil@xxxxxxxxxxxxx> wrote: > > > > Optionally vendor libgpiod source into sdist so that the Python module can > > be built from source, even with a missing or mismatched system libgpiod. > > > > Add two new environment variables "LINK_SYSTEM_LIBGPIOD" and > > "GPIOD_VERSION" to control what kind of package setup.py will build. > > > > In order to build an sdist or wheel package with a vendored libgpiod a > > version must be specified via the "GPIOD_VERSION" environment variable. > > > > This will instruct setup.py to fetch the tarball matching the requested > > version from git.kernel.org, unpack it, and copy the lib and include > > directories into the package root so they can be included in sdist or used > > to build a binary wheel. > > > > eg: GPIOD_VERSION=2.0.2 python3 setup.py sdist > > > > Will build a source distribution with gpiod version 2.0.2 source included. > > > > It will also save the gpiod version into "gpiod-version.txt" so that it can > > be passed to the build when the sdist is built by pip. > > > > Requiring an explicit version ensures that the Python bindings - which > > can be changed and versions independent of libgpiod - are built against a > > stable libgpiod release. > > > > In order to force a package with vendored gpiod source to link the system > > libgpiod, the "LINK_SYSTEM_LIBGPIOD" environment variable can be used: > > > > eg: LINK_SYSTEM_LIBGPIOD=1 pip install libgpiod > > > > Signed-off-by: Phil Howard <phil@xxxxxxxxxxxxx> > > --- > > bindings/python/MANIFEST.in | 5 ++ > > bindings/python/setup.py | 131 +++++++++++++++++++++++++++++++++--- > > 2 files changed, 125 insertions(+), 11 deletions(-) > > > > 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@xxxxxxxxxx> > > > > 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@xxxxxxxx> > > > > -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 > > Doesn't it make our setup.py depend on additional packages on top of > the standard library? "tarfile" and "urllib" are both part of the CPython standard library [1] They'd be in pip otherwise, since it would necessarily need to fetch and unpack tarballs like the sdist. (pip has a habit of vendoring things) If there are cases where these are missing (Yocto weirdness?) then I can move the imports to just-in-time so they're a soft dependency only for when a package is built. [1] - https://docs.python.org/3/library/index.html > > > + > > +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") > > I'd call it LIBGPIOD_VERSION to be more explicit. > > > +GPIOD_WITH_TESTS = getenv("GPIOD_WITH_TESTS") == "1" > > +SRC_BASE_URL = "https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/snapshot/" > > I would prefer to use the http mirror at > https://mirrors.edge.kernel.org/pub/software/libs/libgpiod/ rather > than git snapshots. We have better control over what's in there Yessir! > and we can even verify the checksums after download if needed. We can pull the checksum from sha256sums.asc and verify with hashlib but we can't verify the gpg signature without a dependency I think. > > > +SRC_FILENAME = "libgpiod-{version}.tar.gz" > > Maybe put it in /tmp? Sure "tempfile" is also in the stl, so might as well use it! > > Bart > > > + > > +# __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@xxxxxxxx", > > -- > > 2.34.1 > >