

Introduction:
Through previous studies, we have mastered the basic knowledge of CMake and C++. Although CMake is cross-platform, sometimes the source code is not entirely portable. To ensure that our source code can be cross-platform, configuring and/or building the code according to different platforms is an essential part of the project build process.

✦
Detecting Operating System
✦
CMake
is a set of cross-platform tools. In actual development, we need operating system-specific CMake
code, which enables conditional compilation based on the operating system or uses compiler-specific extensions when available or necessary.
Here is a specific example:
Windows
and Unix
systems have distinct file structures. For instance, when integrating a deep learning model into a C++
software system, we want to copy the deep learning model (*.pth) to a specified file:
if (MSVC)
file(GLOB MODEL "${CMAKE_SOURCE_DIR}/resource/*.pt")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${MODEL}
$<TARGET_FILE_DIR:${PROJECT_NAME}>)
elseif(UNIX)
file(GLOB MODEL "${CMAKE_SOURCE_DIR}/resource/*.pt")
file(COPY ${MODEL} DESTINATION ${EXECUTE_FILE})
endif()
This CMake
code is used to copy the model files to the output directory after building the project, depending on the target platform (Windows
or UNIX/Linux
), ensuring that the required model files (with a .pt
extension) are located in the same directory as the executable.
For MSVC
(Microsoft Visual C++ compiler, typically used on Windows
platform):
-
Use the
file()
command with theGLOB
option to find all.pt
model files in theresource
directory under theCMake
source directory (${CMAKE_SOURCE_DIR}
). -
Then, use the
add_custom_command()
command to add a custom post-build command to the target${PROJECT_NAME}
. -
The custom command will copy all found
.pt
model files to the output directory ($<TARGET_FILE_DIR:${PROJECT_NAME}>
). Using thecopy_if_different
parameter ensures that files are copied only when the target file differs from the source or when the target directory does not contain the file.
For UNIX
platforms (including Linux
):
-
Use the
file()
command with theGLOB
option to find all.pt
model files in theresource
directory under theCMake
source directory (${CMAKE_SOURCE_DIR}
). -
Then, use the
file()
command with theCOPY
option to copy all found.pt
model files to the specified target directory (${EXECUTE_FILE}
).
Next, we will demonstrate how to use CMake
to detect the operating system through an example that does not require compiling any source code.
Project address:
https://gitee.com/jiangli01/tutorials/tree/master/cmake-tutorial/chapter2/01
CMakeLists.txt
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(os_test)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(STATUS "Configuring on/for Linux")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
message(STATUS "Configuring on/for macOs")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
message(STATUS "Configuring on/for Windows")
elseif(CMAKE_SYSTEM_NAME STREQUAL "AIX")
message(STATUS "Configuring on/for IBM AIX")
else()
message(STATUS "Configuring on/for ${CMAKE_SYSTEM_NAME}")
endif()
Output Result
-- The C compiler identification is GNU 9.4.0
-- The CXX compiler identification is GNU 9.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring on/for Linux
-- Configuring done
-- Generating done
-- Build files have been written to: /home/jiangli/repo/tutorials/cmake-tutorial/chapter2/01/build
CMake
defines the CMAKE_SYSTEM_NAME
variable for the target operating system, so there is no need to use custom commands, tools, or scripts to query this information. The value of this variable can then be used to implement OS-specific conditions and solutions.
-
On
macOS
,CMAKE_SYSTEM_NAME
isDarwin
. -
On
Linux
andWindows
,CMAKE_SYSTEM_NAME
isLinux
andWindows
respectively. We have learned how to execute specificCMake
code on specific operating systems. Of course, we should minimize such customizations to simplify the process of migrating to new platforms.
Note: To minimize the cost of transferring from one platform to another, we should avoid using Shell
commands directly, and also avoid explicit path separators
(forward slashes on Linux
and macOS
, and backslashes on Windows
). Use only forward slashes as path separators in CMake
code, and CMake
will automatically convert them to the appropriate format for the operating system involved.
✦
Handling Compiler-Related Source Code
✦
For portability, we try to avoid writing new code, but when there are dependencies, we must address them, especially when dealing with legacy code or compiler-dependent tools.
Project address:
https://gitee.com/jiangli01/tutorials/tree/master/cmake-tutorial/chapter2/02
CMakeLists.txt
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(hello_os LANGUAGES CXX)
add_executable(${PROJECT_NAME} hello_os.cpp)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_compile_definitions(${PROJECT_NAME} PUBLIC "IS_LINUX")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
target_compile_definitions(${PROJECT_NAME} PUBLIC "IS_MACOS")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_compile_definitions(${PROJECT_NAME} PUBLIC "IS_WINDOWS")
endif()
By using the target_compile_definitions()
command, we add predefined macros IS_LINUX
, IS_MACOS
, or IS_WINDOWS
to the target hello_os
, which will take effect during the compilation process.
target_compile_definitions
restricts the definitions to a specific target and limits their visibility using PRIVATE
|PUBLIC
|INTERFACE
qualifiers:
-
PRIVATE
: The compile definitions will only apply to the given target and not to related other targets. -
INTERFACE
: The compile definitions for the given target will only apply to the targets that use it. -
PUBLIC
: The compile definitions will apply to the given target and all other targets that use it.
Of course, in C++
, we can directly use predefined macros to identify different platforms and operating systems. These predefined macros are provided by the compiler or operating system and can be used in the source code to write platform-specific code. Here are some commonly used platform identification macros:
-
__APPLE__
: Defined on Apple platforms (e.g.,macOS
andiOS
). -
__linux__
: Defined onLinux
platforms. -
_WIN32
: Defined on 32-bitWindows
operating systems. -
_WIN64
: Defined on 64-bitWindows
operating systems. -
_MSC_VER
: Defined when using theMicrosoft Visual C++
compiler, indicating the version number of the compiler. -
__GNUC__
: Defined when using theGNU
compiler (e.g.,g++
), indicating the version number of the compiler.
hello_os.cpp
#include <string>
#include <iostream>
std::string HelloOS();
int main() {
std::cout << HelloOS() << std::endl;
return EXIT_SUCCESS;
}
std::string HelloOS() {
#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
}
On Windows
, you will see Hello
from Windows
. Other operating systems will produce different outputs.
✦
Detecting and Processing Processor Architecture
✦
In the 1970s, the emergence of 64-bit
integer operations and the introduction of 64-bit addressing
for personal computers in the early 21st century expanded the memory addressing range, leading developers to invest significant resources in porting hardcoded 32-bit
architectures to support 64-bit addressing
. While avoiding explicit hardcoding is wise, adaptations to hardcoding limitations must be made in code configured with CMake
. Project address:
https://gitee.com/jiangli01/tutorials/tree/master/cmake-tutorial/chapter2/03
CMakeLists.txt
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(arch_dependent LANGUAGES CXX)
add_executable(
${PROJECT_NAME}
${CMAKE_SOURCE_DIR}/arch_dependent.cpp
)
# Check the size of the pointer type
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
target_compile_definitions(
${PROJECT_NAME}
PUBLIC "IS_64_BIT_ARCH"
)
message(STATUS "Target is 64 bits")
else()
target_compile_definitions(
${PROJECT_NAME}
PUBLIC "IS_32_BIT_ARCH"
)
message(STATUS "Target is 32 bits")
endif()
# Define target compile definitions to inform the preprocessor about the host processor architecture
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "i386")
message(STATUS "i386 architecture detected")
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "i686")
message(STATUS "i686 architecture detected")
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64")
message(STATUS "x86_64 architecture detected")
else()
message(STATUS "host processor architecture is unknown")
endif()
target_compile_definitions(
${PROJECT_NAME}
PUBLIC "ARCHITECTURE=${CMAKE_HOST_SYSTEM_PROCESSOR}"
)
CMake
defines the CMAKE_HOST_SYSTEM_PROCESSOR
variable to include the name of the processor currently running. It can be set to i386
, i686
, x86_64
, AMD64
, etc., depending on the current CPU
.
CMAKE_SIZEOF_VOID_P
gives the size of the void pointer
. It can be queried during CMake
configuration to modify target or target compile definitions. Based on the detected host processor architecture, preprocessor definitions can be used to determine which branches of source code need to be compiled.
Of course, such dependencies should be avoided when writing new code, but they are useful when dealing with legacy code or cross-compilation.
Note: Using CMAKE_SIZEOF_VOID_P
is the only “truly” portable way to check whether the current CPU
has 32-bit
or 64-bit
architecture. arch_dependent.cpp
#include <cstdlib>
#include <iostream>
#include <string>
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
std::string ArchInfo();
int main() {
std::cout << ArchInfo() << std::endl;
return EXIT_SUCCESS;
}
std::string ArchInfo() {
std::string arch_info(TOSTRING(ARCHITECTURE));
arch_info += std::string(" architecture. ");
#ifdef IS_32_BIT_ARCH
return arch_info + std::string("Compiled on a 32 bit host processor.");
#elif IS_64_BIT_ARCH
return arch_info + std::string("Compiled on a 64 bit host processor.");
#else
return arch_info + std::string("Neither 32 nor 64 bit, puzzling ...");
#endif
}
Output Result
mkdir build
cd build
cmake ..
-- The CXX compiler identification is GNU 9.4.0
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Target is 64 bits
-- x86_64 architecture detected
-- Configuring done
-- Generating done
-- Build files have been written to: /home/jiangli/repo/tutorials/cmake-tutorial/chapter2/03/build
make
Scanning dependencies of target arch_dependent
[ 50%] Building CXX object CMakeFiles/arch_dependent.dir/arch_dependent.cpp.o
[100%] Linking CXX executable arch_dependent
[100%] Built target arch_dependent
./arch_dependent
x86_64 architecture. Compiled on a 64 bit host processor.
Appendix In addition to CMAKE_HOST_SYSTEM_PROCESSOR
, CMake
also defines the CMAKE_SYSTEM_PROCESSOR
variable. The former contains the name of the CPU
currently running in CMake
, while the latter contains the name of the CPU
currently being built for. This is a subtle difference that plays a crucial role in cross-compilation.
Another way to let CMake
detect the host processor architecture is to use symbols defined in C
or C++
, combined with CMake
‘s try_run
function:
#if defined(__i386) || defined(__i386__) || defined(_M_IX86)
#error cmake_arch i386
#elif defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(_M_X64)
#error cmake_arch x86_64
#endif
This strategy is also the recommended way to detect the target processor architecture, as CMake
does not seem to provide a portable intrinsic solution.
Another option is to use only CMake
without any preprocessor directives, at the cost of setting different source files for each case and then using the target_source
command to set them as source files dependent on the executable target arch_dependent
:
add_executable(arch-dependent "")
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "i386")
message(STATUS "i386 architecture detected")
target_sources(arch_dependent
PRIVATE
arch_dependent_i386.cpp
)
elif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "i686")
message(STATUS "i686 architecture detected")
target_sources(arch_dependent
PRIVATE
arch_dependent_i686.cpp
)
elif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64")
message(STATUS "x86_64 architecture detected")
target_sources(arch_dependent
PRIVATE
arch_dependent_x86_64.cpp
)
else()
message(STATUS "host processor architecture is unknown")
endif()
This method clearly requires more work on existing projects, as source files need to be separated. Additionally, code duplication between different source files will definitely become an issue.
✦
Detecting Processor Instruction Set
✦
CMake
can detect the instruction set supported by the host processor. This feature was added to CMake
in newer versions and requires CMake 3.10
or higher. The detected host system information can be used to set corresponding compiler flags, implement optional source code compilation, or generate source code based on the host system.
We will generate a config.h
file using config.h.in
. The config.h.in
file is as follows:
#ifndef CONFIG_HEADER_IN_H
#define CONFIG_HEADER_IN_H
#define NUMBER_OF_LOGICAL_CORES @_NUMBER_OF_LOGICAL_CORES@
#define NUMBER_OF_PHYSICAL_CORES @_NUMBER_OF_PHYSICAL_CORES@
#define TOTAL_VIRTUAL_MEMORY @_TOTAL_VIRTUAL_MEMORY@
#define AVAILABLE_VIRTUAL_MEMORY @_AVAILABLE_VIRTUAL_MEMORY@
#define TOTAL_PHYSICAL_MEMORY @_TOTAL_PHYSICAL_MEMORY@
#define AVAILABLE_PHYSICAL_MEMORY @_AVAILABLE_PHYSICAL_MEMORY@
#define IS_64BIT @_IS_64BIT@
#define HAS_FPU @_HAS_FPU@
#define HAS_MMX @_HAS_MMX@
#define HAS_MMX_PLUS @_HAS_MMX_PLUS@
#define HAS_SSE @_HAS_SSE@
#define HAS_SSE2 @_HAS_SSE2@
#define HAS_SSE_FP @_HAS_SSE_FP@
#define HAS_SSE_MMX @_HAS_SSE_MMX@
#define HAS_AMD_3DNOW @_HAS_AMD_3DNOW@
#define HAS_AMD_3DNOW_PLUS @_HAS_AMD_3DNOW_PLUS@
#define HAS_IA64 @_HAS_IA64@
#define OS_NAME "@_OS_NAME@"
#define OS_RELEASE "@_OS_RELEASE@"
#define OS_VERSION "@_OS_VERSION@"
#define OS_PLATFORM "@_OS_PLATFORM@"
#endif // ! CONFIG_HEADER_IN_H
CMakeLists.txt
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(progressor_info LANGUAGES CXX)
add_executable(${PROJECT_NAME} "")
target_sources(${PROJECT_NAME}
PRIVATE ${CMAKE_SOURCE_DIR}/processor_info.cpp
)
target_include_directories(${PROJECT_NAME}
PRIVATE ${PROJECT_BINARY_DIR}
)
foreach(key
IN ITEMS
NUMBER_OF_LOGICAL_CORES
NUMBER_OF_PHYSICAL_CORES
TOTAL_VIRTUAL_MEMORY
AVAILABLE_VIRTUAL_MEMORY
TOTAL_PHYSICAL_MEMORY
AVAILABLE_PHYSICAL_MEMORY
IS_64BIT
HAS_FPU
HAS_MMX
HAS_MMX_PLUS
HAS_SSE
HAS_SSE2
HAS_SSE_FP
HAS_SSE_MMX
HAS_AMD_3DNOW
HAS_AMD_3DNOW_PLUS
HAS_IA64
OS_NAME
OS_RELEASE
OS_VERSION
OS_PLATFORM
)
cmake_host_system_information(RESULT _${key} QUERY ${key})
endforeach()
configure_file(config.h.in config.h @ONLY)
The foreach
loop queries multiple keys and defines corresponding variables. The cmake_host_system_information
queries the system information of the host system running CMake
. In this example, a function call is made for each key. Then, these variables are used to configure the placeholders in config.h.in
, inputting and generating config.h
.
This configuration is completed using the configure_file
command. Finally, config.h
is included in processor_info.cpp
. After compilation, it will print the values to the screen.
target_include_directories(${PROJECT_NAME}
PRIVATE ${PROJECT_BINARY_DIR}
)
This will link the generated executable file to the folder where the executable file is located.
After compilation, config.h
will be generated in build
, and the content generated in the native environment is as follows:
#ifndef CONFIG_HEADER_IN_H
#define CONFIG_HEADER_IN_H
#define NUMBER_OF_LOGICAL_CORES 16
#define NUMBER_OF_PHYSICAL_CORES 16
#define TOTAL_VIRTUAL_MEMORY 2047
#define AVAILABLE_VIRTUAL_MEMORY 2047
#define TOTAL_PHYSICAL_MEMORY 7903
#define AVAILABLE_PHYSICAL_MEMORY 6007
#define IS_64BIT 1
#define HAS_FPU 1
#define HAS_MMX 1
#define HAS_MMX_PLUS 0
#define HAS_SSE 1
#define HAS_SSE2 1
#define HAS_SSE_FP 0
#define HAS_SSE_MMX 0
#define HAS_AMD_3DNOW 0
#define HAS_AMD_3DNOW_PLUS 0
#define HAS_IA64 0
#define OS_NAME "Linux"
#define OS_RELEASE "5.15.0-78-generic"
#define OS_VERSION "#85~20.04.1-Ubuntu SMP Mon Jul 17 09:42:39 UTC 2023"
#define OS_PLATFORM "x86_64"
#endif // ! CONFIG_HEADER_IN_H
processor_info.cpp
#include <cstdlib>
#include <iostream>
#include "config.h"
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 << "Total virtual memory in megabytes: " << TOTAL_VIRTUAL_MEMORY
<< std::endl;
std::cout << "Available virtual memory in megabytes: "
<< AVAILABLE_VIRTUAL_MEMORY << std::endl;
std::cout << "Total physical memory in megabytes: " << TOTAL_PHYSICAL_MEMORY
<< std::endl;
std::cout << "Available physical memory in megabytes: "
<< AVAILABLE_PHYSICAL_MEMORY << std::endl;
std::cout << "Processor is 64Bit: " << IS_64BIT << std::endl;
std::cout << "Processor has floating point unit: " << HAS_FPU << std::endl;
std::cout << "Processor supports MMX instructions: " << HAS_MMX << std::endl;
std::cout << "Processor supports Ext. MMX instructions: " << HAS_MMX_PLUS
<< std::endl;
std::cout << "Processor supports SSE instructions: " << HAS_SSE << std::endl;
std::cout << "Processor supports SSE2 instructions: " << HAS_SSE2
<< std::endl;
std::cout << "Processor supports SSE FP instructions: " << HAS_SSE_FP
<< std::endl;
std::cout << "Processor supports SSE MMX instructions: " << HAS_SSE_MMX
<< std::endl;
std::cout << "Processor supports 3DNow instructions: " << HAS_AMD_3DNOW
<< std::endl;
std::cout << "Processor supports 3DNow+ instructions: " << HAS_AMD_3DNOW_PLUS
<< std::endl;
std::cout << "IA64 processor emulating x86 : " << HAS_IA64 << std::endl;
std::cout << "OS name: " << OS_NAME << std::endl;
std::cout << "OS sub-type: " << OS_RELEASE << std::endl;
std::cout << "OS build ID: " << OS_VERSION << std::endl;
std::cout << "OS platform: " << OS_PLATFORM << std::endl;
return EXIT_SUCCESS;
}
Finally: May everything go smoothly for you, and may you become the person you want to be!

