CMake Advanced Techniques

With further progress, there are advanced techniques, this article mainly introduces some of my habits on Windows.

  • Windows GUI applications
  • Windows console applications do not display the console black box
  • Generating .sln files on Windows
  • Custom CMake variables
  • Super useful custom operations
    • Built-in commands (copy, delete, etc.)
    • Execute commands
    • Generate files
  • make install

Windows GUI Applications

add_executable(${PROJECT_NAME} WIN32
    ${SRC_FILES}
)
  • WIN32 parameter tells CMake to configure the application as a Windows GUI application.
  • MACOSX_BUNDLE parameter configures a macOS bundle application.
  • EXCLUDE_FROM_ALL parameter means that the program will not be compiled by default. If you want to compile it, you need to compile it manually, for example, make test specifies the compilation name as test.
  • If no options are specified, it is configured as a console application.

Windows Console Applications Do Not Display the Console Black Box

Sometimes you want the application to run in the background without any pop-up windows. You can set it as follows on Windows:

set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /ENTRY:mainCRTStartup")

Or like this:

set_target_properties(${PROJECT_NAME} PROPERTIES
    LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup"
)

Generating .sln Files on Windows

In Windows development, IDE is likely to be closely related to VS, so I usually use scripts to generate .sln files, and then open the generated .sln file directly in VS for project development. Here is a script example:

cmake -S . ^
-B build_win ^
-G "Visual Studio 16" ^
-A Win32 ^
-DCMAKE_GENERATOR_TOOLSET=v142 ^
-DCMAKE_SYSTEM_VERSION=8.1 ^
-DPROJECT_ARCH=x86 ^
-DBUILD_DEMO=ON
  • **-S .**: Specifies the source code directory. . means the current directory, which is the directory where the script is located.
  • -B build_win: Specifies the build directory. build_win indicates the path of the build directory.
  • **-G "Visual Studio 16"**: Specifies the generator. "Visual Studio 16" indicates using Visual Studio 2019 as the generator.
  • **-A Win32**: Specifies the architecture of the target platform. Win32 indicates a 32-bit Windows platform.
  • **-DCMAKE_GENERATOR_TOOLSET=v142**: This parameter is used to specify the version of the toolset used for building.
  • **-DCMAKE_SYSTEM_VERSION=8.1**: This parameter is used to specify the system version of the target platform.
  • **-DPROJECT_ARCH=x86**: Defines a CMake variable named PROJECT_ARCH and sets its value to x86. This was mentioned in the previous cross-platform article.
  • **-DBUILD_DEMO=ON**: Defines a CMake variable named BUILD_DEMO and sets its value to ON.

Additionally:

  • If the -G parameter is not specified, CMake will try to select an appropriate generator based on the current system’s default configuration. Usually, it will choose the default generator that matches the operating system and system version.
  • If the -DCMAKE_GENERATOR_TOOLSET parameter is not specified, CMake will use the default generator toolset. This means that the default toolset associated with the selected generator and the build target platform will be used.
  • If the -DCMAKE_SYSTEM_VERSION parameter is not specified, CMake will use the default system version. This may cause issues when using certain specific system features or APIs because CMake cannot distinguish the exact version of the target system.

If these parameters are omitted, CMake will build based on the environment’s default configuration, which may lead to different behaviors and results in different environments. Therefore, to ensure consistency and reproducibility of the build, it is best to explicitly specify these parameters according to the required configuration.

In actual projects, I prefer to use scripts to directly fix the compilation parameters, as project compilation parameters are generally fixed, which can be done through several scripts to make it easier and less error-prone.

Custom CMake Variables

Following the previous script, let’s look at how to pass parameters to CMake, which is also used quite frequently in projects.

# Switch ON/OFF type 
option(BUILD_DEMO "Build the demo" OFF)

# Direct if statement
if (BUILD_DEMO)
  add_subdirectory(example)
endif()

# String type
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()

Super Useful Custom Operations

As long as you want to achieve build automation through CMake, I do not allow anyone to not know the add_custom_command command, which is mainly used to add custom build commands to the project. The most commonly used syntax is as follows; first, look at the official description:

add_custom_command(TARGET target_name
                   [PRE_BUILD | PRE_LINK | POST_BUILD]
                   COMMAND command...
                   [ARGS arguments...]
                   [WORKING_DIRECTORY dir]
                   [COMMENT comment...]
                   [DEPENDEES dependees...]
                   [BYPRODUCTS files...]
                   [IMPLICIT_DEPENDS language depend_list...]
                   [COMMAND_EXPAND_LISTS]
                   [VERBATIM]
                   [APPEND]
                   [USES_TERMINAL]
                   [OUTPUT output_files...])

