2025, Dec 10 06:02
Как собрать f2py-расширение на scikit-build-core с учётом .pyf: рабочая конфигурация CMake
Разбор сбоя при миграции f2py с numpy.distutils на scikit-build-core: почему игнорируется .pyf и как исправить. Правильная сборка через numpy.f2py -c и CMake.
При переносе расширения на Fortran, использующего f2py, с устаревшего numpy.distutils на scikit-build-core может проявиться странный сбой: модуль собирается и импортируется, но Python-сигнатуры игнорируют интерфейсный файл .pyf. Это как раз тот случай, когда f2py вызывается так, что директивы из .pyf не применяются. Ниже — минимальный, воспроизводимый путь от некорректного поведения к рабочей конфигурации на scikit-build-core.
Симптом
Расширение собирается и кладёт geopack_tsyganenko.cpython-313-darwin.so в site-packages. Импорт проходит, но сигнатуры функций передают смысл некорректно. Вместо того чтобы возвращать выходные переменные как значения функции, они попадают в список входных аргументов — признак того, что интерфейс .pyf фактически не был учтён при сборке.
Неудачная конфигурация CMake
В следующей конфигурации CMake вызывается numpy.f2py для генерации исходников, после чего они компилируются через python_add_library. Модуль после этого импортируется, но семантика .pyf в экспортируемых сигнатурах не проявляется:
cmake_minimum_required(VERSION 3.17.2...3.29)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C Fortran)
find_package(
Python
COMPONENTS Interpreter Development.Module NumPy
REQUIRED)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_Fortran_FLAGS "-w")
execute_process(
COMMAND "${PYTHON_EXECUTABLE}" -c
"import numpy.f2py; print(numpy.f2py.get_include())"
OUTPUT_VARIABLE F2PY_INC_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE)
add_library(f2py_obj OBJECT "${F2PY_INC_DIR}/fortranobject.c")
target_link_libraries(f2py_obj PUBLIC Python::NumPy)
target_include_directories(f2py_obj PUBLIC "${F2PY_INC_DIR}")
set_property(TARGET f2py_obj PROPERTY POSITION_INDEPENDENT_CODE ON)
set(PYF_PATH ${CMAKE_CURRENT_SOURCE_DIR}/src/tsyganenko/geopack.pyf)
set(SRC_GEOPACK ${CMAKE_CURRENT_SOURCE_DIR}/src/tsyganenko/geopack.f)
set(SRC_T96 ${CMAKE_CURRENT_SOURCE_DIR}/src/tsyganenko/T96.f)
set(SRC_T02 ${CMAKE_CURRENT_SOURCE_DIR}/src/tsyganenko/T02.f)
set(PY_MOD_NAME geopack_tsyganenko)
set(GEN_C ${CMAKE_CURRENT_BINARY_DIR}/${PY_MOD_NAME}module.c)
set(GEN_WRAPS ${CMAKE_CURRENT_BINARY_DIR}/${PY_MOD_NAME}-f2pywrappers.f)
add_custom_command(
OUTPUT ${GEN_C} ${GEN_WRAPS}
DEPENDS
${PYF_PATH}
${SRC_GEOPACK}
${SRC_T96}
${SRC_T02}
VERBATIM
COMMAND "${PYTHON_EXECUTABLE}" -m numpy.f2py
"${PYF_PATH}"
"${SRC_GEOPACK}"
"${SRC_T96}"
"${SRC_T02}"
-m ${PY_MOD_NAME}
--lower
)
python_add_library(
geopack_tsyganenko MODULE "${GEN_C}"
"${GEN_WRAPS}"
"${PYF_PATH}"
"${SRC_GEOPACK}"
"${SRC_T96}"
"${SRC_T02}"
WITH_SOABI)
target_link_libraries(${PY_MOD_NAME} PRIVATE f2py_obj)
install(TARGETS ${PY_MOD_NAME} DESTINATION .)
Почему .pyf не применяется
Это поведение показывает, что на шаге, который создаёт расширение, интерфейс .pyf не был задействован. Генерация промежуточных исходников через numpy.f2py с последующей отдельной компиляцией приводит к модулю, чьи Python-сигнатуры расходятся с тем, что описано в .pyf. Зато прямой запуск f2py с ключом -c на тех же входах даёт ожидаемый результат — значит, дело именно в способе вызова f2py в сборке.
Рабочий подход
Вызов numpy.f2py с ключом -c прямо из CMake приводит сборку в соответствие с командой, которая даёт корректные сигнатуры. Цель в CMake оформляется как пользовательская команда, выпускающая итоговую разделяемую библиотеку; добавляется тонкая цель, чтобы включить её в сборку по умолчанию, и правило установки для укладки полученного .so:
cmake_minimum_required(VERSION 3.17.2...3.29)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C Fortran)
find_package(
Python
COMPONENTS Interpreter Development.Module NumPy
REQUIRED)
execute_process(
COMMAND "${PYTHON_EXECUTABLE}" -c
"import numpy.f2py; print(numpy.f2py.get_include())"
OUTPUT_VARIABLE F2PY_INC_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE)
add_library(f2py_obj OBJECT "${F2PY_INC_DIR}/fortranobject.c")
target_link_libraries(f2py_obj PUBLIC Python::NumPy)
target_include_directories(f2py_obj PUBLIC "${F2PY_INC_DIR}")
set_property(TARGET f2py_obj PROPERTY POSITION_INDEPENDENT_CODE ON)
set(FFLAGS "-w -O2 -fbacktrace -fno-automatic -fPIC")
set(PYF_FILEPATH ${CMAKE_CURRENT_SOURCE_DIR}/src/geopack_tsyganenko/Geopack.pyf)
set(SRC_GEOPACK ${CMAKE_CURRENT_SOURCE_DIR}/src/geopack_tsyganenko/Geopack.for)
set(SRC_T96_FOR ${CMAKE_CURRENT_SOURCE_DIR}/src/geopack_tsyganenko/T96.for)
set(SRC_T02_FOR ${CMAKE_CURRENT_SOURCE_DIR}/src/geopack_tsyganenko/T01_01c.for)
set(PY_MOD_NAME geopack_tsyganenko)
set(BUILT_SO ${CMAKE_CURRENT_BINARY_DIR}/${PY_MOD_NAME}${CMAKE_SHARED_MODULE_SUFFIX})
add_custom_command(
OUTPUT ${BUILT_SO}
DEPENDS
${PYF_FILEPATH}
${SRC_GEOPACK}
${SRC_T96_FOR}
${SRC_T02_FOR}
VERBATIM
COMMAND "${PYTHON_EXECUTABLE}" -m numpy.f2py -c --f77flags="${FFLAGS}"
"${PYF_FILEPATH}"
"${SRC_GEOPACK}"
"${SRC_T96_FOR}"
"${SRC_T02_FOR}"
-m ${PY_MOD_NAME}
--lower
)
add_custom_target(${PY_MOD_NAME}_build ALL
DEPENDS ${BUILT_SO}
)
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/ DESTINATION .
FILES_MATCHING PATTERN "${PY_MOD_NAME}*${CMAKE_SHARED_MODULE_SUFFIX}")
Помимо правки CMake, помогло обновить требования к системе сборки: добавление meson вместе с scikit-build-core и numpy в pyproject.toml убрало проблемы со сборкой:
[build-system]
requires = ["scikit-build-core", "meson", "numpy"]
build-backend = "scikit_build_core.build"
[project]
name = "tsyganenko"
version = "2020.2.0"
dependencies = [
"matplotlib",
"numpy",
"pandas"
]
requires-python = ">=3.12"
authors = [
{name = "John C Coxon", email = "work@johncoxon.co.uk"},
{name = "Sebastien de Larquier"}
]
description = "A Python wrapper for N A Tsyganenko’s field-line tracing routines."
readme = "README.md"
license = "MIT"
license-files = ["LICENCE.txt"]
keywords = [
"magnetic field",
"magnetosphere"
]
classifiers = ["Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering :: Physics"
]
[project.optional-dependencies]
notebook = ["jupyter"]
[project.urls]
Geopack = "https://geo.phys.spbu.ru/~tsyganenko/empirical-models/"
doi = "https://doi.org/10.5281/zenodo.3937276"
repository = "https://github.com/johncoxon/tsyganenko"
[tool.scikit-build]
ninja.version = ">=1.10"
cmake.version = ">=3.17.2"
[tool.setuptools.packages.find]
where = ["src"]
Почему это важно
Для расширений на f2py файл .pyf задаёт контракт, видимый из Python. Если на шаге создания модуля этот файл не применяется, на выходе получится .so, который импортируется, но предоставляет неверный интерфейс. Явный вызов numpy.f2py -c внутри CMake воспроизводит поведение проверенного ручного запуска и гарантирует, что директивы .pyf определяют результат.
Итоги
Перенося Fortran-расширение на scikit-build-core, убедитесь, что в цепочке компиляции используется numpy.f2py -c с вашим .pyf и исходниками, а в CMake именно этот шаг формирует финальную библиотеку. Если сборка подвисает или не завершается, добавьте meson в требования build-system — это может снять блокировку. При такой настройке экспортируемые Python-сигнатуры соответствуют спецификации .pyf, и модуль работает как задумано.