在setup.py中扩展setuptools扩展以使用CMake? 我正在编写一个Python扩展,它链接了C++库,我使用CGuess来帮助构建过程。这意味着,现在,我知道如何捆绑它的唯一方法是,在运行setup.py bdist_wheel之前,我必须首先使用cmake编译它们。一定有更好的办法

在setup.py中扩展setuptools扩展以使用CMake? 我正在编写一个Python扩展,它链接了C++库,我使用CGuess来帮助构建过程。这意味着,现在,我知道如何捆绑它的唯一方法是,在运行setup.py bdist_wheel之前,我必须首先使用cmake编译它们。一定有更好的办法,python,c++,cmake,setuptools,packaging,Python,C++,Cmake,Setuptools,Packaging,我想知道在setup.py ext_模块构建过程中是否有可能(或者任何人都尝试过)调用CMake?我猜有一种方法可以创建某个东西的子类,但我不确定该去哪里寻找 我使用CGOLE,因为它使我能够更精确地控制C和C++库扩展,并有复杂的构建步骤,正如我所希望的那样。此外,我还可以通过findPythonLibs.cmake中的Python_ADD_MODULE()命令直接使用cmake轻松构建Python扩展。我只希望这是一个步骤。您基本上需要做的是覆盖setup.py中的build\u ext命令

我想知道在setup.py ext_模块构建过程中是否有可能(或者任何人都尝试过)调用CMake?我猜有一种方法可以创建某个东西的子类,但我不确定该去哪里寻找


我使用CGOLE,因为它使我能够更精确地控制C和C++库扩展,并有复杂的构建步骤,正如我所希望的那样。此外,我还可以通过findPythonLibs.cmake中的Python_ADD_MODULE()命令直接使用cmake轻松构建Python扩展。我只希望这是一个步骤。

您基本上需要做的是覆盖
setup.py
中的
build\u ext
命令类,并将其注册到命令类中。在
build\u ext
的自定义impl中,配置并调用
cmake
来配置然后构建扩展模块。不幸的是,官方文档对于如何实现自定义
distutils
命令相当简洁(请参阅);我发现直接研究命令代码更有帮助。例如,下面是的源代码

示例项目 我准备了一个简单的项目,由一个C扩展
foo
和一个python模块
spam.eggs
组成:

so-42585210/
├── 垃圾邮件
│   ├── __初始值为空
│   ├── 鸡蛋
│   ├── 富科
│   └── 福安
├── CMakeLists.txt
└── setup.py
用于测试设置的文件 这些只是我为测试安装脚本而编写的一些简单存根

spam/eggs.py
(仅用于测试库调用):

从ctypes导入cdll
导入路径库
def wrap_bar():
foo=cdll.LoadLibrary(str(pathlib.Path(\uuuuuu文件\uuuuuu)。带有\u名称('libfoo.dylib'))
返回foo.bar()
spam/foo.c

#包括“foo.h”
整型条(){
返回42;
}
垃圾邮件/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})
设置脚本 这就是魔法发生的地方。当然,还有很大的改进空间-如果需要,您可以将其他选项传递给
CMakeExtension
类(有关扩展的更多信息,请参阅),通过覆盖方法
initialize_options
finalize_options
等,通过
setup.cfg
使CMake选项可配置

