Extending setuptools extension to use CMake in setup.py?

后端 未结 2 1682
一整个雨季
一整个雨季 2020-11-27 12:47

I\'m writing a Python extension that links a C++ library and I\'m using cmake to help with the build process. This means that right now, the only way I know how to bundle it

2条回答
  •  -上瘾入骨i
    2020-11-27 13:22

    What you basically need to do is to override the build_ext command class in your setup.py and register it in the command classes. In your custom impl of build_ext, configure and call cmake to configure and then build the extension modules. Unfortunately, the official docs are rather laconic about how to implement custom distutils commands (see Extending Distutils); I find it much more helpful to study the commands code directly. For example, here is the source code for the build_ext command.

    Example project

    I have prepared a simple project consisting out of a single C extension foo and a python module spam.eggs:

    so-42585210/
    ├── spam
    │   ├── __init__.py  # empty
    │   ├── eggs.py
    │   ├── foo.c
    │   └── foo.h
    ├── CMakeLists.txt
    └── setup.py
    

    Files for testing the setup

    These are just some simple stubs I wrote to test the setup script.

    spam/eggs.py (only for testing the library calls):

    from ctypes import cdll
    import pathlib
    
    
    def wrap_bar():
        foo = cdll.LoadLibrary(str(pathlib.Path(__file__).with_name('libfoo.dylib')))
        return foo.bar()
    

    spam/foo.c:

    #include "foo.h"
    
    int bar() {
        return 42;
    }
    

    spam/foo.h:

    #ifndef __FOO_H__
    #define __FOO_H__
    
    int bar();
    
    #endif
    

    CMakeLists.txt:

    cmake_minimum_required(VERSION 3.10.1)
    project(spam)
    set(src "spam")
    set(foo_src "spam/foo.c")
    add_library(foo SHARED ${foo_src})
    

    Setup script

    This is where the magic happens. Of course, there is a lot of room for improvements - you could pass additional options to CMakeExtension class if you need to (for more info on the extensions, see Building C and C++ Extensions), make the CMake options configurable via setup.cfg by overriding methods initialize_options and finalize_options etc.

    import os
    import pathlib
    
    from setuptools import setup, Extension
    from setuptools.command.build_ext import build_ext as build_ext_orig
    
    
    class CMakeExtension(Extension):
    
        def __init__(self, name):
            # don't invoke the original build_ext for this special extension
            super().__init__(name, sources=[])
    
    
    class build_ext(build_ext_orig):
    
        def run(self):
            for ext in self.extensions:
                self.build_cmake(ext)
            super().run()
    
        def build_cmake(self, ext):
            cwd = pathlib.Path().absolute()
    
            # these dirs will be created in build_py, so if you don't have
            # any python sources to bundle, the dirs will be missing
            build_temp = pathlib.Path(self.build_temp)
            build_temp.mkdir(parents=True, exist_ok=True)
            extdir = pathlib.Path(self.get_ext_fullpath(ext.name))
            extdir.mkdir(parents=True, exist_ok=True)
    
            # example of cmake args
            config = 'Debug' if self.debug else 'Release'
            cmake_args = [
                '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + str(extdir.parent.absolute()),
                '-DCMAKE_BUILD_TYPE=' + config
            ]
    
            # example of build args
            build_args = [
                '--config', config,
                '--', '-j4'
            ]
    
            os.chdir(str(build_temp))
            self.spawn(['cmake', str(cwd)] + cmake_args)
            if not self.dry_run:
                self.spawn(['cmake', '--build', '.'] + build_args)
            # Troubleshooting: if fail on line above then delete all possible 
            # temporary CMake files including "CMakeCache.txt" in top level dir.
            os.chdir(str(cwd))
    
    
    setup(
        name='spam',
        version='0.1',
        packages=['spam'],
        ext_modules=[CMakeExtension('spam/foo')],
        cmdclass={
            'build_ext': build_ext,
        }
    )
    

    Testing

    Build the project's wheel, install it. Test the library is installed:

    $ pip show -f spam
    Name: spam
    Version: 0.1
    Summary: UNKNOWN
    Home-page: UNKNOWN
    Author: UNKNOWN
    Author-email: UNKNOWN
    License: UNKNOWN
    Location: /Users/hoefling/.virtualenvs/stackoverflow/lib/python3.6/site-packages
    Requires: 
    Files:
      spam-0.1.dist-info/DESCRIPTION.rst
      spam-0.1.dist-info/INSTALLER
      spam-0.1.dist-info/METADATA
      spam-0.1.dist-info/RECORD
      spam-0.1.dist-info/WHEEL
      spam-0.1.dist-info/metadata.json
      spam-0.1.dist-info/top_level.txt
      spam/__init__.py
      spam/__pycache__/__init__.cpython-36.pyc
      spam/__pycache__/eggs.cpython-36.pyc
      spam/eggs.py
      spam/libfoo.dylib
    

    Run the wrapper function from spam.eggs module:

    $ python -c "from spam import eggs; print(eggs.wrap_bar())"
    42
    

提交回复
热议问题