Preface

Good software handles change smoothly and makes change easy.

There are various reasons you may need to update your codebase: a customer requests an unexpected feature, an outdated dependency requires replacement, or you want to support new platforms like macOS or the increasingly popular Windows on ARM.

As your project evolves, it grows larger and more complex. You can manage this complexity by breaking the project into smaller, manageable parts. At the core of this process is your build system. A good build system not only supports growth but also encourages best software development practices.

For C/C++ developers, CMake is an excellent build system. With proper use of CMake, you can automate tasks like cross-platform development, testing, documentation, and packaging for installation. Additionally, CMake is supported by all major IDEs.

While this isn’t a CMake tutorial, you can still learn a lot about it here. I will walk you through different parts of the SeeMake template, explaining what each CMake instruction is intended to achieve. This way, if you choose to use SeeMake, you’ll understand how to adapt it for your own projects and needs.

Before continuing, ensure that you’ve completed the installation process as explained here.

For more information on CMake, I recommend reading Modern CMake for C++ by Rafał Świdziński, which is the foundation of this template.

Directory Layout

If you check out the initial commit, you’ll see the following folder structure for this template:

git checkout $(git rev-list --max-parents=0 HEAD)
tree .
# .
# ├── benchmark
# │   ├── CMakeLists.txt
# │   ├── libsee
# │   │   └── CMakeLists.txt
# │   └── see
# │       └── CMakeLists.txt
# ├── cmake
# │   └── NoInSourceBuilds.cmake
# ├── CMakeLists.txt
# ├── LICENSE.txt
# ├── src
# │   ├── CMakeLists.txt
# │   ├── libsee
# │   │   └── CMakeLists.txt
# │   └── see
# │       └── CMakeLists.txt
# └── test
#     ├── CMakeLists.txt
#     ├── libsee
#     │   └── CMakeLists.txt
#     └── see
#         └── CMakeLists.txt
#
# 11 directories, 12 files

Here’s a breakdown of each directory and its purpose:

  • root: Contains the main CMakeLists.txt file, which serves as the entry point for CMake. It references other CMakeLists.txt files in the subdirectories. Later commits add a CMakePresets.json file, simplifying the build process with presets for different configurations like debug and release. You can also create platform-specific presets, each customized to use the appropriate compilers and settings.

  • src: This is where your application’s source code goes. You can organize your code into smaller libraries and one executable, with each part placed in a separate subdirectory under src.

  • test: Automated tests are stored here, and typically, the test structure mirrors that of src. This folder contains demo code for both Google Test and Boost.Test.

  • benchmark: Similar to how you monitor your code’s correctness in the test directory, you can track its performance in benchmark. This folder includes a demo using the Google Benchmark library.

  • cmake: All supporting CMake files are placed here. These files include instructions for automating tasks like tests, coverage reports, installation, and packaging, which we will explore shortly.

You can use a CMake command to create a dependency graph for your project:

git checkout main
mkdir tmp
cd tmp/
cmake -S .. --preset linux-default-debug --graphviz=dependencies.dot
dot -Tpng -o dependencies.png dependencies.dot

The command cmake -S .. --preset linux-default-debug --graphviz=dependencies.dot generates a dependency graph in Graphviz format.

  • -S ..: Specifies the source directory, which is the parent directory in this case.
  • --preset linux-default-debug: Uses the predefined CMake preset for Linux in debug mode.
  • --graphviz=dependencies.dot: Outputs the dependency graph to a file named dependencies.dot.

After running these commands, you should have a visual representation of your project’s dependencies.

Dependency Graph

CMake Commands

After cloning the SeeMake template, the fastest way to build the library, executable, tests, and benchmarks is by running a workflow. These workflows are defined in the CMakePresets.json file and can be listed with the following command:

cmake --workflow --list-presets 
# Available workflow presets:
#
#   "linux-default-debug"     - Linux Debug
#   "linux-default-release"   - Linux Release
#   "windows-default-debug"   - Windows Debug
#   "windows-default-release" - Windows Release
#   "windows-x86-debug"       - Windows x86 Debug
#   "windows-x86-release"     - Windows x86 Release
#   "windows-clang-debug"     - Windows Clang Debug
#   "windows-clang-release"   - Windows Clang Release
#   "mac-default-debug"       - Mac Debug
#   "mac-default-release"     - Mac Release

