使用 cmake#

就複雜度而言,cmake 介於 makemeson 之間。學習曲線較陡峭,因為 CMake 語法不是 pythonic 風格,且更接近於帶有環境變數的 make

然而,其權衡之處在於增強的彈性以及對大多數架構和編譯器的支援。語法介紹超出本文檔範圍,但這個廣泛的 CMake 資源集合非常棒。

注意

cmake 在混合語言系統中非常受歡迎,但是對 f2py 的支援並非特別原生或令人愉快;更自然的方法是考慮使用 scikit-build

費波納契 (Fibonacci) 演練 (F77)#

回到三種包裝方式 - 入門章節中的 fib 範例。

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

我們不需要顯式地產生 python -m numpy.f2py fib1.f 的輸出,即 fib1module.c,這是有益的。有了這個;我們現在可以初始化一個 CMakeLists.txt 檔案,如下所示

cmake_minimum_required(VERSION 3.18) # Needed to avoid requiring embedded Python libs too

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, 3.8 or newer
find_package(Python 3.8 REQUIRED
  COMPONENTS Interpreter Development.Module NumPy)

# Grab the variables from a local Python installation
# 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
)

# Print out the discovered paths
include(CMakePrintHelpers)
cmake_print_variables(Python_INCLUDE_DIRS)
cmake_print_variables(F2PY_INCLUDE_DIR)
cmake_print_variables(Python_NumPy_INCLUDE_DIRS)

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

# 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 ${Python_EXECUTABLE}  -m "numpy.f2py"
                   "${fortran_src_file}"
                   -m "fibby"
                   --lower # Important
  DEPENDS fib1.f # Fortran source
)

# Set up target
Python_add_library(${CMAKE_PROJECT_NAME} MODULE WITH_SOABI
  "${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_c}" # Generated
  "${F2PY_INCLUDE_DIR}/fortranobject.c" # From NumPy
  "${fortran_src_file}" # Fortran source(s)
)

# Depend on sources
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE Python::NumPy)
add_dependencies(${CMAKE_PROJECT_NAME} genpyf)
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE "${F2PY_INCLUDE_DIR}")

上面定義的 CMakeLists.txt 檔案的一個關鍵要素是,使用 add_custom_command 來產生 wrapper C 檔案,然後透過 add_custom_target 指令將其作為實際共享函式庫目標的依賴項添加,這可以防止命令每次都運行。此外,用於獲取 fortranobject.c 檔案的方法也可用於在較舊的 cmake 版本上抓取 numpy 標頭檔。

這樣一來,它的運作方式與其他模組相同,儘管命名慣例有所不同,並且輸出函式庫不會自動以 cython 資訊作為前綴。

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.]

當現有的工具鏈已經存在,且不鼓勵使用 scikit-build 或其他額外的 python 依賴項時,這特別有用。