Cmake is a wonderful tool for generating building systems for projects. One of the applications that Cmake facilitates, is configuring the project version from within a CMakeLists.txt
file.
This can be easily done by defining the version components as either, cmake variables or as definitions to the compiler command line. This article will give an overview of both methods.
Semantic versioning which is one of the popular methods used to version software gives the project a version number composed of 3 components: Major
, Minor
and the patch
. Details on the meaning of each number go out of this article's scope but it can be checked on this link.
The simplest way of defining a project version is by defining a header file containing the version components as global variables. This header file can then be included to make variables available for various uses. Doing things this way leaves us with the problem of maintaining these variables at the code level instead of aggregating this task to the build system.
So how can we avoid defining the project version in multiple locations? In the code (if we need it there), in the build system or the source packaging.
1. Cmake variables
The idea behind the first method is to use configure_file()
command to copy an input file to another location and modify its content. The configuration file can be a .h
header file that contains the definition of the project version. All variables with the decoration @cmake_variable@
will be replaced with their values given in the CMakeLists.txt
file.
Cmake variables can be defined in the CMakeLists.txt
using the set()
command. For our tutorial, it is sufficient to have them of normal type. The following CMakeLists.txt
file shows how to set them:
cmake_minimum_required (VERSION 3.2)
project (MyProject)
# Add the variables
set(VERSION_MAJOR 0)
set(VERSION_MINOR 1)
set(VERSION_PATCH 0)
# generate version.h replacing all @VARIABLE@
configure_file (version.h.in ${CMAKE_CURRENT_SOURCE_DIR}/version.h @ONLY)
add_executable (${PROJECT_NAME} main.cpp)
The command configure_file()
will copy the content of the file version.h.in
and creates a new file called version.h
in ${CMAKE_CURRENT_SOURCE_DIR}
which is the location where the source files of the project are located. Since we added the argument @ONLY
to the configure_file()
call. The content of version.h
will be similar to version.h.in
except that all references of the form @variable@
will be replaced with what has been set in the CMakeLists.txt
. Of course, this happens during the build (running make
command).
configure_file()
can be checked here.
@ONLY
Restrict variable replacement to references of the form
@VAR@
. This is useful for configuring scripts that use${VAR}
syntax.
The content of the version.h.in
:
#ifndef VERSION_H_IN
#define VERSION_H_IN
#define PROJECT_NAME "@PROJECT_NAME@"
#define VERSION_MAJOR "@VERSION_MAJOR@"
#define VERSION_MINOR "@VERSION_MINOR@"
#define VERSION_PATCH "@VERSION_PATCH@"
#endif // VERSION_H_IN
version.h
will be the following:
#ifndef VERSION_H_IN
#define VERSION_H_IN
#define PROJECT_NAME "MyProject"
#define VERSION_MAJOR "0"
#define VERSION_MINOR "1"
#define VERSION_PATCH "0"
#endif // VERSION_H_IN
Finally, we can include version.h
and use the project versions in main.cpp
like this:
#include <iostream>
#include "version.h"
auto main() -> int
{
// use veriables from CMakeLists.txt
std::cout << "project name " << PROJECT_NAME << std::endl;
std::cout << "verions major: " << VERSION_MAJOR << std::endl;
std::cout << "version minor: " << VERSION_MINOR << std::endl;
std::cout << "version patch: " << VERSION_PATCH << std::endl;
return EXIT_SUCCESS;
}
2. Definitions to the compiler command line
The idea behind the second method is to pass the project version components as pre-processor directives to the compiler which will translate them during compilation and replace them in the target.
For such a purpose we can rely on the #define
pre-processor in c++. This pre-processor will allow us to create a c++ macro. The macro is "simply" a word that will be replaced during compilation time with the value given to it.
The same main.cpp
of the first method can be transformed into:
#include <iostream>
#define VERSION_MAJOR _VERSION_MAJOR
#define VERSION_MINOR _VERSION_MINOR
#define VERSION_PATCH _VERSION_PATCH
auto main() -> int
{
// use veriable from CMakeLists.txt
std::cout << "verions major: " << VERSION_MAJOR << std::endl;
std::cout << "version minor: " << VERSION_MINOR << std::endl;
std::cout << "version patch: " << VERSION_PATCH << std::endl;
return EXIT_SUCCESS;
}
During the compilation VERSION_MAJOR
, for example, will take the value _VERSION_MAJOR
and when executing the target, it is as if we have written _VERSION_MAJOR
in the std::cout
.
Of course, the compiler does not have any definition for _VERSION_MAJOR
yet. These definitions can be passed to the compiler via the flag -D
or equivalently to the CMakeLists.txt
as follow:
cmake_minimum_required (VERSION 3.2)
project (MyProject2)
# Add the variables we need
ADD_DEFINITIONS( -D_VERSION_MAJOR=\"0\")
ADD_DEFINITIONS( -D_VERSION_MINOR=\"1\")
ADD_DEFINITIONS( -D_VERSION_PATCH=\"0\")
add_executable (${PROJECT_NAME} main.cpp)
I should draw the reader's attention that this is not the only cmake command cable of adding compilation definitions as you may read in this link.
Which method to take?
Which method to take? does not have a big impact. Depending on the project size and the number of libraries used it may be preferable to use the config_file()
approach as this will keep the call to the compiler small. (refer to this thread)