r"""
Tools for integrating with `setuptools`_.
Integration with ``setuptools`` is a somewhat messy affair. By putting a
`cslug.CSlug` in a package, we now have to coerce our package setup to do
the following 5 things:
1. (Re)build all `cslug.CSlug`\ s on ``setup.py build``.
2. Include C source code in sdists but exclude them from wheels.
3. Include |cslug| type jsons and |../binaries| of the correct OS in wheels but
exclude them from sdists.
4. Mark |cslug| as a build-time dependency to be installed before trying to run
``setup.py build``.
5. Mark wheels as platform dependent but Python version independent.
"""
from distutils.command.build import build as _build
from cslug._cslug import SUFFIX as CSLUG_SUFFIX
[docs]
def make(*names):
r"""Import and call `cslug.CSlug.make`.
:param names: Names of `cslug.CSlug`\ s.
:type names: str
The syntax for a `cslug.CSlug` name is "module_name:attribute_name".
For example, ``make("foo.bar:pop.my_slug")`` is equivalent to::
from foo.bar import pop
pop.my_slug.make()
Of course, ``foo.bar`` must be importable for this to work.
Multiple strings may be passed as arguments - this is just a lazy for loop.
``make("foo.slug", "bar.other_slug")`` is equivalent to::
make("foo.slug")
make("bar.other_slug")
"""
import importlib
import operator
import sys
import os
sys.path.insert(0, os.getcwd())
for name in names:
import_, *attrs = name.split(":")
assert len(attrs)
mod = importlib.import_module(import_)
operator.attrgetter(".".join(attrs))(mod).make()
# Trying to properly coverage trace these is too much hassle.
[docs]
def build_slugs(*names, base=_build): # pragma: no cover
"""
Overload the ``run()`` method of a distutils build class to
additionally call `cslug.building.make`.
:param names: Names to be passed to `cslug.building.make`.
:param base: An alternative base class to inherit from.
:return: A modified subclass of **base**.
"""
class build(base):
def run(self):
make(*names)
super().run()
return build
try: # pragma: no cover
try:
from setuptools.command.bdist_wheel import bdist_wheel as _bdist_wheel
from setuptools.command.bdist_wheel import get_platform as _get_platform
except ImportError:
from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
from wheel.bdist_wheel import get_platform as _get_platform
except ImportError: # pragma: no cover
# Provide a self-explanatory error message on `setup.py bdist_wheel` if the
# user doesn't have wheel installed.
from distutils.cmd import Command
class bdist_wheel(Command):
user_options = []
def finalize_options(self) -> None:
pass
def initialize_options(self) -> None:
pass
def run(self):
raise SystemExit(
"ERROR: The bdist_wheel command requires setuptools >= 70.1. "
"Please `pip install -U setuptools` then try again.")
else:
[docs]
class bdist_wheel(_bdist_wheel):
"""``wheel.bdist_wheel`` with platform dependent but Python independent
tags.
In addition to setting the tags, also prevent setuptools's build cache
from leaking binaries for the wrong platform into the wheel. See
:meth:`run` for details.
"""
[docs]
def finalize_options(self): # pragma: no cover
"""Set platform dependent wheel tag.
|cslug| packages contain binaries but they don't use
:c:`#include <Python.h>` like traditional Python extensions do.
This makes wheels dependent OS but not Python version dependent.
"""
# Tag setting is done by ``bdist_wheel.get_tag()`` later in the
# build but overloading that means reimplementing some rather
# complex platform normalisation. Injecting the platform name here,
# before the normalisation, ensures that it gets normalised.
# Setting ``plat_name`` is equivalent to using the ``--plat`` CLI
# option.
import platform
self.plat_name = _get_platform(self.bdist_dir)
if platform.system() == "Darwin":
# If on mac, deal with the x86_64/arm64/universal2 shenanigans
# and the deployment target.
self.plat_name = _macos_platform_tag(self.plat_name)
super().finalize_options()
[docs]
def run(self): # pragma: no cover
"""Additionally run ``setup.py clean --all`` before building the
wheel.
Setuptools's caching can cause files for the wrong platform to be
collected if you build wheels for the two platforms on the same
filesystem. This can happen by switching from 64 to 32 bit
Python, using Docker, dual booting or any kind of cross compiling.
Forcing a clean will ensure that no files are included in wheels
which they shouldn't be.
.. versionchanged:: v0.4.0
Add this force-cleaning step.
"""
clean = self.distribution.get_command_obj("clean")
clean.all = True
clean.verbose = 0
self.run_command("clean")
super().run()
def _macos_platform_tag(tag):
"""Correct the macOS platform tag that wheel assigns wheels by default."""
# `wheel` assumes that the macOS target version is the same as the
# target version of Python. This is not the case for cslug because it does
# not use the compile args from sysconfig like Python extension modules do.
# The correct version is whatever cslug passed to gcc's
# -mmacosx-version-min parameter.
import re
import platform
from cslug._cc import mmacosx_version_min, macos_architecture
macos_version = mmacosx_version_min().replace(".", "_")
tag = re.sub(r"macosx[-_]\d+[._]\d+[-_]", f"macosx_{macos_version}_", tag)
# If macOS binaries were either cross compiled or compiled "fat"
# (contains two architectures in one binary) then the architecture
# tag needs to reflect that.
arch = macos_architecture() or platform.machine()
tag = re.sub("x86_64|arm64|universal2", arch, tag)
return tag
[docs]
def copy_requirements(path="pyproject.toml", exclude=()):
"""
Parse the *build-system: requires* list from a :pep:`PEP518 pyproject.toml
<0518#build-system-table>`.
:param path: Specify an alternative toml file to parse from, defaults to
``'pyproject.toml'``.
:type path: str or os.PathLike or io.TextIOBase
:param exclude: Requirements to exclude, use to remove build only
requirements.
:type exclude: Iterable[str]
:return:
:rtype: list[str]
.. note::
This function requires toml_. To use, you must mark ``'toml'`` as
a build dependency, or if you use the ``--no-build-isolation`` option
with pip, have toml_ installed.
.. note::
toml_ wheel and setuptools are always excluded. If you want to re-add
them then append them after::
copy_requirements() + ["toml"]
"""
import re
import toml
from cslug.misc import read, flatten
exclude = flatten(exclude) + ["wheel", "setuptools", "toml"]
conf = toml.loads(read(path)[0])
requirements = conf["build-system"]["requires"]
sep_re = re.compile(r"[<>=|&!\s]")
return [i for i in requirements if sep_re.split(i)[0] not in exclude]