Skip to content

[WIP] intro to cmake #503

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
170 changes: 170 additions & 0 deletions source/learn/building_programs/how_to_cmake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# A brief introduction to CMake

In a [previous section](project_make.md) the concept of Makefiles was introduced. Here, an alternative build system - CMake, is discussed.

[CMake](https://cmake.org/) is a very popular build system heavily popularized and used within the C/C++ community. CMake is an open-source
project within the Kitware organization.

Formally, CMake is a build-system generator. In short, CMake uses user coded directives to scan the source code to build and generates the
files that can be used to compile the code, for example, Makefiles. As discussed in the Makefiles section, the module dependencies are up to the
programmer to figure out. CMake automates this and allows the programmer to not worry about module dependencies.

Additionally, CMake has myriad of features embedded into its "language" that allow for simple addition of dependencies, external libraries, etc.

However, CMake removes the Makefile from the programmer. Thus, you don't have as fine grained control of how each file is compiled. This is a particular
reason why some programmers dislike CMake.

CMake can be downloaded from [here](https://cmake.org/download/). CMake cross-platform capabilities, allowing you to use it as a single build system
to deploy on Mac, Windows, Linux with little overhead.

To exemplify the extendability of CMake we will build a small yet fun project.

## A simple example

Start by git cloning the following [Github repository](https://github.com/JorgeG94/fortran_cmake_hello)

```
git clone [email protected]:JorgeG94/fortran_cmake_hello.git
```

We will do a example that compiles a project that contains both fixed and free format files, this will let us showcase some of CMake's cool features.
Our directory structure is simple:

.
├── CMakeLists.txt
├── source
├── CMakeLists.txt
├── main.f90
└── f90
├── CMakeLists.txt
├── module.f90
├── module2.f90
├── module3.f90
└── f77
├── CMakeLists.txt
├── subroutine.f

By having a file in fixed format I will exemplify how you can adapt a modern build system into a project that might be using outdated
language features/standards.

Old F77 routines rarely used `modules` and rely on free-floating subroutines, which sometimes depend on modules, as the code has been "modernized".

Strategies for modernization are discussed in a later section. But first, let's isolate the f77 routines from the newer f90 ones. We will achieve
this with CMake by putting the f77 routines in a separate static library to those written in f90. Our target is to compile two static libraries:

- f90 library
- f77 library

Finally, the executable main.f90 will be created and linked to the static libraries which contains the needed subroutines/modules.

We start by creating our top level CMakeLists.txt file. Which will set the compilers, libraries, and final project:

```
# set the minimum version
cmake_minimum_required(VERSION 3.22)

# Set the project name and specify that it is a Fortran project
project(MyFortranProject LANGUAGES Fortran)

# Set the directory to store the compiled Fortran modules (.mod files)
set(CMAKE_Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/modules)

# create libraries for the f90 and f77 code
add_library(f90_lib STATIC)
add_library(f77_lib STATIC)

# Ensure the module files from the library are stored in the module directory
set_target_properties(f90_lib PROPERTIES Fortran_MODULE_DIRECTORY ${CMAKE_Fortran_MODULE_DIRECTORY})

# Set compile flags for the module library (-Wall)
target_compile_options(f90_lib PRIVATE -Wall)

# Add the executable target for the main Fortran file in the source/ directory
add_executable(main_executable source/main.f90)

# Set compile flags for the main executable (-O3)
target_compile_options(main_executable PRIVATE -O3)

# Make sure the main executable can find the modules in the specified directory
target_include_directories(main_executable PRIVATE ${CMAKE_Fortran_MODULE_DIRECTORY})

# Link the libraries into the main executable
target_link_libraries(main_executable PRIVATE f90_lib f77_lib)

# now, where are our source files?
add_subdirectory(source)
```

Now, we need to create the CMakeLists.txt files that help define our project. The next CMakeLists.txt file is at the source/ level:

```
#add subdirectories f90 and f77 to the source directory
add_subdirectory(f90)
add_subdirectory(f77)
```

You can see that this is a very simple way of handling source directories, if there were more in here, they can simply be added to the project
by doing `add_subdirectory(xyz)`. Now, each directory, which now contains source files, needs to have its own CMakeLists.txt:

```
# this is source/f90
# add the f90 file
# add the *.f90 sources we've chosen to the target static library we've created
target_sources(f90_lib PRIVATE module.f90 module2.f90 module3.f90)
```

Similarly, the f77...

```
# this is source/f77
# add the f77 file
# add the *.f77 sources we've chosen to the target static library we've created
target_sources(f77_lib PRIVATE subroutine.f)
```

Now we're ready to build and compile our project. From the top level directory, we now create a `build` directory where we will actually compile the project:

```
mkdir build
cd build
cmake ../
make -j
```

Once you execute the CMake step, you should see something similar to:

```
-- The Fortran compiler identification is GNU 9.4.0
-- Detecting Fortran compiler ABI info
-- Detecting Fortran compiler ABI info - done
-- Check for working Fortran compiler: /usr/bin/f95 - skipped
-- Configuring done (0.6s)
-- Generating done (0.0s)
-- Build files have been written to: /home/jorgegv/personal-dev/fortran/fortran-cmake/build
```

Which indicates that is has found the gfortran compiler and the needed dependencies to create a build. Inside the build/ directory, there will now be
certain files:

```
CMakeCache.txt CMakeFiles Makefile cmake_install.cmake modules source
```

These are the files that control the entire build system of the project. From here, simply doing `make -j` will build our app correctly.

## More complex module dependencies

CMake, like humans, is not perfect and will fail for complex module dependencies if you perform a parallel build (`make -j`) with sufficient cores.
This is because race conditions between files and modules are generated which cannot be fullfilled. To prevent this happening and ensuring
that your compilation never fails due to parallel race conditions, one can create dependencies between libraries. For example: I know
that my free floating, legacy subroutines, depend on certain modules being built; but I have 700 files to compile and want to use the 128 cores
of my machine... `make -j` fails.

Because we were smart, we have separated files into libraries of their own, so we can set a dependency on f77 library needing f90 library to be
compiled first. This can be done by `add_dependencies(f77_lib f90_lib)` this has told CMake that before you can start compiling the
f77 library, f90 needs to be done first.

This is mostly an edge case but you should be aware of this happening. With regular Make it cannot happen because you've explicitly
written the dependencies of each module and each file.