Once you’ve chosen the appropriate workflow, run the following command:

cmake --workflow --preset linux-default-release

This command will build all artifacts, run the tests, and create an installer for your software automatically.

Automatic generation of documentation, coverage reports, and dynamic checks are defined as separate targets in CMake. For example, you can generate the documentation by running the following command:

cmake --build --preset linux-default-release --target doxygen-libsee_static

To view the documentation, either serve the documentation folder using a Python server or open the index.html file directly in your browser:

cd ../SeeMake-build-linux-default-release/doxygen-libsee_static/
python3 -m http.server 8172
# Visit localhost:8172 in your browser

To list all available targets, run the following command:

cmake --build --preset linux-default-release --target help
# The following are some of the valid targets for this Makefile:
# ... all (the default if no target is provided)
# ... clean
# ... coverage-google_test_libsee
# ... doxygen-libsee_static
# ... doxygen-terminal_see_static
# ... memcheck-google_test_libsee
# ... appsee
# ... google_bench_libsee
# ... google_bench_see
# ... google_test_libsee
# ... libsee_obj
# ... libsee_shared
# ... libsee_static
# ... terminal_see_static

To clean, configure, and build the project, run the following commands:

# Clean the project
cmake --build --preset linux-default-release --target clean

# Configure the project
cmake --preset linux-default-release

# Build the project
cmake --build --preset linux-default-release

To install the header files, static and shared libraries, and your executable, run the following:

cmake --install ../SeeMake-build-linux-default-release/
tree ../SeeMake-install-linux-default-release/
# ../SeeMake-install-linux-default-release/
# ├── bin
# │   └── appsee
# ├── include
# │   └── libsee
# │       └── see_model.h
# └── lib
#     ├── cmake
#     │   └── libsee
#     │       ├── libsee-config.cmake
#     │       ├── libsee-config-version.cmake
#     │       ├── libsee-targets.cmake
#     │       └── libsee-targets-release.cmake
#     ├── libsee.a
#     └── libsee.so

By default, CMake installs artifacts in the root folder of your system. However, in this case, the CMakePresets.json file defines the installation location to be next to the build directory. If you want to install the artifacts in the root directory on Linux, run the installation command as root and add the --prefix /usr flag:

sudo cmake --install ../SeeMake-build-linux-default-release/ --prefix /usr

The following commands are used for packaging:

cpack --list-presets
# Available package presets:
#
#   "linux-deb"            - Linux DEB package
#   "windows-default-nsis" - Windows NSIS package
#   "windows-x86-nsis"     - Windows NSIS package for x86
#   "windows-clang-nsis"   - Windows NSIS package for Clang

To create a package, run the command:

cpack --preset linux-deb
# CPack: - package: /home/mohammad/repos/SeeMake-build-linux-default-release/package-linux-deb/SeeMake-0.0.0-Linux.deb generated.

CMakePresets.json File

One of the key features that makes CMake portable and easy to use is Presets. Many IDEs, including VSCode, offer excellent support for them. Presets allow you to define multiple configurations for tasks such as building, testing, and packaging. You can also combine these into workflows for different environments.

In this section, I’ll walk you through some of the presets defined in this template, including debug and release builds for Linux, macOS, and Windows.

Configure Presets

Before building your project, CMake sets up the build environment by generating the appropriate build instructions, such as Makefiles or Visual Studio solutions. The type of instructions generated depends on the Generator you choose. Two generators used in this template are Ninja and Visual Studio.

Below is an example of a configure preset for Linux:

