Detecting your environment
Objectives
Learn how to discover the operating system and processor capabilities.
Learn how to handle platform- and compiler-dependent source code.
CMake comes pre-configured with sane defaults for a multitude of properties of the environments in which it can be used. Default generator, default compilers, and compiler flags are few and most notable examples of this up-front configuration.
Run the following if you are curious about what kind of configuration ships for your version of CMake and operating system:
$ cmake --system-information
In this episode, we will show how to use CMake to introspect the environment in which we are running. This is very common in build systems, since it allows to customize the creation of artifacts on-the-fly.
Discovering the operating system
Sometimes we need to write code that performs different operations based on
compile-time constants.
Like in this example (example.cpp
):
#include <cstdlib>
#include <iostream>
#include <string>
std::string say_hello() {
#ifdef IS_WINDOWS
return std::string("Hello from Windows!");
#elif IS_LINUX
return std::string("Hello from Linux!");
#elif IS_MACOS
return std::string("Hello from macOS!");
#else
return std::string("Hello from an unknown system!");
#endif
}
int main() {
std::cout << say_hello() << std::endl;
return EXIT_SUCCESS;
}
Conditional compilation with preprocessor definitions
We can do this with the following CMakeLists.txt
:
# set minimum cmake version
cmake_minimum_required(VERSION 3.14)
# project name and language
project(example LANGUAGES CXX)
# define executable and its source file
add_executable(example example.cpp)
# let the preprocessor know about the system name
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_compile_definitions(example PUBLIC "IS_LINUX")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
target_compile_definitions(example PUBLIC "IS_MACOS")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_compile_definitions(example PUBLIC "IS_WINDOWS")
endif()
We achieved this with a combination of host system
introspection and the target_compile_definitions
command.
Discovering processor capabilities and generating a configuration file
A common customization is to apply processor-specific compiler flags. We can
gain such information on the host system with the built-in
cmake_host_system_information
command.
Another thing that is common and convenient is to have a single file containing
all these compile-time constants, rather than passing them to preprocessor.
This can be achieved by having a scaffold file and then letting CMake
configure it with configure_file
after discovering the values for all the
necessary compile-time constants.
Here is an example source file (example.cpp
):
#include "config.h"
#include <cstdlib>
#include <iostream>
int main() {
std::cout << "Number of logical cores: " << NUMBER_OF_LOGICAL_CORES << std::endl;
std::cout << "Number of physical cores: " << NUMBER_OF_PHYSICAL_CORES << std::endl;
std::cout << "Processor is 64Bit: " << IS_64BIT << std::endl;
std::cout << "Processor supports SSE2 instructions: " << HAS_SSE2 << std::endl;
std::cout << "OS name: " << OS_NAME << std::endl;
std::cout << "OS sub-type: " << OS_RELEASE << std::endl;
// we will add these two later:
// std::cout << "Compiler: " << COMPILER << std::endl;
// std::cout << "Compiler version: " << COMPILER_VERSION << std::endl;
return EXIT_SUCCESS;
}
The file config.h
does not exist, it will be generated at configure time
from config-template.h
:
#pragma once
#define NUMBER_OF_LOGICAL_CORES @_NUMBER_OF_LOGICAL_CORES@
#define NUMBER_OF_PHYSICAL_CORES @_NUMBER_OF_PHYSICAL_CORES@
#define IS_64BIT @_IS_64BIT@
#define HAS_SSE2 @_HAS_SSE2@
#define OS_NAME "@_OS_NAME@"
#define OS_RELEASE "@_OS_RELEASE@"
Here is the CMakeLists.txt
which takes care of introspection and also
generates the file config.h
:
cmake_minimum_required(VERSION 3.14)
project(example LANGUAGES CXX)
include(CMakePrintHelpers)
foreach(key
IN ITEMS
NUMBER_OF_LOGICAL_CORES
NUMBER_OF_PHYSICAL_CORES
IS_64BIT
HAS_SSE2
OS_NAME
OS_RELEASE
)
# query the item ${key} and save its value in the variable _${key}
cmake_host_system_information(RESULT _${key} QUERY ${key})
cmake_print_variables(_${key})
endforeach()
add_executable(example example.cpp)
# this is here because the config.h will be generated in PROJECT_BINARY_DIR
target_include_directories(example
PRIVATE
${PROJECT_BINARY_DIR}
)
configure_file(config-template.h config.h @ONLY)
Exercise: record more useful information using configure_file
Your goal is to adapt the above example (content/examples/configure-file/problem/
)
and make it possible to get the compiler and compiler version into the
output of your code:
#include "config.h"
#include <cstdlib>
#include <iostream>
int main() {
std::cout << "Number of logical cores: " << NUMBER_OF_LOGICAL_CORES << std::endl;
std::cout << "Number of physical cores: " << NUMBER_OF_PHYSICAL_CORES << std::endl;
std::cout << "Processor is 64Bit: " << IS_64BIT << std::endl;
std::cout << "Processor supports SSE2 instructions: " << HAS_SSE2 << std::endl;
std::cout << "OS name: " << OS_NAME << std::endl;
std::cout << "OS sub-type: " << OS_RELEASE << std::endl;
// we will add these two later:
// std::cout << "Compiler: " << COMPILER << std::endl;
// std::cout << "Compiler version: " << COMPILER_VERSION << std::endl;
return EXIT_SUCCESS;
}
Hints:
You will only need to uncomment these and only have to modify
configure-template.h
.You will not need to modify
CMakeLists.txt
.To find the relevant CMake variable names, have a look in Compile flags, definitions, and debugging.
Discussion
What else to record when configuring? Here are some ideas:
Code version and/or Git hash
Compiler versions
Compiler flags
Compile-time definitions
Library versions
Some environment variables
Discuss how they can be useful for you and others running your code.
Keypoints
CMake can introspect the host system.
You can build source code differently, based on the OS, the processor, the compiler, or any combination thereof.
You can generate source code when configuring the project with
configure_file
.