Here are some common parameters and options explained:

  • TARGET target_name: Specifies the target (executable, library, etc.) that needs to execute the custom command.
  • PRE_BUILD | PRE_LINK | POST_BUILD: Specifies when the custom command is executed: during pre-build, pre-link, or post-build.
  • COMMAND command...: Specifies the command to be executed. It can be a shell command, external tool, or script.
  • ARGS arguments...: Specifies the arguments for the command.
  • WORKING_DIRECTORY dir: Specifies the working directory for the command execution.
  • COMMENT comment...: Adds a comment to the custom command.
  • DEPENDEES dependees...: Specifies the dependent targets; the custom command will be re-executed when the dependent targets change.
  • BYPRODUCTS files...: Specifies the files generated by the custom command.
  • IMPLICIT_DEPENDS language depend_list...: Specifies implicit dependencies, which are usually generated by the generator.
  • COMMAND_EXPAND_LISTS: Expands variable references in the parameter list.
  • VERBATIM: Executes the command as is, without any escaping.
  • APPEND: Appends the custom command to the existing commands of the target.
  • USES_TERMINAL: Indicates that the command requires terminal support to view the output.
  • OUTPUT output_files...: Specifies the output files generated by the custom command.

Using add_custom_command allows you to flexibly add custom commands during the build process, such as executing scripts, generating files, copying files, etc. These commands can be combined with CMake’s target and dependency mechanisms to achieve more advanced build automation.

Let me give some frequently used command examples:

Built-in Commands (Copy, Delete, etc.)

Copy the output of module A to the output path of the current module:

add_custom_command(TARGET ${PROJECT_NAME}
    PRE_BUILD

    COMMAND ${CMAKE_COMMAND} -E copy_directory
    $<TARGET_FILE_DIR:A>
    $<TARGET_FILE_DIR:${PROJECT_NAME}>
)

CMAKE_COMMAND -E can carry multiple built-in commands. Here are some commonly used built-in commands and their explanations:

  • copy: Copies files or directories. For example: copy <source> <destination>
  • remove: Deletes files or directories. For example: remove <file>
  • make_directory: Creates a directory. For example: make_directory <directory>
  • remove_directory: Deletes a directory. For example: remove_directory <directory>
  • rename: Renames files or directories. For example: rename <oldname> <newname>
  • echo: Outputs a text message. For example: echo <message>
  • ……

It is recommended to check the CMake official website for detailed supported parameters and the minimum required CMake version: https://cmake.org/cmake/help/latest/manual/cmake.1.html#cmdoption-cmake-E

There are also platform-specific commands, such as write_regv to write to the registry on Windows.

Execute Commands

On Linux, it may be simpler to directly call commands, as follows:

add_custom_command(TARGET ${PROJECT_NAME}
    POST_BUILD

    COMMAND mkdir -p
      "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/config/"
)

On Windows, this is usually avoided because executing commands may have issues with the path /.

Generate Files

This section is not used much, here is a simple example:

add_custom_command(
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/output.txt
    COMMAND echo "Hello, CMake!" &gt; output.txt
    COMMENT "Generating output.txt"
    VERBATIM
)

# Import all files
add_executable(${PROJECT_NAME}
    ${SRC_FILES}
    output.txt
)

One thing to note is that the generated file must be used, otherwise add_custom_command will not execute.

make install

The make install command is commonly used in build systems to install the generated files to a specified directory. By default, this command will not execute automatically unless explicitly called.

In CMake, you can use the install command to define installation rules. This command instructs CMake to install specific files, directories, or targets during the build process. For example, you can install generated binaries, library files, header files, configuration files, etc.

After using the install command, run make install (on Unix systems) or cmake --build . --target install (to install directly after building) or cmake -DBUILD_TYPE=Debug -P build/cmake_install.cmake (BUILD_TYPE can be specified according to actual needs, here only install) (on Windows systems) to execute the installation action. This will copy the generated files to the specified installation directory according to the defined rules.

Here’s a simple example:

# Copy products to the product name folder in the project root directory
install(TARGETS ${PROJECT_NAME} DESTINATION ${PROJECT_ROOT_PATH}/${PROJECT_NAME})

# Copy the config folder in the project root directory to the product name folder in the project root directory
install(DIRECTORY "${PROJECT_ROOT_PATH}/config/" DESTINATION ${PROJECT_ROOT_PATH}/${PROJECT_NAME})

# Copy the README.md file in the project root directory to the product name folder in the project root directory
install(FILES "${PROJECT_ROOT_PATH}/README.md" DESTINATION ${PROJECT_ROOT_PATH}/${PROJECT_NAME})

If the build organization builds the product, actually using add_custom_command is already sufficient~

Leave a Comment