{
	"name": "linux-default-release",
	"displayName": "Linux Release",
	"description": "Sets compilers, build and install directory, release build type",
	"binaryDir": "${sourceDir}/../${sourceDirName}-build-${presetName}",
	"condition": {
		"type": "equals",
		"lhs": "${hostSystemName}",
		"rhs": "Linux"
	},
	"cacheVariables": {
		"CMAKE_BUILD_TYPE": "Release",
		"CMAKE_CXX_STANDARD": "20",
		"CMAKE_CXX_STANDARD_REQUIRED": "YES",
		"CMAKE_CXX_EXTENSIONS": "OFF",
		"CMAKE_EXPORT_COMPILE_COMMANDS": "YES",
		"CMAKE_INSTALL_PREFIX": "${sourceDir}/../${sourceDirName}-install-${presetName}",
		"DEFAULT_CXX_COMPILE_FLAGS": "-Wextra;-Wall;-Wfloat-equal;-Wundef;-Wpointer-arith;-Wshadow;-Wcast-align;-Wswitch-default;-Wswitch-enum;-Wconversion;-Wpedantic;-Werror",
		"DEFAULT_CXX_OPTIMIZE_FLAG": "-O3"
	}
}

With the help of cacheVariables, you can customize various aspects of CMake, such as compiler flags, installation directories, and the C++ standard. Some cacheVariables used in other configure presets include the path to Cppcheck on Windows, as well as WIN_MSVC and WIN_CLANG to differentiate between the two Windows presets. When WIN_MSVC is defined, dynamic checks are enabled on Windows, whereas with WIN_CLANG, coverage reports are enabled. More details on that will follow later.

In this template, In-Source Builds are disabled. Each preset specifies where the project should be built, typically in a directory next to the template, named ${sourceDirName}-build-${presetName}.

Build Presets

Each build preset is linked to a specific configuration preset. A build preset can either build all targets or be assigned to a specific target. Below are two examples of build presets:

[
{
	"name": "windows-clang-debug",
	"jobs": 10,
	"displayName": "Windows Clang Debug",
	"description": "debug build type",
	"configurePreset": "windows-clang-debug",
	"configuration": "Debug"
},
{
	"name": "windows-clang-debug-doxygen",
	"hidden": true,
	"inherits": "windows-clang-debug",
	"targets": ["doxygen-libsee_static", "doxygen-terminal_see_static"]
}
]

The windows-clang-debug-doxygen preset inherits its properties from the windows-clang-debug build preset and is also marked as hidden. A hidden preset cannot be used directly in UI tools or from the command line, but it can still be utilized as a step in workflows.

Test Presets

The test presets in this template are quite simple:

{
	"name": "mac-test-debug",
	"displayName": "Mac Test Debug",
	"description": "Tests the debug build type",
	"configurePreset": "mac-default-debug"
}

You can further customize these presets for more granularity, such as running a specific subset of tests.

Package Presets

Package presets run the CPack program, which in turn calls other programs to create installers for your project. You can specify the programs CPack uses in the generators list. Each generator requires its own configuration, with some options being mandatory and most optional. A full list of available generators and their configuration options can be found here.

[
{
	"name": "linux-deb",
	"description": "linux deb packaging",
	"displayName": "Linux DEB",
	"configurePreset": "linux-default-release",
	"generators": ["DEB"],
	"configurations": ["Release"],
	"vendorName": "Mohammad Rahimi",
	"packageDirectory": "package-linux-deb",
	"environment": {
		"CPACK_DEBIAN_PACKAGE_NAME": "SeeMake",
		"CPACK_DEBIAN_FILE_NAME": "DEB-DEFAULT"
	}
},
{
	"name": "windows-nsis",
	"hidden": true,
	"generators": ["NSIS"],
	"configurations": ["Release"],
	"vendorName": "Mohammad Rahimi",
	"packageDirectory": "package-windows-nsis",
	"environment": {
		"CPACK_NSIS_DISPLAY_NAME": "SeeMake",
		"CPACK_NSIS_PACKAGE_NAME": "SeeMake",
		"CPACK_NSIS_URL_INFO_ABOUT": "https://github.com/MhmRhm"
	}
}
]

Workflow Presets

With all the other presets in place, a workflow can combine them to automate your pipeline:

