Using CMake to Build Software
CMake is both a tool and a scripting language that is used to build various software libraries and programs. This section offers details on to use it to build libraries. Most projects that use CMake follow a similar pattern to getting things setup and built. This section will outline general concepts that you will often see when building a project setup with CMake. There are bound to be deviations that you will encounter in various projects, and it always a good idea to read the project's documentation for more details on how it expects CMake to be invoked, or as a last resort, there's always the CMake project files themselves.
One important tip for any CMake build: if you change ANY CMake related option, whether it be the actual CMake code or changing an option passed to cmake, it is always a good idea to remove the old CMake configuration (such as the build folder) and start fresh. Once you get more comfortable with CMake you will find that this is not always necessary, but doing this practice can help prevent issues related to stale CMake files.
Getting Started
Once you've downloaded a piece of software, check the build instructions to see if CMake is the preferred building tool. It never hurts to check the README file, but you can double check if a project can be built using CMake by checking for any CMakeLists.txt files in the repository. Most projects will have the main CMake file in the top level of the project, but not always. Once you've verified that the project can be built with CMake, the next steps are to collect the following bits of information:
1. Where are you going to build the project (i.e. where should the temporary build files be stored)?
2. Where are you going to install the project (if it needs to be installed)?
3. What build options does the library provide / have you been told to use?
4. Do you need to provide paths to any external libraries to the project?
5. Are there any other default CMake settings you need to change?
The rest of this section will go through each of these and explain common actions you may have to do for each.
The Build Folder
To get started with the process of building a project with CMake, it needs a place to store all of the temporary files it makes (such as the cache of CMake options, intermediate files built by the project itself, temporary .o files, etc.). Generally, most projects will work with a simple build folder inside the top level of the repo:
#<cd to repo>
mkdir build
cd build
cmake .. # Explained later
However, you are also able to create the build folder outside of the repo, and in a location that works with your workflow. If you do this, the only difference is that you have to point the cmake command to the main CMakeLists.txt (usually the one in the top level of a project). In the above example, the path was .. since the main CMake file was in the folder one level up from the build/ directory we created.
It is generally recommended to do a build folder as opposed to not doing one so that everything can be easily removed when not needed (such as after an install, or needing to blow away everything and redo the build process) and to prevent CMake from grabbing the wrong files when building. Additionally, by having everything in one folder, it is easier to tell git to ignore the whole build folder.
Setting Up Your Installation Destination
Now that you have the build folder created, it's time to figure out where you want the project to eventually be installed to. To do this, set CMAKE_INSTALL_PREFIX to the location you want the project file(s) to be installed to:
#<cd to repo>
mkdir build
cd build
cmake -DCMAKE_INSTALL_PREFIX=/path/you/want ..
Note that you need to add a -D before the CMake variable and an = before your path. You can also use a variable (using bash as an example):
#<cd to repo>
mkdir build
cd build
MY_PATH=/path/you/want
cmake -DCMAKE_INSTALL_PREFIX=$MY_PATH ..
If you do not provide this variable, CMake will assume you want to install into /usr/local/, which requires root access to install to. If you are on your own system and you wish to skip providing an install path, just note that this path is a default path searched by many libraries and LD_LIBRARY_PATH/PRELOAD, and could cause odd complications in the future (such as installing multiple MPI implementations into the same path). Like the build folder, having a custom installation folder makes it easy to remove the install and start over.
Configuring Project Options
As is often the case, the project you are building will offer some settings that you can tweak to change the behavior of the application. Usually these are documented in the project README, but some may not be so obvious. Like the install path, all of these options are given to the cmake command with the -D flag.
Below is an example of using some project specific options (in this case, for the Kokkos library):
cmake \
-DCMAKE_INSTALL_PREFIX=/some/fictional/path \
-DKokkos_ENABLE_HIP=ON \
-DKokkos_ARCH_AMD_GFX90A=ON \
```
Most variables are set to a string value, but some variables feature the boolean `ON` and `OFF` values. Case usually matters for the string variables, but refer to the documentation for the particular variables to make sure. As an example, `-DCMAKE_BUILD_TYPE`, is technically a case-sensitive variable, though many (but not all) codes that use it often convert it to uppercase before working on it, making it case-insensitive.
`ccmake` is additional tool that enables you to set all the variables from a GUI. The GUI can be used to show all the variables a project offers (as well as the `CMakeCache.txt` and the project's main `CMakeLists.txt`). For variables that have a set of allowed values, `ccmake` can cycle between them. Most `-D` options should work with `ccmake`, allowing `ccmake` to be used as a direct replacement to `cmake`.
### Providing External Libraries
Some libraries and applications require external dependencies that CMake is not privy to finding on its own. For example, CMake knows how to (try to) find an MPI library on your system, but not how to find Kokkos or Caliper. To help assist a CMake project find these dependencies, applications often offer two approaches to pointing CMake to the correct files.
The first, and more modern, way is using CMake modules. In this method,`-DCMAKE_PREFIX_PATH` should be set to the top-level install folder for each dependency (not to the location of the source code, and not to subdirectories like `/lib`, `/include`, `/share`, or `/bin`). This value can contain multiple paths separated by semi-colons like such:
```bash
cmake -DCMAKE_PREFIX_PATH="/path/to/libraryA/install;/path/to/libraryB/install" # Quotes are needed here!
The second option is project-specific. Without getting into the weeds of CMake code, a library could provide a custom file (often ending with
.cmake or in a cmake/ folder) for finding the dependency, which CMake will use as a guide. In this case, the variable you pass to CMake will vary, but will likely follow a syntax like -DLIBRARY_FOO_PATH=. Again, refer to the documentation of each project to determine what variables are used to control external dependencies.
Regardless of which method is used, there should be print-outs as CMake runs that identify the files found, and it is a good idea to watch the output to make sure CMake found the correct files. We also recommend watching which compiler was selected as occasionally CMake can pick up compilers and/or software that you did not intend for it to pick up.
Configuring Common CMake variables
Here are a set of common CMake variables that you may need to tweak when building a CMake project:
# Setting build type
-DCMAKE_BUILD_TYPE=<option>
# Where "option" is one of the following: Debug, Release, RelWithDebInfo and MinSizeRel
# An application may define more if it chooses to.
# An application may also default to a specific one (usually Release),
# but it's always good to set one.
# Setting C/CXX compiler
-DCMAKE_C_COMPILER=<compiler> # or path to C compiler if not on $PATH
-DCMAKE_CXX_COMPILER=<compiler> # or path to CXX compiler if not on $PATH
# Setting C/CXX compile flags
-DCMAKE_C_FLAGS="c compiler flags here"
-DCMAKE_CXX_FLAGS="cxx compiler flags here"
# Setting install location
-DCMAKE_INSTALL_PREFIX=/path/to/where/to/install/project
# Telling CMake other locations to look for CMake modules
-DCMAKE_PREFIX_PATH="/path/to/libraryA/install;/path/to/libraryB/install" # Quotes are needed here!
These options all have defaults, so none are required, but knowing how to tweak these can help simplify building CMake projects. As a reminder, any time you tweak a CMake-related option (flag or CMake code), it is a good idea to remove old files before running
cmake! You do not have to do this if you are only tweaking source code (e.g. a .cpp or .h file).
Putting it All Together
Here's an example of using CMake to build a project (MPIPCL):
cmake -DCMAKE_BUILD_TYPE=Debug \ # CMake option to set build type
-DCMAKE_INSTALL_PREFIX=/home/theta/apps/mpipcl/ \ # Path to where I want to install MPIPCL
-DWITH_DEBUG=ON \ # MPIPCL-specific option
.. # Location to main CMakeLists.txt
Running the above command generates the following output (note that compilers, file paths, MPI version will vary based on system):
-- The C compiler identification is GNU 13.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Build type: Debug -> debug
-- Enabling debugging print outs
-- Found MPI_C: /home/theta/apps/openmpi/4.1.5/lib/libmpi.so (found version "3.1")
-- Found MPI: TRUE (found version "3.1")
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
-- Found Threads: TRUE
-- The CXX compiler identification is GNU 13.3.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found MPI_CXX: /home/theta/apps/openmpi/4.1.5/lib/libmpi.so (found version "3.1")
-- Found MPI: TRUE (found version "3.1")
-- Configuring done (3.0s)
-- Generating done (0.0s)
-- Build files have been written to: /home/theta/git/MPIPCL/build
Because MPIPCL is a project that uses both C and C++, we can check which compilers CMake found with these lines:
-- The C compiler identification is GNU 13.3.0
-- Check for working C compiler: /usr/bin/cc - skipped
-- The CXX compiler identification is GNU 13.3.0
-- Check for working CXX compiler: /usr/bin/c++ - skipped
Here, the C compiler is the system's
cc and the C++ compiler is the system's c++. The "skipped" simply means CMake already knows the compiler will work to compile the language and does not need to test the compiler before using it.
Continuing on, we can see the MPI libraries CMake found for the project:
-- Found MPI_C: /home/theta/apps/openmpi/4.1.5/lib/libmpi.so (found version "3.1")
-- Found MPI_CXX: /home/theta/apps/openmpi/4.1.5/lib/libmpi.so (found version "3.1")
These are the language specific compilers for MPI (
mpicc and mpic++) and not the C++ bindings for MPI. The version printed at the end are the MPI standard version the library supports, not the specific version of the MPI library.
Note that because there are print outs in MPIPCL's CMake code, you can tell our options were used and set to our wishes:
-- Build type: Debug -> debug
-- Enabling debugging print outs
If you ever have an option that goes un-used by the CMake project, you will likely see a message like this:
CMake Warning:
Manually-specified variables were not used by the project:
FOO_EXAMPLE
Receiving this warning does not stop the project from configuring, but double check that this warning is ok for your build before continuing. Being able to parse the output given in the
cmake configuration phase is key to ensuring a smoother build process.
Building Your Library
Now that you have your code configured, it's now time to actually build it. Assuming you didn't change the default build system generator, the next step is to simply run make:
# In directory where "cmake" was run
make
# - OR if you want to do a multi-process build:
make -j8 # where "8" is the number of threads to simultaneously build
# - OR if you want to see the exact commands make is doing:
make VERBOSE=1
# - And finally, if you want to install it:
make install
# And if you want to combine things, you can:
make VERBOSE=1 install
# ... and so on
make -j4 install
Generally, some variation of the above commands are all you want to do here. Rarely, you may find that you need to tweak the command being run manually when the compilation fails, but it is advised that you tweak the CMake code instead, and it is not advised to adjust any of the Makefiles produced.
Sometimes you are just building a project to run the executable, so once you've built that file, you can stop here. For projects that build libraries or other files to be used in another project, it is highly encouraged that you install it (and use a consistent scheme for installing software into your file system). If you ever need to go back and look at any intermediate files built by CMake, they can usually be found inside the build folder, in a location that usually mirrors the source file location.
Installing your library
Assuming the build of the project went smoothly and the install was not to a location that needs root access, you should see something like this (continuing with the MPIPCL example):
make install
<build process omitted>
Install the project...
-- Install configuration: "RELEASE"
-- Installing: /home/theta/apps/mpipcl/lib/libMPIPCL.so.1.5.0
-- Installing: /home/theta/apps/mpipcl/lib/libMPIPCL.so.1
-- Set non-toolchain portion of runtime path of "/home/theta/apps/mpipcl/lib/libMPIPCL.so.1.5.0" to ""
-- Up-to-date: /home/theta/apps/mpipcl/lib/libMPIPCL.so
-- Installing: /home/theta/apps/mpipcl/include/mpipcl.h
-- Installing: /home/theta/apps/mpipcl/share/MPIPCL/MPIPCLConfig-version.cmake
-- Installing: /home/theta/apps/mpipcl/share/MPIPCL/MPIPCLConfig.cmake
-- Installing: /home/theta/apps/mpipcl/share/MPIPCL/MPIPCLTargets.cmake
-- Installing: /home/theta/apps/mpipcl/share/MPIPCL/MPIPCLTargets-release.cmake
This output shows you exactly what was and was not installed.
Up-to-date means the file in the install folder matches the current one that was to be installed. Be sure to check that the path is also where you expected files to go! The exact subdirectory where things go is up to the project, but generally libraries go in lib/ and headers into include/. CMake module components can either go into share/ (as shown above), but the can also occasionally go into the lib/ folder. Both locations are acceptable to CMake when a project looks for a module.