如何避免CMake使用过时的Fortran模块
在中等规模Fortran项目中使用CMake构建时,重构模块(如将模块文件从一个库移动到另一个库)后,即使执行make -C build clean all全量重建,依然会出现编译结果不符合预期的情况——程序会加载旧的模块文件而非新的。
复现示例
项目初始目录结构:
├── build ├── CMakeLists.txt ├── liba │ ├── CMakeLists.txt │ ├── first.f90 │ └── second.f90 ├── libb │ ├── CMakeLists.txt │ └── third.f90 └── main.f90
核心文件内容:
main.f90:
program test use first, only: x use second, only: y use third, only: z implicit none print *, x, y, z end program
- 根
CMakeLists.txt:
project(test) enable_language(Fortran) add_subdirectory(liba) add_subdirectory(libb) add_executable(main main.f90) target_link_libraries(main a b)
- 库的
CMakeLists.txt:
# liba/CMakeLists.txt add_library(a first.f90 second.f90 ) target_include_directories(a PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) # libb/CMakeLists.txt add_library(b third.f90 ) target_include_directories(b PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
- 模块文件(每个导出一个参数):
! first.f90 module first integer, parameter :: x = 1 end module first ! second.f90 module second integer, parameter :: y = 2 end module second ! third.f90 module third integer, parameter :: z = 3 end module third
问题触发步骤:
- 初始构建运行,输出
1 2 3符合预期。 - 将
second.f90移至libb,并修改y = 42。 - 执行
make -C build clean all后重建,程序仍输出1 2 3,而非预期的1 42 3。
问题原因
每个库都将自身的二进制根目录(CMAKE_CURRENT_BINARY_DIR)设为公开的include目录,当模块从liba移到libb后:
liba的构建目标不再生成second.mod,但之前构建生成的旧second.mod仍残留在liba的二进制目录中。- 编译
main时,CMake的include路径会包含liba和libb的二进制目录,若liba的路径优先级更高,编译器会优先加载旧的second.mod,导致结果不符合预期。 make clean仅清理当前构建目标的产物,不会删除其他库目录中遗留的旧模块文件。
解决方案
1. 为每个库设置独立的模块输出目录
通过set_target_properties为每个库指定专属的模块输出目录,避免模块文件混放,同时确保include路径仅指向自身的模块目录:
修改liba/CMakeLists.txt:
add_library(a first.f90) # 为库a指定独立的模块输出目录 set_target_properties(a PROPERTIES Fortran_MODULE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/modules") # 公开该模块目录而非二进制根目录 target_include_directories(a PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/modules")
修改libb/CMakeLists.txt(移动second.f90后):
add_library(b third.f90 second.f90) set_target_properties(b PROPERTIES Fortran_MODULE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/modules") target_include_directories(b PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/modules")
2. 全局统一模块目录并按库划分(可选)
在根CMakeLists.txt中设置全局模块目录,同时为每个库指定子目录,确保不同库的模块文件隔离:
project(test) enable_language(Fortran) # 设置全局模块根目录 set(CMAKE_Fortran_MODULE_DIRECTORY "${CMAKE_BINARY_DIR}/modules") add_subdirectory(liba) add_subdirectory(libb) add_executable(main main.f90) target_link_libraries(main a b)
然后在每个库的CMakeLists.txt中指定子目录:
# liba/CMakeLists.txt add_library(a first.f90) set_target_properties(a PROPERTIES Fortran_MODULE_DIRECTORY "${CMAKE_Fortran_MODULE_DIRECTORY}/a") target_include_directories(a PUBLIC "${CMAKE_Fortran_MODULE_DIRECTORY}/a")
这样所有模块文件都会被整理到build/modules/<库名>目录下,旧模块会随着对应库的清理被移除,且路径不会冲突。
3. 清理时删除所有模块目录(临时 workaround)
若临时需要解决现有构建目录的问题,可手动删除所有模块文件目录,或在CMake中添加自定义清理目标:
add_custom_target(clean-modules COMMAND ${CMAKE_COMMAND} -E remove_directory "${CMAKE_BINARY_DIR}/modules" COMMENT "Cleaning all Fortran module files" )
执行make clean-modules all即可彻底清理旧模块并重建。
关键原理
Fortran的.mod文件是编译时生成的接口文件,CMake默认将其输出到目标的二进制目录。通过为每个目标指定独立的模块目录,可从根本上避免不同库的模块文件冲突,同时确保当模块移动后,旧的模块文件不会被其他目标的include路径加载。
内容的提问来源于stack exchange,提问作者uranix

