Advanced CMake Techniques for Project Management

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
    – Linux
  • 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/$&lt;CONFIG&gt;/bin)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/output/$&lt;CONFIG&gt;/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/output/$&lt;CONFIG&gt;/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 and CMAKE_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.

Leave a Comment