Using Cmake defined variables in c++ code

Using Cmake defined variables in c++ code

C++ project versioning

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.txtfile.

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)

Source