"workflowPresets": [
	{
		"name": "linux-default-debug",
		"displayName": "Linux Debug",
		"steps": [
			{
				"type": "configure",
				"name": "linux-default-debug"
			},
			{
				"type": "build",
				"name": "linux-default-debug"
			},
			{
				"type": "test",
				"name": "linux-test-debug"
			},
			{
				"type": "build",
				"name": "linux-default-debug-coverage"
			},
			{
				"type": "build",
				"name": "linux-default-debug-memcheck"
			},
			{
				"type": "build",
				"name": "linux-default-debug-doxygen"
			}
		]
	}
]

CMakeLists.txt Files

The CMakeLists.txt file is the first file CMake looks for in a directory. From there, you can include other directories and files in the project.

Anything included in the root CMakeLists.txt gets built. The main role of the CMakeLists.txt file in the root directory of the template is to include other directories and make helper CMake functions available for the targets. Most of the actions take place in other CMake files. However, if you want to disable building tests or benchmarks entirely, just comment out the relevant add_subdirectory lines in this file:

add_subdirectory(src bin)
# add_subdirectory(test)
# add_subdirectory(benchmark)

Let’s take a look at the CMakeLists.txt files that define the executable and library targets. In ./src/libsee/, we have defined the library targets.

add_library(libsee_obj OBJECT
	see_model.cpp
)
add_library(libsee_shared SHARED)
add_library(libsee_static STATIC)

The add_library instruction creates a library target. Both shared and static libraries are built from the object library:

target_link_libraries(libsee_shared libsee_obj)
target_link_libraries(libsee_static libsee_obj)

CMake’s Generator Expressions are used for adding header files to a target:

