pythonnumpycmakef2py

scikit-build-core ignores .pyf files when building Python modules: why?


I maintain a Python module here and I'm having difficulty getting it to build correctly. I'm currently trying to switch from the old numpy distutils over to scikit-build-core on the change-setup branch.

The following steps apparently successfully install the module and put a file called geopack_tsyganenko.cpython-313-darwin.so in my site-packages:

git clone https://github.com/johncoxon/tsyganenko
cd tsyganenko
git checkout change-setup
pip install .

This results in the following in IPython:

In [1]: import geopack_tsyganenko

In [2]: geopack_tsyganenko.igrf_gsw_08?
Signature:   geopack_tsyganenko.igrf_gsw_08(*args, **kwargs)
Type:        fortran
String form: <fortran function igrf_gsw_08>
Docstring:  
igrf_gsw_08(xgsw,ygsw,zgsw,hxgsw,hygsw,hzgsw)

Wrapper for ``igrf_gsw_08``.

Parameters
----------
xgsw : input float
ygsw : input float
zgsw : input float
hxgsw : input float
hygsw : input float
hzgsw : input float

However, geopack.pyf specifies that the hxgsw, hygsw, and hzgsw variables should be outputs, and they are not. This indicates that the build process is not seeing the geopack.pyf file correctly.

On the other hand, if I then run:

cd src/tsyganenko
python -m numpy.f2py -c --f77flags="-w" geopack.pyf geopack.f T96.f T02.f -m geopack_tsyganenko --lower

It creates geopack_tsyganenko.cpython-313-darwin.so in src/tsyganenko and then if I copy that file manually into my site-packages the following happens in IPython:

In [1]: import geopack_tsyganenko

In [2]: geopack_tsyganenko.igrf_gsw_08?
Signature:   geopack_tsyganenko.igrf_gsw_08(*args, **kwargs)
Type:        fortran
String form: <fortran function igrf_gsw_08>
Docstring:  
hxgsw,hygsw,hzgsw = igrf_gsw_08(xgsw,ygsw,zgsw)

Wrapper for ``igrf_gsw_08``.

Parameters
----------
xgsw : input float
ygsw : input float
zgsw : input float

Returns
-------
hxgsw : float
hygsw : float
hzgsw : float

My question is, how do I need to change my CMakeLists.txt file to get the build process to take account of geopack.pyf and compile correctly? At the moment, CMakeLists.txt looks like this:

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")

# F2PY headers
execute_process(
  COMMAND "${PYTHON_EXECUTABLE}" -c
          "import numpy.f2py; print(numpy.f2py.get_include())"
  OUTPUT_VARIABLE F2PY_INCLUDE_DIR
  OUTPUT_STRIP_TRAILING_WHITESPACE)

add_library(fortranobject OBJECT "${F2PY_INCLUDE_DIR}/fortranobject.c")
target_link_libraries(fortranobject PUBLIC Python::NumPy)
target_include_directories(fortranobject PUBLIC "${F2PY_INCLUDE_DIR}")
set_property(TARGET fortranobject PROPERTY POSITION_INDEPENDENT_CODE ON)

# Define variables
set(FORTRAN_PYF_FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/tsyganenko/geopack.pyf)
set(FORTRAN_SRC_FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/tsyganenko/geopack.f)
set(FORTRAN_T96_FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/tsyganenko/T96.f)
set(FORTRAN_T02_FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/tsyganenko/T02.f)
set(MODULE_NAME geopack_tsyganenko)
set(F2PY_MODULE ${CMAKE_CURRENT_BINARY_DIR}/${MODULE_NAME}module.c)
set(F2PY_WRAPPERS ${CMAKE_CURRENT_BINARY_DIR}/${MODULE_NAME}-f2pywrappers.f)

add_custom_command(
  OUTPUT ${F2PY_MODULE} ${F2PY_WRAPPERS}
  DEPENDS
    ${FORTRAN_PYF_FILE}
    ${FORTRAN_SRC_FILE}
    ${FORTRAN_T96_FILE}
    ${FORTRAN_T02_FILE}
  VERBATIM
  COMMAND "${PYTHON_EXECUTABLE}" -m numpy.f2py
    "${FORTRAN_PYF_FILE}"
    "${FORTRAN_SRC_FILE}"
    "${FORTRAN_T96_FILE}"
    "${FORTRAN_T02_FILE}"
    -m ${MODULE_NAME}
    --lower
)

python_add_library(
  geopack_tsyganenko MODULE "${F2PY_MODULE}"
  "${F2PY_WRAPPERS}"
  "${FORTRAN_PYF_FILE}"
  "${FORTRAN_SRC_FILE}"
  "${FORTRAN_T96_FILE}"
  "${FORTRAN_T02_FILE}"
  WITH_SOABI)
target_link_libraries(${MODULE_NAME} PRIVATE fortranobject)

install(TARGETS ${MODULE_NAME} DESTINATION .)

Solution

  • I have finally got this to build correctly by modifying my CMakeLists.txt to look like this:

    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)
    
    # F2PY headers
    execute_process(
      COMMAND "${PYTHON_EXECUTABLE}" -c
              "import numpy.f2py; print(numpy.f2py.get_include())"
      OUTPUT_VARIABLE F2PY_INCLUDE_DIR
      OUTPUT_STRIP_TRAILING_WHITESPACE)
    
    add_library(fortranobject OBJECT "${F2PY_INCLUDE_DIR}/fortranobject.c")
    target_link_libraries(fortranobject PUBLIC Python::NumPy)
    target_include_directories(fortranobject PUBLIC "${F2PY_INCLUDE_DIR}")
    set_property(TARGET fortranobject PROPERTY POSITION_INDEPENDENT_CODE ON)
    
    # Define variables
    set(FORTRAN_FLAGS "-w -O2 -fbacktrace -fno-automatic -fPIC")
    set(PYF_FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/geopack_tsyganenko/Geopack.pyf)
    set(GEOPACK_FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/geopack_tsyganenko/Geopack.for)
    set(T96_FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/geopack_tsyganenko/T96.for)
    set(T02_FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/geopack_tsyganenko/T01_01c.for)
    set(MODULE_NAME geopack_tsyganenko)
    set(F2PY_SO_FILE ${CMAKE_CURRENT_BINARY_DIR}/${MODULE_NAME}${CMAKE_SHARED_MODULE_SUFFIX})
    
    add_custom_command(
      OUTPUT ${F2PY_SO_FILE}
      DEPENDS
        ${PYF_FILE}
        ${GEOPACK_FILE}
        ${T96_FILE}
        ${T02_FILE}
      VERBATIM
      COMMAND "${PYTHON_EXECUTABLE}" -m numpy.f2py -c --f77flags="${FORTRAN_FLAGS}"
        "${PYF_FILE}"
        "${GEOPACK_FILE}"
        "${T96_FILE}"
        "${T02_FILE}"
        -m ${MODULE_NAME}
        --lower
    )
    
    add_custom_target(${MODULE_NAME}_f2py ALL
      DEPENDS ${F2PY_SO_FILE}
    )
    
    install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/ DESTINATION .
            FILES_MATCHING PATTERN "${MODULE_NAME}*${CMAKE_SHARED_MODULE_SUFFIX}")
    

    I had trouble with this until I modified pyproject.toml to look like this, specifically including meson as a requirement in line 2:

    [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"]