SeeMake Template
- Preface
- Directory Layout
- CMake Commands
- CMakePresets.json File
- CMakeLists.txt Files
- .cmake Files
- Customization
- Closing Thoughts
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 otherCMakeLists.txt
files in the subdirectories. Later commits add aCMakePresets.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 inbenchmark
. 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 nameddependencies.dot
.
After running these commands, you should have a visual representation of your project’s dependencies.
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:
- 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 inCLANG-FORMAT_INSTALL_DIR
, defined in the configuration presets.
- Searches for the
- 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).
- Creates a list called
- 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.
- Adds the directory path to each file extension in
- 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
.
- Searches for all files in the specified directory that match the extensions in
- 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:
-
find_program(LLVM_COV_PATH llvm-cov REQUIRED)
This command searches for thellvm-cov
tool and sets its path to the variableLLVM_COV_PATH
. TheREQUIRED
option means that CMake will raise an error if the tool is not found. -
find_program(LLVM_PROFDATA_PATH llvm-profdata REQUIRED)
Similar to the first command, this searches for thellvm-profdata
tool. -
add_custom_target(coverage-${target} ... )
This command creates a custom target namedcoverage-${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. -
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. -
COMMAND del coverage /S /Q
This command deletes any existing coverage reports in thecoverage
directory. -
COMMAND ${LLVM_PROFDATA_PATH} merge -sparse default.profraw -o default.profdata
This command usesllvm-profdata
to merge the raw coverage data files (default.profraw
) into a more manageable format (default.profdata
). -
COMMAND ${LLVM_COV_PATH} show $<TARGET_FILE:${target}> ...
This command usesllvm-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!