target_include_directories(libsee_obj
	PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
	PUBLIC "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)

When building the project, the compiler looks for header files in the source directory. When you install the libraries, others linking against them should find the header files in the installation folder. Using these Generator Expressions ensures that the CMake Config file generated contains the correct paths. With the help of the Config file, others can use find_package(libsee) to integrate your work into their project. See Relocatable Packages and First Step.

To ensure your installation step moves the header files to the include directory, you should add them to the PUBLIC_HEADER property and enable POSITION_INDEPENDENT_CODE, so the virtual memory addresses are applied correctly when the library is loaded into memory.

set_target_properties(libsee_obj PROPERTIES
	PUBLIC_HEADER src/libsee/include/libsee/see_model.h
	POSITION_INDEPENDENT_CODE 1
)

Next, let’s look at the CMakeLists.txt in ./src/see. This file defines two targets. One of them contains only the main() function, which is called bootstrapping. By placing everything our application needs to run in a separate file (in this case, terminal_see.cpp) and reducing main() to a few function calls or object creations, we can also test the code responsible for initializing the application.

add_library(terminal_see_static STATIC
	terminal_see.cpp
)
target_link_libraries(terminal_see_static
	PRIVATE precompiled
	PUBLIC libsee_static
)

add_executable(appsee main.cpp)
target_link_libraries(appsee
	PRIVATE precompiled
	PRIVATE terminal_see_static
)

When terminal_see_static links to libsee_static, all the header files in libsee_obj become available to terminal_see_static. This is because of the following instruction:

target_include_directories(libsee_obj
	PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
	PUBLIC "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)

By using PUBLIC, this target and anything that links to it inherit the property. This is known as Transitive Usage Requirements in CMake.

Other CMakeLists.txt files are straightforward and simply define test and benchmarking targets, linking them to the appropriate targets.

.cmake Files

The template provides various features through the .cmake files located under ./cmake. Let’s review them one by one:

NoInSourceBuilds.cmake

CMake can act as a cross-platform scripting language. In this script, using the file and string commands, in-source builds are disabled.

file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/" WhereBinIs)
file(TO_CMAKE_PATH "${PROJECT_SOURCE_DIR}/" WhereSrcIs)
string(FIND "${WhereBinIs}/" "${WhereSrcIs}/" FoundInSource)

If the source path is found as a substring of the binary path, FoundInSource will be set to the position of the substring. Otherwise, it is set to -1, ensuring in-source builds don’t occur.

Precompiling.cmake

To speed up the build process, you can precompile commonly used header files and link your targets to the precompiled headers. For example, if your executable frequently uses <vector> and <string>, you can precompile these headers as follows:

add_executable(your_exe main.cpp)
target_precompile_headers(your_exe PRIVATE
	<vector>
	<string>
)

In this template, an Interface Library is added to propagate the target_precompile_headers property to other targets using INTERFACE Usage Requirement.

include_guard(GLOBAL)

add_library(precompiled INTERFACE)

target_precompile_headers(precompiled INTERFACE
	<string>
	<string_view>
	<format>
)

The include_guard(GLOBAL) ensures this file is only included once. A second call to include(Precompiling.cmake) won’t redefine the precompiled target.

EnableIPOSupport.cmake

This functionality checks whether interprocedural optimization (IPO) is supported by the compiler, and if it is, it enables it. Interprocedural optimization allows the compiler to optimize across different functions or translation units, improving performance by analyzing code beyond function boundaries.

include(CheckIPOSupported)
check_ipo_supported(RESULT result OUTPUT output)
if(result)
	set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
else()
	message(WARNING "IPO is not supported: ${output}")
endif()

BuildInfo.cmake

This .cmake file, along with buildinfo.h.in, allows Git information to be embedded into your built artifacts. For instance, you can display the current Git tag as a version number on your application’s “About” page.

Using CMake’s execute_process, string, and configure_file commands, Git information from ${PROJECT_SOURCE_DIR} is extracted and inserted into the buildinfo.h.in file. The buildinfo.h.in file acts as a template and is transformed into buildinfo.h at build time. To add this information to your targets, you can use the BuildInfo function:

function(BuildInfo target)
	target_include_directories(${target} PRIVATE ${DESTINATION})
endfunction()

Cppcheck.cmake

Cppcheck is a static analysis tool for C/C++ code. It provides unique code analysis to detect bugs and focuses on detecting undefined behaviour and dangerous coding constructs. CMake has a native support for Cppcheck:

find_program(CPPCHECK_PATH cppcheck REQUIRED PATHS "${CPPCHECK_INSTALL_DIR}")
set_target_properties(${target}
	PROPERTIES CXX_CPPCHECK
	"${CPPCHECK_PATH};--enable=warning;--error-exitcode=10"
)

The find_program instruction searches for the Cppcheck executable in default system paths and within ${CPPCHECK_INSTALL_DIR}. Once the program is found, it configures the Cppcheck command on a target using set_target_properties. The ${CPPCHECK_INSTALL_DIR} is defined in the configuration presets.

Doxygen.cmake

Doxygen is a widely-used documentation generator tool in software development. It automates the generation of documentation from source code comments, parsing information about classes, functions, and variables to produce output in formats like HTML and PDF. CMake also has a native support for Doxygen.

function(Doxygen target input)
	set(NAME "doxygen-${target}")
	# ...
	doxygen_add_docs(${NAME}
		${PROJECT_SOURCE_DIR}/${input}
		COMMENT "Generate HTML documentation"
	)
endfunction()

The doxygen_add_docs command is intended as a convenience for adding a target for generating documentation with Doxygen. To give the documentation a modern appearance, the doxygen-awesome-css is used.

Format.cmake

clang-format is a tool to format C/C++ code according to a set of rules and heuristics. Unlike tools like Doxygen and Cppcheck, CMake doesn’t natively support formatting tools. Here’s an overview of the steps taken to format header and source files using clang-format:

Here’s a shorter and simpler explanation of each step:

  1. Find clang-format
    find_program(CLANG-FORMAT_PATH
     clang-format REQUIRED
     PATHS "${CLANG-FORMAT_INSTALL_DIR}"
    )
    
    • Searches for the clang-format executable.
    • Stores the path in CLANG-FORMAT_PATH.
    • Fails if clang-format isn’t found, looking specifically in CLANG-FORMAT_INSTALL_DIR, defined in the configuration presets.
  2. Set File Extensions
    set(EXPRESSION h hpp hh c cc cxx cpp)
    
    • Creates a list called EXPRESSION with common C/C++ file extensions (header and source files).
  3. Prepend Directory
    list(TRANSFORM EXPRESSION PREPEND "${directory}/*.")
    
    • Adds the directory path to each file extension in EXPRESSION.
    • Transforms h to ${directory}/*.h, making it ready for searching.
  4. Find Source Files
    file(GLOB_RECURSE SOURCE_FILES FOLLOW_SYMLINKS
     LIST_DIRECTORIES false ${EXPRESSION}
    )
    
    • Searches for all files in the specified directory that match the extensions in EXPRESSION.
    • Recursively includes files from subdirectories.
    • Stores the found files in SOURCE_FILES.
  5. Format Files
    add_custom_command(TARGET ${target} PRE_BUILD 
     COMMAND ${CLANG-FORMAT_PATH} -i --style=file ${SOURCE_FILES}
    )
    
    • Adds a command to format files before building the target.
    • Uses clang-format with the -i option to edit files in place.

Testing.cmake

Using CMake’s CTest, you can write tests without relying on any third-party tools. However, since there are more robust and widely-used testing frameworks available, this template includes sample code for both Google Test and Boost.Test to help you get started.

Automatic test discovery is enabled for Google Test with the following commands:

enable_testing()
# ...
gtest_discover_tests(${target})

After enabling testing in CMake, the gtest_discover_tests function from the Google Test module is called to automatically find each test. We should also include enable_testing() in the root CMakeLists.txt for this feature to function properly.

CMake’s FetchContent provides a straightforward method for incorporating external dependencies into your project. Many well-known projects support this feature, including Google Test.

include(FetchContent)
FetchContent_Declare(
	googletest
	GIT_REPOSITORY https://github.com/google/googletest.git
	GIT_TAG v1.15.2
	GIT_SHALLOW 1
)
FetchContent_MakeAvailable(googletest)

A helper macro is defined to facilitate testing while also enabling coverage reports and dynamic checks:

macro(AddTests target)
    AddCoverage(${target})
    target_link_libraries(${target} PRIVATE gtest_main gmock)
    gtest_discover_tests(${target})
    AddMemcheck(${target})
endmacro()

The final step in testing involves writing the tests in a source file and calling the helper macro:

# ./test/libsee/CMakeLists.txt
add_executable(google_test_libsee
    google_test_see_model.cpp
)
AddTests(google_test_libsee)
EnableCoverage(libsee_obj)

Coverage.cmake

Coverage reports are relatively complex in this template because not all compilers across different platforms support generating coverage information. Fortunately, Clang-LLVM is a cross-platform toolset that does support this feature.

The general process for generating coverage reports is as follows:

  • Compile your tests with the appropriate compiler options in debug mode.
  • Run the tests to generate raw coverage information files.
  • Then utilize another tool to read these raw files and display the report, either in HTML format or in the command line.

To implement this, we use add_custom_command and add_custom_target instructions to define the necessary steps for generating the report.

find_program(LLVM_COV_PATH llvm-cov REQUIRED)
find_program(LLVM_PROFDATA_PATH llvm-profdata REQUIRED)
add_custom_target(coverage-${target}
	COMMAND $<TARGET_FILE:${target}>
	COMMAND del coverage /S /Q
	COMMAND ${LLVM_PROFDATA_PATH} merge
							-sparse default.profraw -o default.profdata
	COMMAND ${LLVM_COV_PATH} show $<TARGET_FILE:${target}>
							-instr-profile=default.profdata
							-show-line-counts-or-regions
							-use-color
							-show-instantiation-summary
							-show-branches=count
							-format=html
							-output-dir=coverage-${target}
	COMMAND ${LLVM_COV_PATH} report $<TARGET_FILE:${target}>
							-instr-profile=default.profdata
							-show-region-summary=false
							-show-branch-summary=false
	WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)

Here’s the explanation of each command:

  1. find_program(LLVM_COV_PATH llvm-cov REQUIRED)
    This command searches for the llvm-cov tool and sets its path to the variable LLVM_COV_PATH. The REQUIRED option means that CMake will raise an error if the tool is not found.

  2. find_program(LLVM_PROFDATA_PATH llvm-profdata REQUIRED)
    Similar to the first command, this searches for the llvm-profdata tool.

  3. add_custom_target(coverage-${target} ... )
    This command creates a custom target named coverage-${target}. This target will be executed when building the project, and it will perform a series of commands related to generating coverage reports for the specified target.

  4. COMMAND $<TARGET_FILE:${target}>
    This command runs the executable associated with the target specified by ${target}. This is the first step in the coverage generation process, which collects coverage data.

  5. COMMAND del coverage /S /Q
    This command deletes any existing coverage reports in the coverage directory.

  6. COMMAND ${LLVM_PROFDATA_PATH} merge -sparse default.profraw -o default.profdata
    This command uses llvm-profdata to merge the raw coverage data files (default.profraw) into a more manageable format (default.profdata).

  7. COMMAND ${LLVM_COV_PATH} show $<TARGET_FILE:${target}> ...
    This command uses llvm-cov to generate an HTML report showing the coverage information for the target executable.

Memcheck.cmake

Dynamic checks, like coverage reports, lack unified cross-platform support. Currently, dynamic checks are supported on Windows only for x86 builds, using the /fsanitize=address and /Zi compiler options. To verify if dynamic checks are working, you can intentionally insert a faulty code into one of the tests.

On Linux, this template utilizes memcheck-cover, which in turn runs Valgrind to perform dynamic checks.

Benchmarking.cmake

Google Benchmark is used for benchmarking purposes. Integrating it into the template is similar to how Google Test is incorporated.

Boost.cmake

The FetchContent command is also used to add Boost to your project by fetching Boost and all its submodules from the Git repository. While this is a one-time operation, it can take some time to complete. Alternatively, you can integrate Boost by installing it separately and using CMake’s FindPackage feature.

In either case, you must specify which libraries from Boost you are using:

set(BOOST_INCLUDE_LIBRARIES
    test
)

You can add any additional libraries to this list as needed.

Install.cmake

All the good practices we’ve followed so far will pay off in the installation and packaging stages. These typically complicated steps are streamlined by CMake, requiring only some configuration on our part to handle the process smoothly.

The following command installs the targets:

install(
	TARGETS precompiled libsee_obj libsee_static libsee_shared
	EXPORT libsee-targets
	PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/libsee
)

This ensures that the header files associated with the targets using the PUBLIC_HEADER property are installed in a folder named after the project. By doing so, it reduces the risk of conflicting header filenames in the /usr/include directory.

The following line enables users of our project to include it in their own using FetchContent:

export(
	TARGETS precompiled libsee_obj libsee_static libsee_shared
	NAMESPACE see::
	FILE "${PROJECT_BINARY_DIR}/libsee-targets.cmake"
)

The following line enables users of our project to include it in their own with find_package:

install(
	EXPORT libsee-targets
	NAMESPACE see::
	DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/libsee"
)
configure_package_config_file(
	"${CMAKE_CURRENT_SOURCE_DIR}/cmake/libsee-config.cmake.in"
	"${CMAKE_CURRENT_BINARY_DIR}/cmake/libsee-config.cmake"
	INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/libsee/cmake"
	PATH_VARS CMAKE_INSTALL_INCLUDEDIR
)
write_basic_package_version_file(
	"${CMAKE_CURRENT_BINARY_DIR}/cmake/libsee-config-version.cmake"
	VERSION ${PACKAGE_VERSION}
	COMPATIBILITY ExactVersion
)
install(
	FILES
	"${CMAKE_CURRENT_BINARY_DIR}/cmake/libsee-config.cmake"
	"${CMAKE_CURRENT_BINARY_DIR}/cmake/libsee-config-version.cmake"
	DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/libsee"
)

This installs the CMake configuration files, making it possible for other projects to use find_package(libsee) to locate and link against our targets, with the targets being available under the see:: namespace.

Finally, add the following line to enable packaging with CPack:

include(CPack)

Customization

This template is designed to be flexible and easily customizable. You can modify the targets to add or remove executable and library targets based on your project requirements and modify presets to adjust the configuration, build, test, and packaging presets to fit your development workflow. Just remember to change the target names, rename folders, and use your own information for packaging in the presets and Install.cmake.

Closing Thoughts

This template aims to streamline your development process by providing a solid cross-platform foundation for CMake projects. By leveraging modern CMake features, you can enhance your project’s portability and maintainability. Please feel free to reach out for new features or to report any issues on GitHub. I hope you find this template helpful!