如何使用 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}")
我正在尝试使用 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
的博客 postfrom 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}")