如何使用 CMake 构建 Python 扩展模块?

How do I build a Python extension module with CMake?

我正在尝试使用 CMake 和 f2py 构建一个 Python 扩展模块。该模块构建得很好,但 setuptools 找不到它。

我的构建目录如下所示:

cmake/modules/FindF2PY.cmake
cmake/modules/FindPythonExtensions.cmake
cmake/modules/UseF2PY.cmake
cmake/modules/FindNumPy.cmake
cmake/modules/targetLinkLibrariesWithDynamicLookup.cmake
setup.py
CMakeLists.txt
f2py_test/__init__.py
f2py_test.f90

f2py_test/init.py 只是一个空文件。 cmake/modules 中的文件是从 scikit-build 复制的。

setup.py 基于来自 Martino Pilia

的博客 post
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
import os
import sys

class CMakeExtension(Extension):
    def __init__(self, name, cmake_lists_dir='.', **kwa):
        Extension.__init__(self, name, sources=[], **kwa)
        self.cmake_lists_dir = os.path.abspath(cmake_lists_dir)

class cmake_build_ext(build_ext):
    def build_extensions(self):

        import subprocess

        # Ensure that CMake is present and working
        try:
            out = subprocess.check_output(['cmake', '--version'])
        except OSError:
            raise RuntimeError('Cannot find CMake executable')

        for ext in self.extensions:

            extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
            cfg = 'Debug' if os.environ.get('DISPTOOLS_DEBUG','OFF') == 'ON' else 'Release'

            cmake_args = [
                '-DCMAKE_BUILD_TYPE=%s' % cfg,
                # Ask CMake to place the resulting library in the directory
                # containing the extension
                '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), extdir),
                # Other intermediate static libraries are placed in a
                # temporary build directory instead
                '-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), self.build_temp),
                # Hint CMake to use the same Python executable that
                # is launching the build, prevents possible mismatching if
                # multiple versions of Python are installed
                '-DPYTHON_EXECUTABLE={}'.format(sys.executable),

            ]

            if not os.path.exists(self.build_temp):
                os.makedirs(self.build_temp)

            # Config
            subprocess.check_call(['cmake', ext.cmake_lists_dir] + cmake_args,
                                  cwd=self.build_temp)

            # Build
            subprocess.check_call(['cmake', '--build', '.', '--config', cfg],
                                  cwd=self.build_temp)

setup(
    name="f2py_test",
    version='0.0.1',
    packages=['f2py_test'],
    ext_modules=[CMakeExtension(name='f2py_test_')],
    cmdclass={'build_ext':cmake_build_ext},
)

CMakeLists.txt:

cmake_minimum_required(VERSION 3.10.2)

project(f2py_test)

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake/modules/")

enable_language(Fortran)

find_package(F2PY)
find_package(PythonExtensions)

set(f2py_test_sources f2py_test.f90)

add_library(f2py_test ${f2py_test_sources})

function(add_f2py_target)
  set(options)
  set(singleValueArgs)
  set(multiValueArgs SOURCES DEPENDS)
  cmake_parse_arguments(
    PARSE_ARGV 1
    F2PY_TARGET "${options}" "${singleValueArgs}"
    "${multiValueArgs}"
    )

  set(F2PY_TARGET_MODULE_NAME ${ARGV0})

  set(generated_module_file ${CMAKE_CURRENT_BINARY_DIR}/${F2PY_TARGET_MODULE_NAME}${PYTHON_EXTENSION_MODULE_SUFFIX})

  message(${generated_module_file})
  
  set(f2py_module_sources_fullpath "")
  foreach(f ${F2PY_TARGET_SOURCES})
    list(APPEND f2py_module_sources_fullpath "${CMAKE_CURRENT_SOURCE_DIR}/${f}")
  endforeach()

  add_custom_target(${F2PY_TARGET_MODULE_NAME} ALL
    DEPENDS ${generated_module_file} ${generated_module_file}
    )

  if(F2PY_TARGET_DEPENDS)
    add_dependencies(${F2PY_TARGET_MODULE_NAME} ${F2PY_TARGET_DEPENDS})
  endif()

  if(APPLE)
    set(F2PY_ENV LDFLAGS='-undefined dynamic_lookup -bundle')
  else()
    set(F2PY_ENV LDFLAGS='$ENV{LDFLAGS} -shared')
  endif()

  add_custom_command(
    OUTPUT ${generated_module_file}
    DEPENDS ${F2PY_TARGET_SOURCES}
    COMMAND env ${F2PY_ENV} ${F2PY_EXECUTABLE} --quiet
    -m ${F2PY_TARGET_MODULE_NAME}
    -c ${f2py_module_sources_fullpath}
    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})

  set_target_properties(
    ${F2PY_TARGET}
    PROPERTIES
    PREFIX ""
    OUTPUT_NAME ${F2PY_TARGET_MODULE_NAME})

endfunction(add_f2py_target)

if(F2PY_FOUND)
  add_f2py_target(f2py_test_ SOURCES ${f2py_test_sources} DEPENDS f2py_test)
endif()

f2py_test.f90:

module mod_f2py_test
  implicit none
contains
  subroutine f2py_test(a,b,c)
    real(kind=8), intent(in)::a,b
    real(kind=8), intent(out)::c
  end subroutine f2py_test
end module mod_f2py_test

python setup.py develop 调用 cmake 构建扩展模块,我可以在 ./build/temp.macosx-10.14-x86_64-3.8/f2py_test_.cpython-38-darwin.so 中看到。但是,setuptools 无法找到该文件并打印消息 error: can't copy 'build/lib.macosx-10.14-x86_64-3.8/f2py_test_.cpython-38-darwin.so': doesn't exist or not a regular file.

我该怎么做 1) 告诉 CMake 在 setuptools 期望的地方安装扩展模块或 2) 告诉 setuptools 在哪里可以找到扩展模块。

setuptools查找编译模块的目录可以通过build_ext.get_ext_fullpath(ext.name)获得。在上面的代码中,结果路径通过设置变量 CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE.

传递给 CMake

由于f2py是通过自定义命令调用的,扩展模块不会自动复制到输出目录。这可以通过再次调用 add_custom_command:

来实现
  add_custom_command(TARGET "${F2PY_TARGET_MODULE_NAME}" POST_BUILD
    COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${CMAKE_CURRENT_BINARY_DIR}/${generated_module_file}" "${CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE}/${generated_module_file}")