From 20e168f91742be16fb5d0f7dc8e1a910238f2eb6 Mon Sep 17 00:00:00 2001 From: Daniel Zullo Date: Fri, 16 Oct 2026 10:37:08 +0200 Subject: [PATCH] Fix platform tags for libfaketime wheels The universal wheel lets ARM64 pip install an x86-64 library that cannot be loaded. Platform tags let pip reject that wheel and use a compatible wheel or build from source instead. Keep the library beside its Python package and declare the build dependencies needed for macOS platform tags. Fixes #94 --- CHANGELOG.md | 1 + README.md | 4 ++++ pyproject.toml | 4 ++++ requirements.txt | 2 ++ setup.py | 24 +++++++++++++++++++++-- test/test_wheel.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 test/test_wheel.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ea8e3..edd23b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Changelog ----- released ... +- tag wheels for their build platform so pip does not install an incompatible shared library on ARM64 - breaking: drop support for python 3.9 and 3.10 - add support for python 3.14 - upgrade vendored libfaketime to 0.9.13 without modifications, fixing build failures with recent glibc versions and compilers defaulting to ISO C23 diff --git a/README.md b/README.md index 3b9fd4e..a75d2f6 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ Installation $ pip install libfaketime ``` +Wheels contain a native library and are platform-specific. If a compatible wheel +is not available, installation builds the library from source and requires `make` +and a C compiler. + Usage ----- diff --git a/pyproject.toml b/pyproject.toml index 6d24c6c..91a8b5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools>=70.1.0", "wheel"] +build-backend = "setuptools.build_meta" + [tool.pytest.ini_options] filterwarnings = [ # utcnow() is still covered on purpose: it remains supported and used diff --git a/requirements.txt b/requirements.txt index ac49ba8..0caa8b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ mock==5.1.0 pytest==8.2.0 python-dateutil==2.9.0post0 pytz==2024.1 +setuptools>=70.1.0 tox==4.15.0 +wheel diff --git a/setup.py b/setup.py index df95baf..841249c 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,10 @@ import subprocess import sys +from setuptools import Distribution from setuptools import find_packages from setuptools import setup +from setuptools.command.bdist_wheel import bdist_wheel from setuptools.command.install import install # This hack is from http://stackoverflow.com/a/7071358/1231454; @@ -36,6 +38,23 @@ faketime_lib = os.path.join(_vendor_path, "src", libname) +class BinaryDistribution(Distribution): + """A distribution containing a platform-specific shared library.""" + + def has_ext_modules(self) -> bool: + """Use the platform library directory for the bundled native library.""" + return True + + +class PlatformWheel(bdist_wheel): + """A platform-specific wheel without a Python ABI dependency.""" + + def get_tag(self) -> tuple[str, str, str]: + """Keep Python compatibility while selecting the build platform.""" + _, _, platform_tag = super().get_tag() + return "py3", "none", platform_tag + + class CustomInstall(install): def run(self): self.my_outputs = [] @@ -55,7 +74,7 @@ def run(self): elif sys.platform == "darwin": subprocess.check_call(["make", "-C", _vendor_path]) - dest = os.path.join(self.install_purelib, os.path.dirname(faketime_lib)) + dest = os.path.join(self.install_lib, os.path.dirname(faketime_lib)) try: os.makedirs(dest) except OSError as e: @@ -106,7 +125,8 @@ def get_outputs(self): ], include_package_data=True, zip_safe=False, - cmdclass={"install": CustomInstall}, + distclass=BinaryDistribution, + cmdclass={"bdist_wheel": PlatformWheel, "install": CustomInstall}, entry_points={ "console_scripts": [ "python-libfaketime = libfaketime:main", diff --git a/test/test_wheel.py b/test/test_wheel.py new file mode 100644 index 0000000..87e4880 --- /dev/null +++ b/test/test_wheel.py @@ -0,0 +1,47 @@ +import subprocess +import sys +from email.parser import BytesParser +from pathlib import Path +from zipfile import ZipFile + + +def test_wheel_is_platform_specific(tmp_path: Path) -> None: + """Check wheel compatibility tags and the bundled library's location. + + Args: + tmp_path: Directory for the built wheel. + + """ + root = Path(__file__).resolve().parents[1] + subprocess.run( + [ + sys.executable, + "-m", + "pip", + "wheel", + "--no-deps", + "--no-build-isolation", + "--wheel-dir", + str(tmp_path), + str(root), + ], + check=True, + timeout=180, + ) + [wheel] = tmp_path.glob("libfaketime-*.whl") + _, python_tag, abi_tag, platform_tag = wheel.stem.rsplit("-", 3) + assert python_tag == "py3" + assert abi_tag == "none" + assert platform_tag != "any" + + library_name = ( + "libfaketime.1.dylib" if sys.platform == "darwin" else "libfaketime.so.1" + ) + with ZipFile(wheel) as archive: + [metadata_path] = ( + name for name in archive.namelist() if name.endswith(".dist-info/WHEEL") + ) + metadata = BytesParser().parsebytes(archive.read(metadata_path)) + assert metadata["Root-Is-Purelib"] == "false" + library = archive.getinfo(f"libfaketime/vendor/libfaketime/src/{library_name}") + assert library.file_size > 0