Building CMake Projects – Part 1

Introduction:

This post demonstrates some basic usage of CMake to build a C++ program. The post is made with help of chatGPT AI, with some references to: CMake tutorials, and the book Mastering CMake.

The Concepts section is written as answers to questions about CMake. Most answers are provided by chatGPT, but some are edited and added with some extra materials.

The Implementation section provides CMake files and explanations that fulfills certain build requirements.

Concepts

What is CMake?

CMake is an open-source cross-platform build system that helps automate the process of building software. It uses a CMakeLists.txt file to define the build process and generates native build files (e.g. Makefiles, Visual Studio project files, Xcode project files, etc.) for different platforms and build environments.

CMake provides a unified way to manage the build process for a project that can be compiled and built on multiple platforms, such as Windows, Linux, and macOS. It supports a wide range of programming languages, including C++, C, Java, Python, and many others.

What are basic components in a CMakeLists.txt file?

A CMakeLists.txt file is used to define the build process for a CMake-based project. It typically contains several components that are used to specify the project's source files, dependencies, and build options. Here are some of the basic components that can be included in a CMakeLists.txt file:

  1. cmake_minimum_required(): This component sets the minimum required version of CMake that is needed to build the project.
  2. project(): This component specifies the name of the project and any languages that will be used in the project.
  3. add_executable() or add_library(): These components are used to specify the source files for the project and create an executable or library file.
  4. target_include_directories(): This component specifies any include directories that are needed to build the project.
  5. target_link_libraries(): This component specifies any libraries that are needed to build the project.
  6. set(): This component is used to set CMake variables that can be used to customize the build process.
  7. add_definitions(): This component is used to add compile-time definitions to the project.
  8. add_subdirectory(): This component is used to include subdirectories in the build process.

What are some basic concepts defined by CMake?

CMake is a build system that defines a number of basic concepts that are used to describe the structure of a software project and its build process. Some of the basic concepts defined by CMake include:

  1. Source files: These are the files that contain the source code of the project. Source files can include C++ files, header files, configuration files, and other types of files that are used in the project.
  2. Targets: These are the objects that are built as part of the project. Targets can be executables, libraries, or other types of objects that are used in the project.
  3. Dependencies: These are the relationships between targets and source files. Dependencies describe the relationships between the different components of the project, and they are used to ensure that targets are built in the correct order.
  4. Build types: These are the different types of builds that can be performed, such as debug builds, release builds, or profile builds. Build types determine the optimization level, debugging symbols, and other properties of the built binaries.
  5. Compiler flags: These are the flags that are passed to the compiler when building the project. Compiler flags can be used to specify optimization levels, warning levels, and other properties of the build process.
  6. Install rules: These are the rules that are used to install the built binaries on the target system. Install rules can be used to specify the location of the installed files, the permissions of the installed files, and other properties of the installation process.

These are just a few examples of the basic concepts defined by CMake. By using these concepts to describe the structure of a software project, CMake provides a powerful and flexible tool for building and managing complex software projects.

What is a target in CMake?

In CMake, targets are a fundamental concept that represent a logical build artifact, such as an executable, a library, or a test. Targets are defined using the add_executable(), add_library(), and add_test() commands in the CMakeLists.txt file.

Targets can have properties such as source files, compile options, linker options, dependencies, and installation rules. These properties can be set using various commands and functions in CMake.

Targets can also have dependencies on other targets. For example, an executable target might depend on a library target, which in turn might depend on other library targets. CMake uses these dependencies to generate the build system in the correct order, ensuring that all dependencies are built before the target that depends on them.

Targets can also be used to specify installation rules. For example, you might want to install the executable target to a specific directory, or install the headers and library files associated with a library target to different directories.

Here's an example of how you might define a target in CMake:

# Define a library target
add_library(mylib STATIC mylib.cpp)

# Define an executable target that depends on the library target
add_executable(myexe main.cpp)
target_link_libraries(myexe PRIVATE mylib)

In this example, we define a library target called mylib by calling add_library(). We then define an executable target called myexe that depends on the mylib target by calling add_executable() and target_link_libraries(). This ensures that the mylib target is built before the myexe target.

