Issue
I'm using CMake to build a moderate-sized Fortran project. I noticed that sometimes when I refactor modules I ran into weird compilation errors that are gone after clean rebuild in a fresh build directory.
I've managed to reproduce the problem in the following minimal example. Consider a project with two libraries and a single executable
├── build
├── CMakeLists.txt
├── liba
│ ├── CMakeLists.txt
│ ├── first.f90
│ └── second.f90
├── libb
│ ├── CMakeLists.txt
│ └── third.f90
└── main.f90
The main.f90
is simply
program test
use first, only: x
use second, only: y
use third, only: z
implicit none
print *, x, y, z
end program
and the root CMakeLists.txt
is
project(test)
enable_language(Fortran)
add_subdirectory(liba)
add_subdirectory(libb)
add_executable(main main.f90)
target_link_libraries(main a b)
Each module just exports a single variable
! 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
The libraries are described similarly as
# 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})
Building and running I get 1 2 3
, as expected.
Now the problem. If I move the second.f90
to libb
and change y = 2
to y = 42
I still have 1 2 3
as the output, even if I do full rebuild using make -C build clean all
. This is because liba
output directory contains second.mod
with outdated parameter value y = 2
and it is used instead of the correct module in the libb
build directory. The Ninja generator behaves just the same.
I greatly appreciate any suggestions how to fix my build scripts to avoid this kind of errors.
Just in case you want to try it yourself, here is the example on GitHub:
$ git clone https://github.com/uranix/cmake-fortran-modules
$ cd cmake-fortran-modules
$ git checkout a90ddfc
$ mkdir build
$ cmake -B build
$ make -C build
$ ./build/main
1 2 3
$ git checkout 736b738
$ make -C build
$ ./build/main
1 2 3
$ make -C build clean all
$ ./build/main
1 2 3
$ mkdir build2
$ cmake -B build2
$ make -C build2
$ ./build2/main
1 42 3
PS. I've heard that C++20 also has modules and I wonder whether the problem can be reproduced with C++ instead of Fortran. I'll check and expand the question later
Solution
While there are some complications that specifically stem from module files (or any generated files), the underlying issue here can really be reproduced even with normal libraries.
CMake already handles dependency tracking for module files, knows when to rebuild targets based on time stamps and only when the module file has actually changed. A lot of this happens as build time due to the nature of generated files (you can view a lot of happens with make VERBOSE=1
).
It also takes care of only cleaning the stuff that it has caused; if a target generates second.mod
, a make clean
will only remove second.mod
.
For all cmake can know, extra files might have been generated by some external tools or custom commands, so expecting it to figure out that there are orphaned mod files from an earlier different cmake configuration is just asking too much.
When you check out the different branch, which has a completely different setup, the first thing that happens, even if you start off with a clean
, is that cmake is triggered and rebuilds all the dependencies and such to match the new setup, orphaning any files generated by the previous build.
If you decided to rename liba
to libc
, you'd see the orphaned liba
directory left over.
As I see, you have a few options;
- You can make sure to
clean
before you switch branches (or start the major refactoring).
$ git clone https://github.com/uranix/cmake-fortran-modules
$ cd cmake-fortran-modules
$ git checkout a90ddfc
$ mkdir build
$ cmake -B build
$ make -C build
$ ./build/main
1 2 3
$ make -C build clean # make sure to do this before refactoring!
$ git checkout 736b738
$ make -C build
$ ./build/main # and it will work
1 42 3
You could possibly even add this as a local git hook (but i don't think there exists a pre-checkout hook)
- You can accept that such major refactorings require a clean slate.
- Might be possible to enrich your
clean
command by combining aglob
for*.mod
files and settingset_property(DIRECTORY PROPERTY ADDITIONAL_MAKE_CLEAN_FILES list_of_mod_files)
. Still would require a manualclean
, so not my favorite option. You could of course also just run a manualrm build/lib*/*.mod
- You can use a shared mod directory by setting Fortran_MODULE_DIRECTORY for each target. Modify each
lib{a,b}/CMakeLists
accordingly
set_target_properties(a PROPERTIES Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/mod)
target_include_directories(a PUBLIC ${CMAKE_BINARY_DIR}/mod)
set_target_include_directories(b PROPERTIES Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/mod)
target_include_directories(b PUBLIC ${CMAKE_BINARY_DIR}/mod)
assuming that is a good fit for your submodules. Note that this still wouldn't be detect against yet another orphaned fourth.mod
which might accidentally been left over by a library you removed completely, but still was in USE
by the main program.
Answered By - Mikael Öhman Answer Checked By - David Goodson (WPSolving Volunteer)