mirror of
https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git
synced 2026-10-24 00:48:53 +03:00
Previously, a number of places in setup.py did not use a context manager
to manage the lifetime of the underlying file descriptor for reading and
writing the `libgpiod-version.txt` file.
While the previous method works, it relies on the returned file object
being assigned to a temporary Python object that has its refcount
decremented immediately after statement execution. This then relies on
the file object's finalizer to close the underlying handle which can
emit ResourceWarnings:
/bindings/python/setup.py:193: ResourceWarning: unclosed file <_io.TextIOWrapper name='libgpiod-version.txt' mode='w' encoding='UTF-8'>
open("libgpiod-version.txt", "w").write(LIBGPIOD_VERSION)
Now, we specifically use a context manager to close the file descriptor
when done to explicitly manage its lifetime.
Signed-off-by: Vincent Fazio <vfazio@gmail.com>
Link: https://patch.msgid.link/20261020003728.13669-5-vfazio@gmail.com
Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
288 lines
11 KiB
Python
288 lines
11 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
|
|
|
|
from collections.abc import Callable
|
|
from logging import getLogger
|
|
from os import getenv, path, unlink
|
|
from shutil import copy, copytree, rmtree
|
|
from typing import TypeVar, cast
|
|
|
|
from setuptools import Extension, setup
|
|
from setuptools.command.build_ext import build_ext as orig_build_ext
|
|
from setuptools.command.sdist import sdist as orig_sdist
|
|
from setuptools.errors import BaseError
|
|
|
|
T = TypeVar("T", "sdist", "build_ext")
|
|
|
|
log = getLogger(__name__)
|
|
|
|
LINK_SYSTEM_LIBGPIOD = getenv("LINK_SYSTEM_LIBGPIOD") == "1"
|
|
LIBGPIOD_MINIMUM_VERSION = "2.1"
|
|
LIBGPIOD_VERSION = getenv("LIBGPIOD_VERSION")
|
|
SRC_BASE_URL = "https://mirrors.edge.kernel.org/pub/software/libs/libgpiod/"
|
|
TAR_FILENAME = "libgpiod-{version}.tar.gz"
|
|
ASC_FILENAME = "sha256sums.asc"
|
|
SHA256_CHUNK_SIZE = 2048
|
|
LICENSE_FILE = "LICENSE"
|
|
|
|
|
|
def sha256(filename: str) -> str:
|
|
"""
|
|
Return a sha256sum for a specific filename, loading the file in chunks
|
|
to avoid potentially excessive memory use.
|
|
"""
|
|
from hashlib import sha256
|
|
|
|
sha256sum = sha256()
|
|
with open(filename, "rb") as f:
|
|
for chunk in iter(lambda: f.read(SHA256_CHUNK_SIZE), b""):
|
|
sha256sum.update(chunk)
|
|
|
|
return sha256sum.hexdigest()
|
|
|
|
|
|
def find_sha256sum(asc_file: str, tar_filename: str) -> str:
|
|
"""
|
|
Search through a local copy of sha256sums.asc for a specific filename
|
|
and return the associated sha256 sum.
|
|
"""
|
|
with open(asc_file) as f:
|
|
for line in f:
|
|
_line = line.strip().split(" ")
|
|
if len(_line) == 2 and _line[1] == tar_filename:
|
|
return _line[0]
|
|
|
|
raise BaseError(f"no signature found for {tar_filename}")
|
|
|
|
|
|
def fetch_tarball(func: Callable[[T], None]) -> Callable[[T], None]:
|
|
"""
|
|
Verify the requested LIBGPIOD_VERSION tarball exists in sha256sums.asc,
|
|
fetch it from https://mirrors.edge.kernel.org/pub/software/libs/libgpiod/
|
|
and verify its sha256sum.
|
|
|
|
If the check passes, extract the tarball and copy the lib and include
|
|
dirs into our source tree.
|
|
"""
|
|
|
|
# If no LIBGPIOD_VERSION is specified in env, just run the command
|
|
if LIBGPIOD_VERSION is None:
|
|
return func
|
|
|
|
# If LIBGPIOD_VERSION is specified, apply the tarball wrapper
|
|
def wrapper(cmd: T) -> None:
|
|
# Just-in-time import of tarfile and urllib.request so these are
|
|
# not required for Yocto to build a vendored or linked package
|
|
import sys
|
|
import tarfile
|
|
from tempfile import TemporaryDirectory
|
|
from urllib.request import urlretrieve
|
|
|
|
from packaging.version import Version
|
|
|
|
# The "build" frontend will run setup.py twice within the same
|
|
# temporary output directory. First for "sdist" and then for "wheel"
|
|
# This would cause the build to fail with dirty "lib" and "include"
|
|
# directories.
|
|
# If the version in "libgpiod-version.txt" already matches our
|
|
# requested tarball, then skip the fetch altogether.
|
|
try:
|
|
with open("libgpiod-version.txt") as f:
|
|
if f.read() == LIBGPIOD_VERSION:
|
|
log.info("skipping tarball fetch")
|
|
func(cmd)
|
|
return
|
|
except OSError:
|
|
pass
|
|
|
|
# Early exit for build tree with dirty lib/include dirs
|
|
for check_dir in "lib", "include":
|
|
if path.isdir(f"./{check_dir}"):
|
|
raise BaseError(f"refusing to overwrite ./{check_dir}")
|
|
|
|
with TemporaryDirectory(prefix="libgpiod-") as temp_dir:
|
|
tarball_filename = TAR_FILENAME.format(version=LIBGPIOD_VERSION)
|
|
tarball_url = f"{SRC_BASE_URL}{tarball_filename}"
|
|
asc_url = f"{SRC_BASE_URL}{ASC_FILENAME}"
|
|
|
|
log.info(f"fetching: {asc_url}")
|
|
|
|
asc_filename, _ = urlretrieve(asc_url, path.join(temp_dir, ASC_FILENAME))
|
|
|
|
tarball_sha256 = find_sha256sum(asc_filename, tarball_filename)
|
|
|
|
if Version(LIBGPIOD_VERSION) < Version(LIBGPIOD_MINIMUM_VERSION):
|
|
raise BaseError(f"requires libgpiod>={LIBGPIOD_MINIMUM_VERSION}")
|
|
|
|
log.info(f"fetching: {tarball_url}")
|
|
|
|
downloaded_tarball, _ = urlretrieve(
|
|
tarball_url, path.join(temp_dir, tarball_filename)
|
|
)
|
|
|
|
log.info(f"verifying: {tarball_filename}")
|
|
if sha256(downloaded_tarball) != tarball_sha256:
|
|
raise BaseError(f"signature mismatch for {tarball_filename}")
|
|
|
|
# Unpack the downloaded tarball
|
|
log.info(f"unpacking: {tarball_filename}")
|
|
with tarfile.open(downloaded_tarball) as f:
|
|
if sys.version_info < (3, 12):
|
|
f.extractall(temp_dir)
|
|
else:
|
|
f.extractall(temp_dir, filter=tarfile.fully_trusted_filter)
|
|
|
|
# Copy the include and lib directories we need to build libgpiod
|
|
base_dir = path.join(temp_dir, f"libgpiod-{LIBGPIOD_VERSION}")
|
|
copytree(path.join(base_dir, "include"), "./include")
|
|
copytree(path.join(base_dir, "lib"), "./lib")
|
|
|
|
# Since the license file is not in the bindings directory, we cannot
|
|
# use the PEP-639 `license-files` property since the `Distribution`
|
|
# class is constructed during `setup`, parses pyproject.toml, and
|
|
# searches for the license file before we can copy files in to the
|
|
# directory*. If the file is not there, we get an error.
|
|
#
|
|
# To work around this, we manually copy the license file to the
|
|
# current directory and insert the license file into the metadata of
|
|
# the `Distribution` prior to the cmdclass building the distribution.
|
|
#
|
|
# Copying the file as `LICENSE` allows wheels built off of the sdist
|
|
# to automatically include the LICENSE, see:
|
|
# https://setuptools.pypa.io/en/latest/userguide/miscellaneous.html
|
|
#
|
|
# *Note: this is not technically true, but it would require doing
|
|
# something more complex like a custom build backend or to wrap
|
|
# `setup` or the `Distrubtion` object which get invoked/created
|
|
# multiple times and could fetch the tarball sources multiple times.
|
|
# Further, we only really care about the license file when building
|
|
# the released distribution files (sdist and wheels).
|
|
|
|
# Read the SPDX license identifier from pyproject.toml
|
|
license_file = None
|
|
with open("pyproject.toml", "rb") as f:
|
|
if sys.version_info >= (3, 11):
|
|
import tomllib
|
|
|
|
project = cast("dict[str,str]", tomllib.load(f).get("project"))
|
|
license_file = project.get("license")
|
|
else: # tomllib isn't standard, fall back to parsing by line
|
|
for line in f.readlines():
|
|
_line = line.decode()
|
|
if _line.startswith("license = "):
|
|
license_file = _line.split("=")[1].strip(' \r\n"')
|
|
break
|
|
|
|
if license_file: # Do not allow empty string or None
|
|
_path = path.join(base_dir, "LICENSES", f"{license_file}.txt")
|
|
if path.isfile(_path):
|
|
# manually set the license list for the distribution after
|
|
# the license file has been copied into the build directory.
|
|
#
|
|
# Note: that this must be in place prior to `egg_info.run`.
|
|
# For further details, see `egg_info.find_sources` and
|
|
# `manifest_maker.add_license_files`
|
|
copy(_path, LICENSE_FILE)
|
|
cmd.distribution.metadata.license_files = [LICENSE_FILE] # type: ignore[attr-defined]
|
|
|
|
# Save the libgpiod version for sdist
|
|
with open("libgpiod-version.txt", "w") as f:
|
|
f.write(LIBGPIOD_VERSION)
|
|
|
|
# Run the command
|
|
func(cmd)
|
|
|
|
# Clean up the build directory
|
|
if path.exists(LICENSE_FILE):
|
|
unlink(LICENSE_FILE)
|
|
rmtree("./lib", ignore_errors=True)
|
|
rmtree("./include", ignore_errors=True)
|
|
unlink("libgpiod-version.txt")
|
|
|
|
return wrapper
|
|
|
|
|
|
class build_ext(orig_build_ext):
|
|
"""
|
|
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 LIBGPIOD_VERSION is specified, eg:
|
|
|
|
LIBGPIOD_VERSION="2.0.2" python3 -m build .
|
|
"""
|
|
|
|
@fetch_tarball
|
|
def run(self) -> None:
|
|
# Try to get the gpiod version from the .txt file included in sdist
|
|
libgpiod_version: str | None
|
|
try:
|
|
with open("libgpiod-version.txt") as f:
|
|
libgpiod_version = f.read()
|
|
except OSError:
|
|
libgpiod_version = LIBGPIOD_VERSION
|
|
|
|
if libgpiod_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_API_VERSION_STR="{libgpiod_version}"',
|
|
)
|
|
|
|
super().run()
|
|
|
|
|
|
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 LIBGPIOD_VERSION is specified, eg:
|
|
|
|
LIBGPIOD_VERSION="2.0.2" python3 -m build . --sdist
|
|
"""
|
|
|
|
@fetch_tarball
|
|
def run(self) -> None:
|
|
super().run()
|
|
|
|
|
|
gpiod_ext = Extension(
|
|
"gpiod._ext",
|
|
sources=[
|
|
"gpiod/ext/chip.c",
|
|
"gpiod/ext/common.c",
|
|
"gpiod/ext/line-config.c",
|
|
"gpiod/ext/line-settings.c",
|
|
"gpiod/ext/module.c",
|
|
"gpiod/ext/request.c",
|
|
],
|
|
define_macros=[("_GNU_SOURCE", "1")],
|
|
libraries=["gpiod"],
|
|
extra_compile_args=["-Wall", "-Wextra"],
|
|
)
|
|
|
|
setup(
|
|
ext_modules=[gpiod_ext],
|
|
cmdclass={"build_ext": build_ext, "sdist": sdist},
|
|
)
|