20

I want to build a C extension for CPython. I could do it traditionally with a setup.py file. However, for the reasons mentioned in PEP 517, I would prefer a declarative approach using a pyproject.toml. I understand that setuptools is the only build backend that can build C extensions on all relevant platforms. In fact, I am unaware of any backend capable of building C extensions at all alongside the outdated distutils.

Against this background, a common setup.py would look like this:

from setuptools import setup, Extension
kwargs = dict(
    name='mypackage',
    # more metadata
    ext_modules=[
        Extension('mypackage.mymodule', ['lib/mymodule.c',
                                         'lib/mypackage.c',
                                         'lib/myalloc.c'],
                  include_dirs=['lib'],
                  py_limited_api=True)])

setup(**kwargs)

Now, the challenge is to put the above into a pyproject.toml plus a setup.cfg.

The setuptools docs suggest a pyproject.toml like this:

[build-system]
requires = [
    "setuptools >=52.0",
        'wheel >= 0.36']
build-backend = "setuptools.build_meta"

See

Further, the actual metadata should go into setup.cfg. However, I haven't found any explanation on how to translate the ext_modules kwarg, in particular the Extension() call, into setup.cfg syntax.

EvgenKo423
  • 2,256
  • 2
  • 16
  • 23
Dr Leo
  • 379
  • 2
  • 5
  • So what is blocking? What is your current status? Any error message? – sinoroc Feb 11 '21 at 19:05
  • 2
    My status is: blocked by lack of documentation on how to declare the C extension in the setup.cfg file. I thought about a [build_ext] section, but didn't find any docs either. So I guess I am a few steps away from attempting a build. – Dr Leo Feb 13 '21 at 12:25
  • 1
    Looks like it is not supported (yet): https://github.com/pypa/setuptools/issues/2220 – sinoroc Feb 13 '21 at 15:44

2 Answers2

13

pyproject.toml is not strictly meant to replace setup.py, but rather to ensure its correct execution if it's still needed (see PEP 517 and my answer here):

Where the build-backend key exists, this takes precedence and the source tree follows the format and conventions of the specified backend (as such no setup.py is needed unless the backend requires it). Projects may still wish to include a setup.py for compatibility with tools that do not use this spec.

While setuptools plans to move everything from a script into a config file, it's not always possible:

There are two types of metadata: static and dynamic.

  • Static metadata (setup.cfg): guaranteed to be the same every time. This is simpler, easier to read, and avoids many common errors, like encoding errors.
  • Dynamic metadata (setup.py): possibly non-deterministic. Any items that are dynamic or determined at install-time, as well as extension modules or extensions to setuptools, need to go into setup.py.

Static metadata should be preferred and dynamic metadata should be used only as an escape hatch when absolutely necessary.

In fact, setuptools will still use an imaginary setup.py if it doesn't exist.

Note: Since version 61.0.0 setuptools allows to specify project metadata and other configuration options in pyproject.toml file. Its use looks more attractive as this file has another, more useful function and allows to specify most of the metadata in standardized, tool-agnostic way.


With that being said, if you want to stick as much as possible to a static way, you could move everything you can into pyproject.toml file and leave the rest in setup.py for now:

pyproject.toml
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[project]
name = "mypackage"
# more metadata
setup.py
from setuptools import setup, Extension

setup_args = dict(
    ext_modules = [
        Extension(
            'mypackage.mymodule',
            ['lib/mymodule.c', 'lib/mypackage.c', 'lib/myalloc.c'],
            include_dirs = ['lib'],
            py_limited_api = True
        )
    ]
)
setup(**setup_args)
EvgenKo423
  • 2,256
  • 2
  • 16
  • 23
  • Does this still work? – not2qubit Nov 12 '22 at 02:50
  • 2
    @not2qubit Yes, it does. The [setuptools documentation suggests the same](https://setuptools.pypa.io/en/latest/userguide/ext_modules.html). – EvgenKo423 Nov 12 '22 at 08:14
  • This look a little cleaner than Chandan's answer. – not2qubit Nov 13 '22 at 12:48
  • I got this to work, and much better when using a `src` structure, instead of a flat structure. The only thing that doesn't work is asking for a different compiler when trying to use `python -m build ...` or `pip install ..`. Only thing that works in the *deprecated* method of `python setup.py build --compiler=mingw32`. – not2qubit Nov 14 '22 at 23:10
  • I managed to get the build under control by copying the (global) `distutils.cfg` to `setup.cfg`, where I specify the compiler. Then the build is done using `build` package, and then `python -m build -n -w` && `pip install .whl`. – not2qubit Nov 15 '22 at 02:54
2

Below is a hack which can be used to build ext with pyproject.toml without depending on setup.py

pyproject.toml

[tool.setuptools]
py-modules = ["_custom_build"]

[tool.setuptools.cmdclass]
build_py = "_custom_build.build_py"

_custom_build.py

from setuptools import Extension
from setuptools.command.build_py import build_py as _build_py

class build_py(_build_py):
    def run(self):
        self.run_command("build_ext")
        return super().run()

    def initialize_options(self):
        super().initialize_options()
        if self.distribution.ext_modules == None:
            self.distribution.ext_modules = []

        self.distribution.ext_modules.append(
            Extension(
                "termial_random.random",
                sources=["termial_random/random.c"],
                extra_compile_args=["-std=c17", "-lm"],
            )
        )
Chandan
  • 11,465
  • 1
  • 6
  • 25
  • 1
    Isn't there an issue with the `_custom_build` module being actually pip-installed? Instead of this module being available only during the "build time", it is always available and importable even at "run time". -- Would it still work if it were removed from the list of `py-modules`? Maybe it should only be added to `MANIFEST.in` instead, so that `_custom_build` is available in the sdists but not in the wheels (so not installed). – sinoroc Oct 25 '22 at 16:20
  • What's the build command with this? – not2qubit Nov 12 '22 at 02:49
  • 2
    @sinoroc Yes, your suggestion should work. The whole point, however, is not to get rid of `setup.py`, but of any excess code execution. So this solution doesn't make much sense anyway, it just replaces `setup.py` with another `.py`. – EvgenKo423 Nov 12 '22 at 07:45
  • 1
    @not2qubit As with any `pyproject.toml`-style project, you should use the [`build` tool](https://github.com/pypa/build) or any other PEP 517 build frontend. – EvgenKo423 Nov 12 '22 at 07:52
  • @evgenko423 I agree. There does not seem to be much of a point in removing `setup.py` and putting the custom command in a different file instead. There does not seem to be any gain. But technically, it still seems correct, even though (again) I do not know in which case one would want to do that (instead of `setup.py`). – sinoroc Nov 12 '22 at 09:18
  • This method now works for me after having installed all the (7 GB+) Desktop build tools for MS Visual Studio, before it would not work. However, it seem that the compiler args are completely ignored, as i also tried this with MinGW compiler before being *forced* back to the MS/VS universe of madness. – not2qubit Nov 13 '22 at 12:47