Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
4.0k views
in Technique[技术] by (71.8m points)

How do I build a Python extension module with CMake?

I'm trying to build a Python extension module with CMake and f2py. The module builds just fine, but setuptools can't find it.

My build directory looks like this:

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 is just an empty file. The files within cmake/modules are copied from scikit-build.

setup.py is based on a blog post from Martino Pilia

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 invokes cmake to build the extension module, which I can see in ./build/temp.macosx-10.14-x86_64-3.8/f2py_test_.cpython-38-darwin.so. However, setuptools can't find the file and prints the message 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.

How do I either 1) Tell CMake to install the extension module where setuptools expects it or 2) Tell setuptools where to find the extension module.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

The directory where setuptools looks for the compiled module can be obtained by build_ext.get_ext_fullpath(ext.name). In the above code the resulting path is passed to CMake by setting the variable CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE.

Since f2py is invoked through a custom command, the extension module is not automatically copied to the output directory. This can be achieved by another call to 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}")

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...