Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 181 additions & 135 deletions Utilities/python_wrapper/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,152 +1,198 @@
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
find_package(PythonExtensions REQUIRED)
find_package(NumPy REQUIRED)
find_package(F2PY REQUIRED)
find_package(F90Wrap REQUIRED)

# Fortran preprocessing compiler
if(CMAKE_Fortran_COMPILER_ID STREQUAL Intel)
set(FPP_COMPILER fpp)
set(FPP_COMPILE_FLAGS "")
# =============================================================================
# Python wrapper for SPEC using f90wrap and f2py
# =============================================================================

# Load Python and various components
find_package(Python 3.10 REQUIRED
COMPONENTS Interpreter Development.Module NumPy
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure setting a version here is a good idea?

)

# Get the path of the `f2py` include directory that will be needed for the library compilation
execute_process(
COMMAND "${Python_EXECUTABLE}" -c "import numpy.f2py; print(numpy.f2py.get_include())"
OUTPUT_VARIABLE F2PY_INCLUDE_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
message(STATUS "f2py include dir: ${F2PY_INCLUDE_DIR}")

# Create the list of Fortran files to include in the Python module
# These come from src/CMakeLists.txt via fortran_src_files variable
set(spec4py_f90_src_files ${fortran_src_files})
message(STATUS "Fortran source files for Python wrapper: ${spec4py_f90_src_files}")

# =============================================================================
# Step 1: Preprocess Fortran files using the Fortran compiler preprocessor
# =============================================================================

# Set preprocessor flags for each compiler
if(CMAKE_Fortran_COMPILER_ID STREQUAL "GNU")
set(PP_FLAG "-cpp")
set(PP_ONLY_FLAG "-E")
elseif(CMAKE_Fortran_COMPILER_ID MATCHES "Intel")
set(PP_FLAG "-fpp")
set(PP_ONLY_FLAG "-E")
elseif(CMAKE_Fortran_COMPILER_ID MATCHES "NVHPC|PGI")
set(PP_FLAG "-Mpreprocess")
set(PP_ONLY_FLAG "-E")
else()
set(FPP_COMPILER ${CMAKE_Fortran_COMPILER})
set(FPP_COMPILE_FLAGS -E -cpp)
message(WARNING "Unknown Fortran compiler: preprocessing may not work")
set(PP_FLAG "-cpp")
set(PP_ONLY_FLAG "-E")
endif()

get_directory_property(COMP_DEFS COMPILE_DEFINITIONS)
message(STATUS "Compile definitions for preprocessor are ${COMP_DEFS}")
string(REPLACE ";" " " COMP_DEF_STR "${COMPILE_DEFINITIONS}")


function(preprocess_fortran outvar)
message(STATUS "preprocess_fortran arguments: ${outvar}, followed by ${ARGN}")
set(srcs)
foreach(f ${ARGN})
# is it a Fortran file?
if(f MATCHES "\\.[Ff](9[05])?")
message(STATUS "Got fortran file: ${f}")
# construct output filename
if(NOT IS_ABSOLUTE "${f}")
get_filename_component(f "${f}" ABSOLUTE)
endif()
file(RELATIVE_PATH r "${CMAKE_CURRENT_SOURCE_DIR}" "${f}")
get_filename_component(e "${r}" EXT)
get_filename_component(n "${r}" NAME_WE)
get_filename_component(p "${r}" PATH)
set(of "${CMAKE_CURRENT_BINARY_DIR}/${n}_fpp${e}")
message(STATUS "Output name: ${of}")
# preprocess the thing
if (CMAKE_Fortran_COMPILER_ID STREQUAL Intel)
add_custom_command(OUTPUT "${of}"
COMMAND ${FPP_COMPILER} ${FPP_COMPILE_FLAGS} ${COMP_DEF_STR} "${f}" "${of}"
IMPLICIT_DEPENDS Fortran "${f}"
COMMENT "Preprocessing ${f}"
VERBATIM
)
else()
add_custom_command(OUTPUT "${of}"
COMMAND ${FPP_COMPILER} ${FPP_COMPILE_FLAGS} ${COMP_DEF_STR} "${f}" -o "${of}"
IMPLICIT_DEPENDS Fortran "${f}"
COMMENT "Preprocessing ${f}"
VERBATIM
)
endif()
list(APPEND srcs "${of}")
#else()
# list(APPEND srcs "${f}")
endif()
endforeach()
# return the (preprocessed) sources
set(${outvar} "${srcs}" PARENT_SCOPE)
endfunction()

#message(STATUS "fortran_src_files is ${fortran_src_files}")
preprocess_fortran(fpp_files ${fortran_src_files})
#message(STATUS "fpp_files is ${fpp_files}")

# ----------------------------------------------------------------------------
# NOTE: There is no way to identify the f90wrap---.f90 files ahead of running f90wrap
# NOTE: The files produced have no one->one relation with the source files.
# NOTE: So giving the names of f90wrap_---.f90 files manually
#-----------------------------------------------------------------------------
set(f90wrap_output_files ${CMAKE_CURRENT_BINARY_DIR}/f90wrap_global_m_fpp.f90
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_inputlist_m_fpp.f90
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_intghs_m_fpp.f90
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_msphdf5_m_fpp.f90
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_newton_m_fpp.f90
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_toplevel.f90
)

set(kind_map_file ${CMAKE_CURRENT_SOURCE_DIR}/kind_map)
set(python_mod_name spec_f90wrapped)
set(python_mod_file ${CMAKE_CURRENT_BINARY_DIR}/${python_mod_name}.py)

add_custom_target(preprocessing ALL
DEPENDS ${fpp_files}
)

add_custom_command(OUTPUT ${python_mod_file} ${f90wrap_output_files}
COMMAND "${F90Wrap_EXECUTABLE}" -m "${python_mod_name}" ${fpp_files} -k "${kind_map_file}"
#IMPLICIT_DEPENDS Fortran ${fpp_files}
DEPENDS ${fpp_files} ${kind_map_file}
COMMENT "Executing F90Wrap for" ${fpp_files}
VERBATIM
# Preprocess each Fortran source file
foreach(src_file ${spec4py_f90_src_files})
get_filename_component(basename "${src_file}" NAME_WE)
set(pp_file "${CMAKE_CURRENT_BINARY_DIR}/${basename}_fpp.F90")
list(APPEND spec4py_f90_pp_files "${pp_file}")
add_custom_command(
COMMAND ${CMAKE_Fortran_COMPILER} ${PP_FLAG} ${PP_ONLY_FLAG} ${src_file}
-o ${pp_file}
DEPENDS ${src_file}
OUTPUT ${pp_file}
COMMENT "Preprocess ${src_file} -> ${basename}_fpp.F90"
)
endforeach()

message(STATUS "Preprocessed files: ${spec4py_f90_pp_files}")

# =============================================================================
# Step 2: Run f90wrap to generate Fortran wrapper files
# =============================================================================

# f90wrap generates wrapper files only for modules that have wrappable content.
# These are the known output files based on the SPEC source structure.
# f90wrap_toplevel.f90 contains the standalone subroutines.
set(f90wrap_output_files
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_global_m_fpp.f90
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_inputlist_m_fpp.f90
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_intghs_m_fpp.f90
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_msphdf5_m_fpp.f90
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_newton_m_fpp.f90
${CMAKE_CURRENT_BINARY_DIR}/f90wrap_toplevel.f90
)

#add_custom_target("${python_mod_name}_pymod"
# DEPENDS ${python_mod_file} ${f90wrap_output_files}
#)
# The module name must be "spec_f90wrapped" to be compatible with simsopt
# which imports "spec.spec_f90wrapped"
set(f90wrap_module_name "spec_f90wrapped")
set(f90wrap_python_file "${CMAKE_CURRENT_BINARY_DIR}/${f90wrap_module_name}.py")

set(f2py_module_name "_${python_mod_name}")
set(generated_module_file ${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_name}${PYTHON_EXTENSION_MODULE_SUFFIX})
message(STATUS "Python exten suffix expansion: ${PYTHON_EXTENSION_MODULE_SUFFIX}")
message(STATUS "f90_wrap_output_files: " ${f90wrap_output_files})
message(STATUS "f2py_module_name: ${f2py_module_name}")
message(STATUS "generated_module_name: ${generated_module_file}")
add_custom_command(
COMMAND "${Python_EXECUTABLE}"
-m f90wrap
${spec4py_f90_pp_files}
--mod-name "${f90wrap_module_name}"
--kind-map "${CMAKE_CURRENT_SOURCE_DIR}/kind_map"
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS ${spec4py_f90_pp_files}
OUTPUT ${f90wrap_output_files} ${f90wrap_python_file}
COMMENT "Generate Fortran interfaces using f90wrap"
VERBATIM
)

include_directories("${NumPy_INCLUDE_DIRS}" "${F2PY_INCLUDE_DIRS}" "${CMAKE_CURRENT_BINARY_DIR}")
add_custom_target(${f2py_module_name} ALL
DEPENDS ${generated_module_file} spec ${f90wrap_output_files}
add_custom_target(
f90wrap_SPEC
DEPENDS ${f90wrap_output_files} ${f90wrap_python_file}
)

# =============================================================================
# Step 3: Run f2py-f90wrap to generate C interface code
# =============================================================================

set(f2py_module_name "_${f90wrap_module_name}")
set(f2py_c_file "${CMAKE_CURRENT_BINARY_DIR}/${f2py_module_name}module.c")

add_custom_command(
OUTPUT ${generated_module_file}
COMMAND ${F2PY_EXECUTABLE}
-m ${f2py_module_name}
--build-dir ${CMAKE_CURRENT_BINARY_DIR}
--f90exec=${CMAKE_Fortran_COMPILER}
--f77exec=${CMAKE_Fortran_COMPILER}
--f90flags="-fopenmp"
-lgomp
-c
#${SCALAPACK_LIB} ${NETCDF_F} ${NETCDF_C}
${f90wrap_output_files}
-I${CMAKE_BINARY_DIR}/build/modules/spec_modules
-I${HDF5_Fortran_INCLUDE_DIRS}
--verbose
${CMAKE_BINARY_DIR}/build/lib/libspec.a
${SPEC_LINK_LIB}
#IMPLICIT_DEPENDS Fortran ${f90wrap_output_files}
DEPENDS spec ${f90wrap_output_files}
COMMAND f2py-f90wrap
-m "${f2py_module_name}"
--lower
${f90wrap_output_files}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
#VERBATIM
COMMAND_EXPAND_LISTS
DEPENDS ${f90wrap_output_files}
OUTPUT ${f2py_c_file}
COMMENT "Generate C interfaces for Python using f2py-f90wrap"
VERBATIM
)

python_extension_module(${generated_module_file})
install(FILES ${python_mod_file} ${generated_module_file} #${CMAKE_CURRENT_SOURCE_DIR}/__init__.py
DESTINATION Utilities/python_wrapper/spec
add_custom_target(
f2py_SPEC
DEPENDS ${f2py_c_file}
)

#set(PYINIT_STR "import sys\nimport os.path\nsys.path.append(os.path.dirname(__file__))\n\nfrom .spec import *\n")
#set(PYINIT_FILE ${CMAKE_CURRENT_BINARY_DIR}/__init__.py)
#FILE(WRITE ${PYINIT_FILE} ${PYINIT_STR})
#install(FILES ${python_mod_file} ${generated_module_file} ${PYINIT_FILE}
# DESTINATION spec # Here spec is directory location
#)
#install(TARGETS xspec spec)
# LIBRARY DESTINATION ${CMAKE_INSTALL_DIR}/.
# RUNTIME DESTINATION bin
#)
# =============================================================================
# Step 4: Build the Python extension module
# =============================================================================

# Create the shared library using the generated files
add_library("${f2py_module_name}" MODULE
${f2py_c_file}
"${F2PY_INCLUDE_DIR}/fortranobject.c"
${f90wrap_output_files}
)

# Set the library properties for Python extension
set_target_properties("${f2py_module_name}" PROPERTIES
PREFIX ""
SUFFIX ".${Python_SOABI}.so"
C_VISIBILITY_PRESET hidden
Fortran_MODULE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/modules
)

# Tell CMake that the source files are generated
set_source_files_properties(
${f2py_c_file}
${f90wrap_output_files}
PROPERTIES GENERATED TRUE
)

target_include_directories("${f2py_module_name}" PRIVATE
${F2PY_INCLUDE_DIR}
${Python_NumPy_INCLUDE_DIRS}
${Python_INCLUDE_DIRS}
${CMAKE_BINARY_DIR}/src
${CMAKE_Fortran_MODULE_DIRECTORY}/spec_modules
)

# Compile options for the wrapper Fortran files
target_compile_options("${f2py_module_name}" PRIVATE
$<$<COMPILE_LANGUAGE:Fortran>:-cpp>
$<$<AND:$<COMPILE_LANGUAGE:Fortran>,$<Fortran_COMPILER_ID:GNU>>:-ffree-line-length-none>
$<$<AND:$<COMPILE_LANGUAGE:Fortran>,$<Fortran_COMPILER_ID:GNU>>:-fdefault-real-8>
$<$<AND:$<COMPILE_LANGUAGE:Fortran>,$<Fortran_COMPILER_ID:GNU>>:-fPIC>
)

target_link_libraries("${f2py_module_name}" PRIVATE
Python::NumPy
spec
${SPEC_LINK_LIB}
)

# Ensure proper build order
add_dependencies("${f2py_module_name}" f90wrap_SPEC f2py_SPEC spec)

# =============================================================================
# Step 5: Installation
# =============================================================================

# The package name is "spec" and the wrapper module inside is "spec_f90wrapped"
# This matches what simsopt expects: "import spec.spec_f90wrapped"
set(spec_package_name "spec")

# Install the existing spec package files from the source directory
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/spec/"
DESTINATION "${spec_package_name}"
COMPONENT python_wrapper
FILES_MATCHING PATTERN "*.py"
)

# Install the generated Python wrapper file (spec_f90wrapped.py)
install(FILES "${f90wrap_python_file}"
DESTINATION "${spec_package_name}"
COMPONENT python_wrapper
)

# Install the shared library into the spec package
install(TARGETS "${f2py_module_name}"
LIBRARY DESTINATION "${spec_package_name}"
COMPONENT python_wrapper
)
Loading