Targets are a powerful and flexible concept in CMake that allow you to define, configure, and manage build artifacts in a consistent and scalable way.

What is a dependency in CMake?

In CMake, a dependency refers to the relationship between different components of a software project, such as source files, libraries, and executables. Dependencies define the order in which these components are built, and ensure that each component has access to the necessary resources and tools to build successfully.

Dependencies are typically defined using the target_link_libraries() command in the CMakeLists.txt file. This command specifies the target that depends on one or more other targets, and the other targets that it depends on. For example, the following command specifies that the my_executable target depends on the my_library target:

target_link_libraries(my_executable my_library)

This tells CMake that my_executable needs to be built after my_library, and that it depends on the objects and resources provided by my_library.

Dependencies can also be specified using the add_dependencies() command, which allows you to specify dependencies between targets that are not linked together. This is useful when you have targets that are built separately and need to be completed before other targets can be built.

What are build types in CMake?

In CMake, build types are different configurations in which a project can be built, each with its own set of compiler flags, optimizations, and other build options. The most common build types are:

  1. Debug: A build type with debugging symbols and no optimization, intended for development and debugging purposes.
  2. Release: A build type with optimization and no debugging symbols, intended for production use.
  3. RelWithDebInfo: A build type with optimization and debugging symbols, intended for profiling and debugging optimized code.
  4. MinSizeRel: A build type with optimization for minimum binary size, useful for embedded systems or applications with limited storage space.

Build types can be specified using the CMAKE_BUILD_TYPE variable in the CMakeLists.txt file, or by passing the -DCMAKE_BUILD_TYPE=<type> option to the CMake command line. For example, to specify the Debug build type, you can add the following line to your CMakeLists.txt file:

set(CMAKE_BUILD_TYPE Debug)

By specifying the build type, you can control the behavior of the build process, such as which compiler flags are used, whether or not debugging symbols are included, and whether or not optimizations are applied

What are C++ compiler flags that can be set in CMake?

In CMake, compiler flags are options passed to the compiler during the build process to control its behavior. Some common compiler flags that can be set in CMake include:

  • -Wall: Enables all warning messages from the compiler.
  • -Werror: Treats all warnings as errors.
  • -std=<standard>: Specifies the language standard to use, such as c++11 or c11.
  • -O<level>: Enables optimization at the specified level, such as -O1 or -O3.
  • -g: Generates debugging symbols for use with a debugger.
  • -D<name>=<value>: Defines a preprocessor macro with the specified value.
  • -I<path>: Adds the specified directory to the include search path.
  • -L<path>: Adds the specified directory to the library search path.
  • -l<library>: Links against the specified library.

Compiler flags can be set in CMake using the add_compile_options() command, which adds the specified options to the compiler command line for all targets in the current directory and its subdirectories. For example, to enable all warnings and treat them as errors, you can add the following lines to your CMakeLists.txt file:

add_compile_options(-Wall -Werror)

You can also set compiler flags for a specific target using the target_compile_options() command. For example, to enable optimization at level 2 for a target named my_target, you can add the following line:

target_compile_options(my_target PRIVATE -O2)

What are installation rules that can be set in CMake?

In CMake, installation rules specify how the built targets and files should be installed on the target system. Some common installation rules that can be set in CMake include:

  • install(TARGETS ...): Installs the specified targets to the specified directory on the target system. For example, the following command installs the my_executable target to the bin directory:
install(TARGETS my_executable DESTINATION bin)
  • install(FILES ...): Installs the specified files to the specified directory on the target system. For example, the following command installs the my_header.h file to the include directory:
install(FILES my_header.h DESTINATION include)
  • install(DIRECTORY ...): Installs the specified directories to the specified directory on the target system. For example, the following command installs the my_data directory and its contents to the share directory:
install(DIRECTORY my_data DESTINATION share)
  • install(PROGRAMS ...): Installs the specified executable scripts to the specified directory on the target system. For example, the following command installs the my_script.py script to the bin directory:
install(PROGRAMS my_script.py DESTINATION bin)
  • install(SCRIPT ...): Executes the specified CMake script during the installation process. For example, the following command executes the my_script.cmake script during installation:
install(SCRIPT my_script.cmake)
  • install(CODE ...): Executes the specified CMake code during the installation process. For example, the following command executes the message() command during installation:
install(CODE "message(\"Installing my project\")")

What are most used CMake commands?

There are many CMake commands available, but some of the most commonly used ones include:

  1. project(): Sets the name and version of the project.
  2. add_executable(): Defines a new executable target from the specified source files.
  3. add_library(): Defines a new library target from the specified source files.
  4. target_link_libraries(): Links a target against one or more libraries.
  5. include_directories(): Adds directories to the include path of the project.
  6. set(): Sets a CMake variable to the specified value.
  7. if(), elseif(), else(), and endif(): Allows conditional statements in CMake scripts.
  8. foreach(), while(), and endforeach(): Provides looping constructs in CMake scripts.
  9. install(): Specifies installation rules for the built targets and files.
  10. find_package(): Locates and sets up a third-party package or library for use in the project.
  11. add_definitions(): Adds preprocessor definitions to the compiler command line.
  12. add_subdirectory(): Adds a subdirectory to the build process.
  13. add_custom_command(): Adds a custom command to the build process.
  14. add_test(): Adds a test to the project.
  15. enable_testing(): Enables the testing subsystem for the project.

These commands can be used to define the project, create targets, set up dependencies, specify build settings, and more. CMake provides a powerful and flexible way to manage the build process of a project, and the above commands are just a few examples of what can be done with CMake.

The basic structures of CMake commands are:

<COMMAND_NAME>([arguments] ...)

The COMMAND_NAME is the name of the CMake command, and the arguments are zero or more arguments passed to the command.

The arguments can take different forms depending on the command, but they are generally key-value pairs or lists. For example, some commands use a NAME key to specify a name for a target or file, while others use a DESTINATION key to specify an installation destination.

In addition to the basic structure, some CMake commands have optional arguments or modifiers. Optional arguments can be specified using square brackets, like [OPTIONAL], while modifiers can be specified using parentheses, like (RESULT result_var). These optional arguments and modifiers are specific to each command and are documented in the CMake documentation.

Here's an example of a CMake command that creates a new executable target:

add_executable(my_executable main.cpp helper.cpp)

In this example, add_executable is the command name, and my_executable is the name of the new target. The main.cpp and helper.cpp files are the source files used to build the target.

What are common CMake variables?

There are many CMake variables available, but here are some of the most common ones:

  1. CMAKE_SOURCE_DIR: The root directory of the source code.
  2. CMAKE_BINARY_DIR: The root directory of the build files.
  3. CMAKE_CURRENT_SOURCE_DIR: The directory where the currently processed CMakeLists.txt file is located.
  4. CMAKE_CURRENT_BINARY_DIR: The directory where the binary files for the currently processed CMakeLists.txt file are located.
  5. CMAKE_INSTALL_PREFIX: The installation directory for the project.
  6. CMAKE_C_COMPILER: The C compiler to use for building the project.
  7. CMAKE_CXX_COMPILER: The C++ compiler to use for building the project.
  8. CMAKE_BUILD_TYPE: The build type, such as "Debug" or "Release".
  9. CMAKE_MODULE_PATH: A list of directories where CMake should look for additional modules.
  10. CMAKE_PREFIX_PATH: A list of directories where CMake should look for third-party packages and libraries.
  11. CMAKE_INCLUDE_PATH: A list of directories where CMake should look for header files.
  12. CMAKE_LIBRARY_PATH: A list of directories where CMake should look for library files.
  13. CMAKE_BUILD_RPATH: A list of directories where CMake should look for runtime libraries.

These variables can be used to customize the build process, set up dependencies, specify build settings, and more. Each variable has a default value, but they can be overridden by the user through various means, such as command-line options or environment variables.

How can a variable be set in CMake?

CMake variables can be set in several ways. Here are some methods:

  • Set() command: The most common way to set a CMake variable is to use the set() command. For example, to set the CMAKE_BUILD_TYPE variable to Release, you can use the following command:
