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

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.
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.
#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.
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.
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.
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.
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.
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.
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
- Code, Tech, and Tutorials. 2021. CMake Tutorial EP 4 | Versioning Source Code. YouTube.
- Code, Tech, and Tutorials. 2021. CMake Tutorial EP 5 | Making Libs Optional. YouTube.
- Code, Tech, and Tutorials. 2021. CMake Tutorial EP 6 | Installing Your Software! (part 1/2 of install). YouTube.
- Code, Tech, and Tutorials. 2021. CMake Tutorial EP 7 | Installing With CPack! (part 2/2 of install). YouTube.
- Code, Tech, and Tutorials. 2021. CMake Tutorial EP 8 | find_library(...) (part 1/2 of find lib). YouTube.
- Code, Tech, and Tutorials. 2021. CMake Tutorial EP 9 | find_package modules and config options (2/2 of find libs). YouTube.