导入操作系统
导入路径库
从设置工具导入设置,扩展
从setuptools.command.build\u ext导入build\u ext作为build\u ext\u orig
类CMakeExtension(扩展名):
定义初始化(self,name):
#不要为这个特殊的扩展调用原始的build_ext
超级()
类生成外部(生成外部源):
def运行(自):
对于self.extensions中的ext:
自我构建(扩展)
super().run()
def build_cmake(自身、外部):
cwd=pathlib.Path().absolute()
#这些dir将在build_py中创建,因此如果您没有
#任何要捆绑的python源代码,都将丢失dir
build\u temp=pathlib.Path(self.build\u temp)
生成临时mkdir(父项=真,存在\u确定=真)
extdir=pathlib.Path(self.get\u ext\u fullpath(ext.name))
extdir.mkdir(parents=True,exist\u ok=True)
#cmake args的示例
配置='Debug'如果self.Debug或'Release'
cmake_args=[
'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY='+str(extdir.parent.absolute()),
'-DCMAKE_BUILD_TYPE='+config
]
#构建参数的示例
生成参数=[
'--config',config,
'--','-j4'
]
os.chdir(str(build_temp))
自我繁殖(['cmake',str(cwd)]+cmake_args)
如果不是自干运行:
self.spawn(['cmake','-build','.]+build_参数)
#故障排除:如果上述第行出现故障,则删除所有可能的
#临时CMake文件,包括顶级目录中的“CMakeCache.txt”。
os.chdir(str(cwd))
设置(
name='spam',
version='0.1',
软件包=['spam'],
ext_模块=[CMakeExtension('spam/foo')],
cmdclass={
“构建外部”:构建外部,
}
)
测试 构建项目的控制盘,然后安装它。测试库是否已安装:

$ 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

我想补充我自己的答案,作为霍夫林描述的补充

谢谢你,hoefling,因为你的回答帮助我以与我自己的存储库基本相同的方式编写安装脚本

序言 写这个答案的主要动机是试图“粘合”缺失的部分。OP不说明正在开发的C/C++ Python模块的性质;我想先说明一下,下面的步骤是针对C/C++构建链,创建多个<代码> .dll >代码> >代码> .S/<代码>文件以及预编译<>代码> *.Pyd>代码> /<代码> SO 文件,除了需要在脚本目录中放置的一些通用的<代码> .Py <代码>文件外,

所有这些文件都是在运行cmake build命令后直接产生的。。。有趣。没有建议以这种方式构建setup.py

由于setup.py意味着您的脚本将成为软件包/库的一部分,并且需要生成的
.dll
文件必须通过库部分声明,并列出源代码和包含目录,因此无法直观地告诉setuptools这些库,在
build\u ext
中发生的对
cmake-b
的一次调用的结果脚本和数据文件都应该放在各自的位置。如果您想让setuptools和
$ python -c "from spam import eggs; print(eggs.wrap_bar())"
42
from distutils.command.install_data import install_data
from setuptools import find_packages, setup, Extension
from setuptools.command.build_ext import build_ext
from setuptools.command.install_lib import install_lib
from setuptools.command.install_scripts import install_scripts
import struct

BITS = struct.calcsize("P") * 8
PACKAGE_NAME = "example"

class CMakeExtension(Extension):
    """
    An extension to run the cmake build

    This simply overrides the base extension class so that setuptools
    doesn't try to build your sources for you
    """

    def __init__(self, name, sources=[]):

        super().__init__(name = name, sources = sources)

class InstallCMakeLibsData(install_data):
    """
    Just a wrapper to get the install data into the egg-info

    Listing the installed files in the egg-info guarantees that
    all of the package files will be uninstalled when the user
    uninstalls your package through pip
    """

    def run(self):
        """
        Outfiles are the libraries that were built using cmake
        """

        # There seems to be no other way to do this; I tried listing the
        # libraries during the execution of the InstallCMakeLibs.run() but
        # setuptools never tracked them, seems like setuptools wants to
        # track the libraries through package data more than anything...
        # help would be appriciated

        self.outfiles = self.distribution.data_files

class InstallCMakeLibs(install_lib):
    """
    Get the libraries from the parent distribution, use those as the outfiles

    Skip building anything; everything is already built, forward libraries to
    the installation step
    """

    def run(self):
        """
        Copy libraries from the bin directory and place them as appropriate
        """

        self.announce("Moving library files", level=3)

        # We have already built the libraries in the previous build_ext step

        self.skip_build = True

        bin_dir = self.distribution.bin_dir

        # Depending on the files that are generated from your cmake
        # build chain, you may need to change the below code, such that
        # your files are moved to the appropriate location when the installation
        # is run

        libs = [os.path.join(bin_dir, _lib) for _lib in 
                os.listdir(bin_dir) if 
                os.path.isfile(os.path.join(bin_dir, _lib)) and 
                os.path.splitext(_lib)[1] in [".dll", ".so"]
                and not (_lib.startswith("python") or _lib.startswith(PACKAGE_NAME))]

        for lib in libs:

            shutil.move(lib, os.path.join(self.build_dir,
                                          os.path.basename(lib)))

        # Mark the libs for installation, adding them to 
        # distribution.data_files seems to ensure that setuptools' record 
        # writer appends them to installed-files.txt in the package's egg-info
        #
        # Also tried adding the libraries to the distribution.libraries list, 
        # but that never seemed to add them to the installed-files.txt in the 
        # egg-info, and the online recommendation seems to be adding libraries 
        # into eager_resources in the call to setup(), which I think puts them 
        # in data_files anyways. 
        # 
        # What is the best way?

        # These are the additional installation files that should be
        # included in the package, but are resultant of the cmake build
        # step; depending on the files that are generated from your cmake
        # build chain, you may need to modify the below code

        self.distribution.data_files = [os.path.join(self.install_dir, 
                                                     os.path.basename(lib))
                                        for lib in libs]

        # Must be forced to run after adding the libs to data_files

        self.distribution.run_command("install_data")

        super().run()

class InstallCMakeScripts(install_scripts):
    """
    Install the scripts in the build dir
    """

    def run(self):
        """
        Copy the required directory to the build directory and super().run()
        """

        self.announce("Moving scripts files", level=3)

        # Scripts were already built in a previous step

        self.skip_build = True

        bin_dir = self.distribution.bin_dir

        scripts_dirs = [os.path.join(bin_dir, _dir) for _dir in
                        os.listdir(bin_dir) if
                        os.path.isdir(os.path.join(bin_dir, _dir))]

        for scripts_dir in scripts_dirs:

            shutil.move(scripts_dir,
                        os.path.join(self.build_dir,
                                     os.path.basename(scripts_dir)))

        # Mark the scripts for installation, adding them to 
        # distribution.scripts seems to ensure that the setuptools' record 
        # writer appends them to installed-files.txt in the package's egg-info

        self.distribution.scripts = scripts_dirs

        super().run()

class BuildCMakeExt(build_ext):
    """
    Builds using cmake instead of the python setuptools implicit build
    """

    def run(self):
        """
        Perform build_cmake before doing the 'normal' stuff
        """

        for extension in self.extensions:

            if extension.name == 'example_extension':

                self.build_cmake(extension)

        super().run()

    def build_cmake(self, extension: Extension):
        """
        The steps required to build the extension
        """

        self.announce("Preparing the build environment", level=3)

        build_dir = pathlib.Path(self.build_temp)

        extension_path = pathlib.Path(self.get_ext_fullpath(extension.name))

        os.makedirs(build_dir, exist_ok=True)
        os.makedirs(extension_path.parent.absolute(), exist_ok=True)

        # Now that the necessary directories are created, build

        self.announce("Configuring cmake project", level=3)

        # Change your cmake arguments below as necessary
        # Below is just an example set of arguments for building Blender as a Python module

        self.spawn(['cmake', '-H'+SOURCE_DIR, '-B'+self.build_temp,
                    '-DWITH_PLAYER=OFF', '-DWITH_PYTHON_INSTALL=OFF',
                    '-DWITH_PYTHON_MODULE=ON',
                    f"-DCMAKE_GENERATOR_PLATFORM=x"
                    f"{'86' if BITS == 32 else '64'}"])

        self.announce("Building binaries", level=3)

        self.spawn(["cmake", "--build", self.build_temp, "--target", "INSTALL",
                    "--config", "Release"])

        # Build finished, now copy the files into the copy directory
        # The copy directory is the parent directory of the extension (.pyd)

        self.announce("Moving built python module", level=3)

        bin_dir = os.path.join(build_dir, 'bin', 'Release')
        self.distribution.bin_dir = bin_dir

        pyd_path = [os.path.join(bin_dir, _pyd) for _pyd in
                    os.listdir(bin_dir) if
                    os.path.isfile(os.path.join(bin_dir, _pyd)) and
                    os.path.splitext(_pyd)[0].startswith(PACKAGE_NAME) and
                    os.path.splitext(_pyd)[1] in [".pyd", ".so"]][0]

        shutil.move(pyd_path, extension_path)

        # After build_ext is run, the following commands will run:
        # 
        # install_lib
        # install_scripts
        # 
        # These commands are subclassed above to avoid pitfalls that
        # setuptools tries to impose when installing these, as it usually
        # wants to build those libs and scripts as well or move them to a
        # different place. See comments above for additional information

setup(name='my_package',
      version='1.0.0a0',
      packages=find_packages(),
      ext_modules=[CMakeExtension(name="example_extension")],
      description='An example cmake extension module',
      long_description=open("./README.md", 'r').read(),
      long_description_content_type="text/markdown",
      keywords="test, cmake, extension",
      classifiers=["Intended Audience :: Developers",
                   "License :: OSI Approved :: "
                   "GNU Lesser General Public License v3 (LGPLv3)",
                   "Natural Language :: English",
                   "Programming Language :: C",
                   "Programming Language :: C++",
                   "Programming Language :: Python",
                   "Programming Language :: Python :: 3.6",
                   "Programming Language :: Python :: Implementation :: CPython"],
      license='GPL-3.0',
      cmdclass={
          'build_ext': BuildCMakeExt,
          'install_data': InstallCMakeLibsData,
          'install_lib': InstallCMakeLibs,
          'install_scripts': InstallCMakeScripts
          }
    )