Как создать модуль расширения Python с помощью CMake?

Я пытаюсь создать модуль расширения Python с помощью CMake и f2py. Модуль строится нормально, но 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 основан на записи в блоге Мартино Пилия

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, где найти модуль расширения.


person jhaiduce    schedule 19.01.2021    source источник


Ответы (1)


Каталог, в котором setuptools ищет скомпилированный модуль, можно получить с помощью build_ext.get_ext_fullpath(ext.name). В приведенном выше коде результирующий путь передается в CMake путем установки переменной CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE.

Поскольку 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}")
person jhaiduce    schedule 21.01.2021