Skip to main content
`Ctrl`+`K`

# Using via `scikit-build`

`scikit-build` provides two separate concepts geared towards the users of Python extension modules.

1. A `setuptools` replacement (legacy behaviour)

2. A series of `cmake` modules with definitions which help building Python extensions

Note

It is possible to use `scikit-build`’s `cmake` modules to bypass the cmake setup mechanism completely, and to write targets which call `f2py -c`. This usage is not recommended since the point of these build system documents are to move away from the internal `numpy.distutils` methods.

For situations where no `setuptools` replacements are required or wanted (i.e. if `wheels` are not needed), it is recommended to instead use the vanilla `cmake` setup described in Using via cmake.

## Fibonacci walkthrough (F77)

We will consider the `fib` example from Three ways to wrap - getting started section.

    C FILE: FIB1.F
          SUBROUTINE FIB(A,N)
    C
    C     CALCULATE FIRST N FIBONACCI NUMBERS
    C
          INTEGER N
          REAL*8 A(N)
          DO I=1,N
             IF (I.EQ.1) THEN
                A(I) = 0.0D0
             ELSEIF (I.EQ.2) THEN
                A(I) = 1.0D0
             ELSE
                A(I) = A(I-1) + A(I-2)
             ENDIF
          ENDDO
          END
    C END FILE FIB1.F

### `CMake` modules only

Consider using the following `CMakeLists.txt`.

    ### setup project ###
    cmake_minimum_required(VERSION 3.9)

    project(fibby
      VERSION 1.0
      DESCRIPTION "FIB module"
      LANGUAGES C Fortran
      )

    # Safety net
    if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR)
      message(
        FATAL_ERROR
          "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there.\n"
      )
    endif()

    # Ensure scikit-build modules
    if (NOT SKBUILD)
      find_package(PythonInterp 3.8 REQUIRED)
      # Kanged --> https://github.com/Kitware/torch_liberator/blob/master/CMakeLists.txt
      # If skbuild is not the driver; include its utilities in CMAKE_MODULE_PATH
      execute_process(
        COMMAND "${PYTHON_EXECUTABLE}"
        -c "import os, skbuild; print(os.path.dirname(skbuild.__file__))"
        OUTPUT_VARIABLE SKBLD_DIR
        OUTPUT_STRIP_TRAILING_WHITESPACE
      )
      list(APPEND CMAKE_MODULE_PATH "${SKBLD_DIR}/resources/cmake")
      message(STATUS "Looking in ${SKBLD_DIR}/resources/cmake for CMake modules")
    endif()

    # scikit-build style includes
    find_package(PythonExtensions REQUIRED) # for ${PYTHON_EXTENSION_MODULE_SUFFIX}

    # Grab the variables from a local Python installation
    # NumPy headers
    execute_process(
      COMMAND "${PYTHON_EXECUTABLE}"
      -c "import numpy; print(numpy.get_include())"
      OUTPUT_VARIABLE NumPy_INCLUDE_DIRS
      OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    # 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
    )

    # Prepping the module
    set(f2py_module_name "fibby")
    set(fortran_src_file "${CMAKE_SOURCE_DIR}/fib1.f")
    set(f2py_module_c "${f2py_module_name}module.c")

    # Target for enforcing dependencies
    add_custom_target(genpyf
      DEPENDS "${fortran_src_file}"
    )
    add_custom_command(
      OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_c}"
      COMMAND ${PYTHON_EXECUTABLE}  -m "numpy.f2py"
                       "${fortran_src_file}"
                       -m "fibby"
                       --lower # Important
      DEPENDS fib1.f # Fortran source
    )

    add_library(${CMAKE_PROJECT_NAME} MODULE
                "${f2py_module_name}module.c"
                "${F2PY_INCLUDE_DIR}/fortranobject.c"
                "${fortran_src_file}")

    target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC
                               ${F2PY_INCLUDE_DIR}
                               ${NumPy_INCLUDE_DIRS}
                               ${PYTHON_INCLUDE_DIRS})
    set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES SUFFIX "${PYTHON_EXTENSION_MODULE_SUFFIX}")
    set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES PREFIX "")

    # Linker fixes
    if (UNIX)
      if (APPLE)
        set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
        LINK_FLAGS  '-Wl,-dylib,-undefined,dynamic_lookup')
      else()
        set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
      LINK_FLAGS  '-Wl,--allow-shlib-undefined')
      endif()
    endif()

    add_dependencies(${CMAKE_PROJECT_NAME} genpyf)

    install(TARGETS ${CMAKE_PROJECT_NAME} DESTINATION fibby)

Much of the logic is the same as in Using via cmake, however notably here the appropriate module suffix is generated via `sysconfig.get_config_var("SO")`. The resulting extension can be built and loaded in the standard workflow.

    ls .
    # CMakeLists.txt fib1.f
    cmake -S . -B build
    cmake --build build
    cd build
    python -c "import numpy as np; import fibby; a = np.zeros(9); fibby.fib(a); print (a)"
    # [ 0.  1.  1.  2.  3.  5.  8. 13. 21.]

### `setuptools` replacement

Note

As of November 2021

The behavior described here of driving the `cmake` build of a module is considered to be legacy behaviour and should not be depended on.

The utility of `scikit-build` lies in being able to drive the generation of more than extension modules, in particular a common usage pattern is the generation of Python distributables (for example for PyPI).

The workflow with `scikit-build` straightforwardly supports such packaging requirements. Consider augmenting the project with a `setup.py` as defined:

    from skbuild import setup

    setup(
        name="fibby",
        version="0.0.1",
        description="a minimal example package (fortran version)",
        license="MIT",
        packages=['fibby'],
        python_requires=">=3.7",
    )

Along with a commensurate `pyproject.toml`

    [build-system]
    requires = ["setuptools>=42", "wheel", "scikit-build", "cmake>=3.9", "numpy>=1.21"]
    build-backend = "setuptools.build_meta"

Together these can build the extension using `cmake` in tandem with other standard `setuptools` outputs. Running `cmake` through `setup.py` is mostly used when it is necessary to integrate with extension modules not built with `cmake`.

    ls .
    # CMakeLists.txt fib1.f pyproject.toml setup.py
    python setup.py build_ext --inplace
    python -c "import numpy as np; import fibby.fibby; a = np.zeros(9); fibby.fibby.fib(a); print (a)"
    # [ 0.  1.  1.  2.  3.  5.  8. 13. 21.]

Where we have modified the path to the module as `--inplace` places the extension module in a subfolder.