CMake Basics #2 - Version, Option, and Installation

Last Edited: 1/24/2025

This blog post introduces versioning, option, and installation set up in CMake.

DevOps

In the previous article, we covered how to set up a CMakeLists.txt file for a small and basic project. In this article, we will cover some concepts that become more relevant when setting up large projects: versions, options, and installation.

Versions

When working on a large project, we might distribute various versions of the project. We can assign a version to a project using the project argument, like project(Project VERSION 1.0). To allow the source code to access the version set in CMakeLists.txt, we can ask CMake to generate a configuration file, ProjectConfig.h, as shown below.

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
 
project(PROJECT VERSION 1.0)
 
configure_file(ProjectConfig.h.in ProjectConfig.h)
 
add_executable(${PROJECT_NAME} main.cc)
 
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_BINARY_DIR})

We use configure_file to generate ProjectConfig.h at the build location using ProjectConfig.h.in, which we include using target_include_directories. ProjectConfig.h.in can access the versioning information as follows.

ProjectConfig.h.in
#define PROJECT_VERSION_MAJOR "@Project_VERSION_MAJOR@" // if version is 1.0, major is 1.
#define PROJECT_VERSION_MINOR "@Project_VERSION_MINOR@" // if version is 1.0, minor is 0.

The source code can include ProjectConfig.h to access those variables defined as macros, as shown below.

#include <iostream>
#include <ProjectConfig.h>
 
int main(int argc, char* argv[]) {
 
    std::cout << argv[0] << " Version: " << 
    PROJECT_VERSION_MAJOR << "." << PROJECT_VERSION_MINOR << std::endl;
 
    return 0;
};

Options

In certain large projects, we might have extra features that make use of optional libraries. In such cases, we can allow users to select whether to use the library and its extra features or not with options. Suppose we have <Addition.h> that offers add functions, which perform addition in a magical way that is faster than normal addition in C++. We can set up an option for using the library as follows.

CMakeLists.txt
option(USE_ADDITION "A magical library for optimizing addition." ON)
 
if (USE_ADDITION)
    add_subdirectory(Addition)
    list(APPEND OPTIONAL_LIBS Addition)
    list(APPEND OPTIONAL_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/Addition/include)
endif ()
 
target_link_libraries(${PROJECT_NAME} LibraryA ${OPTIONAL_LIBS})
 
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/LibraryA/include ${OPTIONAL_INCLUDE_DIRS})

We make use of option to set the variable USE_ADDITION to ON by default and decide whether to add the library according to USE_ADDITION. We can define USE_ADDITION in ProjectConfig.h.in with #define USE_ADDITION, which we can utilize in the source code as follows.

#include <iostream>
#include <ProjectConfig.h>
 
#ifdef USE_ADDITION
    #include <Addition.h>
#endif
 
int main(int argc, char* argv[]) {
 
#ifdef USE_ADDITION
    std::cout << "Using Addition Library: 1+2=" << add(1, 2) << std::endl;
#elif
    std::cout << "Using Normal Addition 1+2=" << 1+2 << std::endl;
#endif
 
    return 0;
};

Find Executable & Libraries

Instead of setting up executables and libraries within our codebase and requiring developers and users to install them on their devices, we can assume that they have already installed the executables and libraries on their computers and make use of them. Especially for large executables or libraries that most of us tend to have already installed, like Git (which will be covered in the DevOps series), we do not want to include Git in our project directory. Instead, we want to use the version installed on their computer. To ensure the computer has the necessary executable installed and obtain its path, we can use find_program as follows.

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
 
project(PROJECT VERSION 1.0)
 
find_program(GIT_EXECUTABLE git)
 
if (GIT_EXECUTABLE)
    message(STATUS "Git found at: ${GIT_EXECUTABLE}")
else ()
    message(FATAL_ERROR "Git not found")
endif ()

The find_program command checks if the executable exists and determines the full path to the executable. We can use message to indicate whether the executable is found. For libraries, we can use find_library to check the path to the required math library, libm.so, as follows.

CMakeLists.txt
find_library(MATH_LIBRARY m)
if (MATH_LIBRARY)
    message(STATUS "Math library found: ${MATH_LIBRARY}")
else ()
    message(FATAL_ERROR "Math library not found")
endif ()
target_link_libraries(${PROJECT_NAME} PRIVATE ${MATH_LIBRARY})

A library might have a Find<name>.cmake file set up. For those libraries, You can use find_package to find the library path along with additional information typically required for using the package. For example, we can use FindGit.cmake to get the path to the executable and the version of Git installed.

CMakeLists.txt
find_package(Git)
if (GIT_FOUND)
    message("Git found: ${GIT_EXECUTABLE}, Version: ${GIT_VERSION_STRING}")
    execute_process(COMMAND ${GIT_EXECUTABLE} init)
else ()
    message(FATAL_ERROR "Git not found")
endif ()

The example above also uses execute_process to run an executable with the path obtained using find_package. When creating a large library, it is highly recommended to set up a Find<name>.cmake file for easier access to the library.

Installation

After building a large project, we can distribute it and allow users or developers to install it on their computers. We can use CMake and install to achieve this, but beginners with no background in CMake will need to learn how to build with CMake. We can simplify the process by including documentation in readme.md as follows.

readme.md
To install the project, you need to install CMake and Git and run the following commands:
 
    cmake -S . -DUSE_ADDITION=ON -B build
    cd build
    sudo make install

To hide those details, we can create shell scripts like configure.sh, build.sh, and install.sh, each containing the commands above, and make users run these shell scripts. However, this approach might still be complicated for beginners or users with no experience working with the command line. Hence, we can use CPack, which provides users with an easy interface for installing the package on any OS. To use it, we just need to include the lines below.

CMakeLists.txt
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set(CPACK_VERSION_MAJOR "${Project_VERSION_MAJOR}")
set(CPACK_VERSION_MINOR "${Project_VERSION_MINOR}")
include(CPack)

CPack requires a License.txt file to be set up at the root directory. We can move to the build directory and run cpack to compile the project. On Linux, the above will create Project--Linux.tar.gz and Project--Linux.tar.Z files in the build directory, which can be installed by running the Project--Linux.sh file created along with them. There are many configurations available for CPack, including those for setting up a GUI, but this is the very basics of CPack.

Conclusion

In this article, we covered the methods of setting versions and options, along with methods for finding libraries and setting up installation. For more information on functions in CMake, I recommend checking the official documentation of CMake for your version.

Resources