Previously, I translated the official documentation of CMake
, and then I organized a personal guide at a beginner level. Over time, I have compiled some commonly used CMake
usages in project management.
-
Cross-Platform -
Versioning Techniques – Windows
-
Output Directory for Artifacts -
Multi-Module Management
Cross-Platform
Referencing some open-source projects, I have extracted some concise examples as follows:
# If PROJECT_ARCH is not specified, try to guess it
if(NOT DEFINED PROJECT_ARCH)
if(OS_WINDOWS AND "${CMAKE_GENERATOR_PLATFORM}" STREQUAL "arm64")
set(PROJECT_ARCH "arm64")
elseif(CMAKE_SIZEOF_VOID_P MATCHES 8)
set(PROJECT_ARCH "x86_64")
else()
set(PROJECT_ARCH "x86")
endif()
endif()
# Temporarily using Linux and Windows as examples
# Define macros OS_LINUX, OS_WINDOWS, and compilation parameters
if("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux")
set(OS_LINUX 1)
set(OS_POSIX 1)
add_definitions(-DOS_LINUX=1 -DOS_POSIX=1)
if(PROJECT_ARCH STREQUAL "x86_64")
add_compile_options(-m64 -march=x86-64)
add_link_options(-m64)
elseif(PROJECT_ARCH STREQUAL "x86")
add_compile_options(-m32)
add_link_options(-m32)
endif()
elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
set(OS_WINDOWS 1)
add_definitions(-DOS_WINDOWS=1)
endif()
Versioning Techniques
When I was translating the official documentation, there was a section on Adding Version Numbers and Configuration Header Files
, where some configurable information is set via configure_file()
, defined or obtained from the system in CMakeLists.txt
, and then a header file is generated and included in the project.
Windows
project(test VERSION 1.0.0)
if(MSVC)
set(MY_VERSIONINFO_RC "${CMAKE_CURRENT_BINARY_DIR}/version.rc")
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.rc.in"
"${MY_VERSIONINFO_RC}")
endif()
There’s a small detail here: when setting the version number with the project()
command, the following four variables are assigned:
-
<PROJECT-NAME>_VERSION_MAJOR
: Major version number -
<PROJECT-NAME>_VERSION_MINOR
: Minor version number -
<PROJECT-NAME>_VERSION_PATCH
: Patch version number -
<PROJECT-NAME>_VERSION_TWEAK
: Tweak version number
The content of version.rc.in
is as follows, which is the version information for a DLL library under Windows:
1 VERSIONINFO
FILEVERSION ${PROJECT_VERSION_MAJOR}, ${PROJECT_VERSION_MINOR}, ${PROJECT_VERSION_PATCH}
PRODUCTVERSION ${PROJECT_VERSION_MAJOR}, ${PROJECT_VERSION_MINOR}, ${PROJECT_VERSION_PATCH}
FILEFLAGSMASK 0x17L
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
#endif
FILEOS 0x4L
FILETYPE 0x0L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "FileDescription", "${PROJECT_NAME} Binary"
VALUE "FileVersion", "${PROJECT_VERSION_MAJOR}, ${PROJECT_VERSION_MINOR}, ${PROJECT_VERSION_PATCH}"
VALUE "InternalName", "${PROJECT_NAME}.dll"
VALUE "LegalCopyright", "Copyright (C) 2023"
VALUE "OriginalFilename", "${PROJECT_NAME}.dll"
VALUE "ProductName", "${PROJECT_NAME}"
VALUE "ProductVersion", "${PROJECT_VERSION_MAJOR}, ${PROJECT_VERSION_MINOR}, ${PROJECT_VERSION_PATCH}"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
Linux
In Linux, it’s generally about adding a version string to the program, which can be viewed using the strings
command after compiling into a library, similar to the following:
const char *LIB_INFO = "version: 1.0.0";
Alternatively, you can provide a return string like getVersion
as a version interface; essentially, they are quite similar. We can create a VersionDefine.h.in
file:
#pragma once
#define APP_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}"
In CMake, it can be simply written as follows:
set(COMMON_INFO "${CMAKE_CURRENT_SOURCE_DIR}/VersionDefine.h")
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/VersionDefine.h.in"
"${COMMON_INFO}")
The provided interface is also very simple:
#include "VersionDefine.h"
std::string getVersion()
{
return APP_VERSION;
}
Changing the version number is very elegant; you only need to change the project(test VERSION 1.0.0)
line in CMakeLists.txt
.
Output Directory for Artifacts
$<config>
distinguishes between debug and release versions, separating dynamic libraries, static libraries, and executables for easier management and post-processing.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/output/$<CONFIG>/bin)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/output/$<CONFIG>/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/output/$<CONFIG>/lib)
-
CMAKE_ARCHIVE_OUTPUT_DIRECTORY
: Default folder for static libraries; -
CMAKE_LIBRARY_OUTPUT_DIRECTORY
: Default folder for dynamic libraries; -
LIBRARY_OUTPUT_PATH
: Default folder for library files. If a static library is produced andCMAKE_ARCHIVE_OUTPUT_DIRECTORY
is not specified, it will be stored in this directory, and the same applies to dynamic libraries; -
CMAKE_RUNTIME_OUTPUT_DIRECTORY
: Directory for executable software;
Multi-Module Management
For example, if I have two modules A
(dynamic library) and B
(executable program) in my Project
, I would generally organize the project’s structure as follows:
Project
├─ CMakeLists.txt
└─ src
├─ A
│ └─ CMakeLists.txt
├─ B
│ ├─ CMakeLists.txt
│ └─ main.cpp
└─ common
└─ version.h.in
At this point, I will extract the common configuration for A
and B
into the outer CMakeLists.txt
:
# Common Configuration
cmake_minimum_required(VERSION 3.4)
...
# Compilation parameters, options, etc.
...
# Unified export location
...
# Cross-platform macros, etc.
...
# Targets
add_subdirectory(A)
add_subdirectory(B)
To ensure that module A
is compiled before module B
, I just need to use the following command in B
‘s CMakeLists.txt
:
add_dependencies(${PROJECT_NAME} A)
This way, when compiling B
, it will ensure that A
is compiled first.