set(CMAKE_BUILD_TYPE "Release")
  • Command line: You can also set CMake variables using command-line arguments when invoking CMake. For example, to set the CMAKE_INSTALL_PREFIX variable to "/usr/local" using the command line, you can use the following command:
cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
  • Cache entry: CMake also provides a cache mechanism that can be used to set and store variable values between runs. You can use the set() command with the CACHE option to set a variable in the cache. For example, to set the CMAKE_BUILD_TYPE variable in the cache, you can use the following command:
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type")
  • Environment variable: You can also set CMake variables using environment variables on Linux. For example, to set the CMAKE_BUILD_TYPE variable to Release using an environment variable, you can use the following command:
export CMAKE_BUILD_TYPE=Release

What is a list in CMake?

CMake provides a LIST command that allows you to manipulate list variables. The LIST command has several subcommands that perform various operations on list variables. Some of the most commonly used subcommands are:

  • APPEND: Appends one or more values to the end of a list variable.
  • INSERT: Inserts one or more values at a specified position in a list variable.
  • REMOVE_ITEM: Removes all occurrences of a specified value from a list variable.
  • LENGTH: Returns the number of elements in a list variable.
  • GET: Retrieves the value at a specified position in a list variable.
  • SORT: Sorts the elements of a list variable.

Here is an example that demonstrates the use of some of these subcommands:

SET(my_list "apple" "banana" "cherry")
LIST(APPEND my_list "date")
LIST(INSERT my_list 2 "orange")
LIST(REMOVE_ITEM my_list "banana")
SET(num_elements ${LIST_LENGTH my_list})
SET(first_element ${LIST_GET my_list 0})
LIST(SORT my_list)

Implementation:

Basic and topmost CMakeLists.txt to configure build

The CMake Tutorial already does a good job in explaining basic 7 steps to build a CMake project. Those are:

  1. Add basic information (CMake version, project name, compiler info, C++ standard)
  2. Add Library and Dependency (using commands such as library_target_links)
  3. Add Usage requirements for library (using list and APPEND to add more library)
  4. Add Installation Rules. (using install, TARGET, DESTINATION to build library)
  5. Add Testing support
  6. Add System Introspection (using if else command to add further options for different build modes)
  7. Build an Installer. (using CPack to make installation package that supports binary installations and package management features)

Normally, we would have multiple CMakelists.txt files to in parent and sub directories to define build for different modules in a big project. For that reason, not every config of the above 7 steps is written into a single file.

The following is a top-most CMakeLists.txt program that I usually use to define build. The file should be place at the top directory of the project.

CMakeLists.txt

cmake_minimum_required(VERSION 3.12)

project(MYPROJECT LANGUAGES CXX)

#######################################
# Compiler option
set(CXX_STANDARD 14)
set(CXX_STANDARD_REQUIRED OFF)
set(CMAKE_CXX_FLAGS "-std=c++14 -m32")

#######################################
# Build mode as Dev or Release

#######################################
# Cache option
# Only cache on dev build server, not release build server
# Controlled using CCACHE_DIR environment variable
if(DEFINED ENV{CCACHE_DIR})
  find_program(CCACHE_PROGRAM ccache)
  if(CCACHE_PROGRAM)
    message(STATUS "Using ccache: ${CCACHE_PROGRAM}")
    set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
  endif()
endif()

#######################################
# CMake options

set(BUILD_SHARED_LIBS ON)

list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)

list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/cpack)

set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/libraries)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/programs)

set(MY_PROJECT_SUPPLEMENTARY_LIBS_DIR /usr/local/lib/)

set(MY_PROJECT_LIBS_DIR $ORIGIN/../libraries)

set(CMAKE_BUILD_RPATH
  ${MY_PROJECT_LIBS_DIR})

# Do not strip RPATH from executables when installing
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

# Project configuration file
include(MyProjectConfig)

# Directory which contains all other CMakeLists.txt files
add_subdirectory(source)

#######################################
# Packaging

include(CPackConfig)

Leave a Reply

Your email address will not be published. Required fields are marked *