Using via cmake
¶
In terms of complexity, cmake
falls between make
and meson
. The
learning curve is steeper since CMake syntax is not pythonic and is closer to
make
with environment variables.
However, the trade-off is enhanced flexibility and support for most architectures and compilers. An introduction to the syntax is out of scope for this document, but this extensive CMake collection of resources is great.
Note
cmake
is very popular for mixed-language systems, however support for
f2py
is not particularly native or pleasant; and a more natural approach
is to consider Using via scikit-build
Fibonacci Walkthrough (F77)¶
Returning to 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
We do not need to explicitly generate the python -m numpy.f2py fib1.f
output, which is fib1module.c
, which is beneficial. With this; we can now
initialize a CMakeLists.txt
file as follows:
### setup project ###
cmake_minimum_required(VERSION 3.17.3) # 3.17 > for Python3_SOABI
set(CMAKE_CXX_STANDARD_REQUIRED ON)
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()
# Grab Python
find_package(Python3 3.9 REQUIRED
COMPONENTS Interpreter Development NumPy)
# Grab the variables from a local Python installation
# F2PY headers
execute_process(
COMMAND "${Python3_EXECUTABLE}"
-c "import numpy.f2py; print(numpy.f2py.get_include())"
OUTPUT_VARIABLE F2PY_INCLUDE_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
# Project scope; consider using target_include_directories instead
include_directories(
BEFORE
${Python3_INCLUDE_DIRS}
${Python3_NumPy_INCLUDE_DIRS}
${F2PY_INCLUDE_DIR}
)
message(STATUS ${Python3_INCLUDE_DIRS})
message(STATUS ${F2PY_INCLUDE_DIR})
message(STATUS ${Python3_NumPy_INCLUDE_DIRS})
# Vars
set(f2py_module_name "fibby")
set(fortran_src_file "${CMAKE_SOURCE_DIR}/fib1.f")
set(f2py_module_c "${f2py_module_name}module.c")
set(generated_module_file "${f2py_module_name}${Python3_SOABI}")
# Generate sources
add_custom_target(
genpyf
DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_c}"
)
add_custom_command(
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_c}"
COMMAND ${Python3_EXECUTABLE} -m "numpy.f2py"
"${fortran_src_file}"
-m "fibby"
--lower # Important
DEPENDS fib1.f # Fortran source
)
# Set up target
add_library(${CMAKE_PROJECT_NAME} SHARED
"${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_c}" # Generated
"${F2PY_INCLUDE_DIR}/fortranobject.c" # From NumPy
"${fortran_src_file}" # Fortran source(s)
)
# Depend on sources
add_dependencies(${CMAKE_PROJECT_NAME} genpyf)
set_target_properties(
${CMAKE_PROJECT_NAME}
PROPERTIES
PREFIX ""
OUTPUT_NAME "${CMAKE_PROJECT_NAME}"
LINKER_LANGUAGE C
)
A key element of the CMakeLists.txt
file defined above is that the
add_custom_command
is used to generate the wrapper C
files and then
added as a dependency of the actual shared library target via a
add_custom_target
directive which prevents the command from running every
time. Additionally, the method used for obtaining the fortranobject.c
file
can also be used to grab the numpy
headers on older cmake
versions.
This then works in the same manner as the other modules, although the naming
conventions are different and the output library is not automatically prefixed
with the cython
information.
ls .
# CMakeLists.txt fib1.f
mkdir build && cd build
cmake ..
make
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.]
This is particularly useful where an existing toolchain already exists and
scikit-build
or other additional python
dependencies are discouraged.