Thanks to visit codestin.com
Credit goes to www.scribd.com

0% found this document useful (0 votes)
33 views597 pages

CMake

The document is a comprehensive guide to CMake, aimed at managing and building C++ projects from basic to advanced levels. It covers topics such as the installation of CMake on various operating systems, understanding CMakeLists.txt, and the workflow of configuring, generating, and building projects. Additionally, it includes practical examples and best practices to enhance the user's proficiency with CMake.

Uploaded by

Ujjwal Arora
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
33 views597 pages

CMake

The document is a comprehensive guide to CMake, aimed at managing and building C++ projects from basic to advanced levels. It covers topics such as the installation of CMake on various operating systems, understanding CMakeLists.txt, and the workflow of configuring, generating, and building projects. Additionally, it includes practical examples and best practices to enhance the user's proficiency with CMake.

Uploaded by

Ujjwal Arora
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 597

CMake:

The Comprehensive Guide to Managing and Building


C++ Projects
(From Basics to Mastery)

Prepared by Ayman Alheraki


simplifycpp.org

February 2025
Contents

Contents 2

Author’s Introduction 23

1 Introduction to CMake 25
1.1 Why Do You Need CMake? . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.1.2 The Challenges of Traditional Build Systems . . . . . . . . . . . . . . 26
1.1.3 How CMake Solves These Challenges . . . . . . . . . . . . . . . . . . 28
1.1.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.2 CMake vs. Traditional Build Systems (Make, Autotools, Ninja, etc.) . . . . . . 31
1.2.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.2.2 Traditional Build Systems: Overview and Limitations . . . . . . . . . . 31
1.2.3 How CMake Compares to Traditional Build Systems . . . . . . . . . . 35
1.2.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
1.3 Installing CMake on Different Operating Systems (Windows, Linux, macOS) . 38
1.3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
1.3.2 Installing CMake on Windows . . . . . . . . . . . . . . . . . . . . . . 38
1.3.3 Installing CMake on Linux . . . . . . . . . . . . . . . . . . . . . . . . 42
1.3.4 Installing CMake on macOS . . . . . . . . . . . . . . . . . . . . . . . 44

2
3

1.3.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
1.4 Verifying CMake Installation and Running It . . . . . . . . . . . . . . . . . . . 48
1.4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
1.4.2 Verifying CMake Installation . . . . . . . . . . . . . . . . . . . . . . . 48
1.4.3 Running CMake for the First Time . . . . . . . . . . . . . . . . . . . . 51
1.4.4 Troubleshooting Common Issues . . . . . . . . . . . . . . . . . . . . . 55
1.4.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
1.5 Your First CMake Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
1.5.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
1.5.2 Setting Up a Simple C++ Project . . . . . . . . . . . . . . . . . . . . . 57
1.5.3 Creating the CMakeLists.txt File . . . . . . . . . . . . . . . . . . 59
1.5.4 Configuring the Build System with CMake . . . . . . . . . . . . . . . 60
1.5.5 Building the Project . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
1.5.6 Running the Executable . . . . . . . . . . . . . . . . . . . . . . . . . 63
1.5.7 Understanding the Build Process . . . . . . . . . . . . . . . . . . . . . 63
1.5.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64

2 Fundamentals of CMakeLists.txt 65
2.1 Understanding the Structure of CMakeLists.txt . . . . . . . . . . . . . . . . . 65
2.1.1 Introduction to CMakeLists.txt . . . . . . . . . . . . . . . . . . . . . . 65
2.1.2 Key Sections of a CMakeLists.txt File . . . . . . . . . . . . . . . . . . 66
2.1.3 Best Practices for Organizing CMakeLists.txt . . . . . . . . . . . . . . 72
2.1.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
2.2 Defining the Minimal CMake Project . . . . . . . . . . . . . . . . . . . . . . . 74
2.2.1 What Makes a CMake Project ”Minimal”? . . . . . . . . . . . . . . . 74
2.2.2 CMake File Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
2.2.3 CMakeLists.txt File for the Minimal Project . . . . . . . . . . . . . . . 75
2.2.4 Understanding the Minimal CMake Project . . . . . . . . . . . . . . . 79
4

2.2.5 Next Steps and Expansion . . . . . . . . . . . . . . . . . . . . . . . . 80


2.2.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
2.3 Essential CMake Commands (cmake minimum required, project,
add executable) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
2.3.1 cmake minimum required . . . . . . . . . . . . . . . . . . . . . 82
2.3.2 project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
2.3.3 add executable . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
2.3.4 Putting It All Together: A Simple Example . . . . . . . . . . . . . . . 87
2.3.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
2.4 CMake Variable Types (CACHE, ENV, LOCAL) . . . . . . . . . . . . . . . . . 89
2.4.1 CACHE Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
2.4.2 ENV Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
2.4.3 LOCAL Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
2.4.4 Summary of Variable Types . . . . . . . . . . . . . . . . . . . . . . . 95
2.4.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
2.5 Using message() for Debugging and Output . . . . . . . . . . . . . . . . . 97
2.5.1 Purpose of the message() Command . . . . . . . . . . . . . . . . . 97
2.5.2 Syntax of message() . . . . . . . . . . . . . . . . . . . . . . . . . 98
2.5.3 Basic Examples of message() . . . . . . . . . . . . . . . . . . . . . 99
2.5.4 Using Variables in message() . . . . . . . . . . . . . . . . . . . . . 101
2.5.5 Controlling Output Visibility . . . . . . . . . . . . . . . . . . . . . . . 102

3 Building Projects with CMake 104


3.1 Understanding ”Configure,” ”Generate,” and ”Build” Steps . . . . . . . . . . . 104
3.1.1 Overview of the CMake Workflow . . . . . . . . . . . . . . . . . . . . 104
3.1.2 The ”Configure” Step . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
3.1.3 The ”Generate” Step . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
3.1.4 The ”Build” Step . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
5

3.1.5 Relationship Between Configure, Generate, and Build . . . . . . . . . 109


3.1.6 Re-running the Configuration Steps . . . . . . . . . . . . . . . . . . . 110
3.1.7 Summary of the Configure, Generate, and Build Phases . . . . . . . . . 110
3.1.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
3.2 Running cmake with Different Generators (Ninja, Makefile, Visual Studio) . . 112
3.2.1 Overview of CMake Generators . . . . . . . . . . . . . . . . . . . . . 112
3.2.2 Running cmake with the Ninja Generator . . . . . . . . . . . . . . . . 113
3.2.3 Running cmake with the Makefile Generator . . . . . . . . . . . . . . 114
3.2.4 Running cmake with the Visual Studio Generator . . . . . . . . . . . 116
3.2.5 Choosing the Right Generator for Your Project . . . . . . . . . . . . . 117
3.2.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
3.3 Managing Source Files and Output Executables . . . . . . . . . . . . . . . . . 119
3.3.1 Overview of Source Files in CMake . . . . . . . . . . . . . . . . . . . 119
3.3.2 Defining Source Files for Executables . . . . . . . . . . . . . . . . . . 120
3.3.3 Organizing Source Files Using file() and
aux source directory() . . . . . . . . . . . . . . . . . . . . . 121
3.3.4 Defining Output Executables and Directories . . . . . . . . . . . . . . 122
3.3.5 Managing Multiple Executables and Targets . . . . . . . . . . . . . . . 123
3.3.6 Defining Libraries for Reusability . . . . . . . . . . . . . . . . . . . . 124
3.3.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
3.4 Running cmake --build and cmake --install . . . . . . . . . . . . . 127
3.4.1 Running cmake --build . . . . . . . . . . . . . . . . . . . . . . . 127
3.4.2 Running cmake --install . . . . . . . . . . . . . . . . . . . . . 129
3.4.3 Customizing the Installation Process . . . . . . . . . . . . . . . . . . . 132
3.4.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
3.5 Controlling Build Options via CMAKE BUILD TYPE . . . . . . . . . . . . . . 134
3.5.1 Overview of CMAKE BUILD TYPE . . . . . . . . . . . . . . . . . . . 134
6

3.5.2 Setting CMAKE BUILD TYPE . . . . . . . . . . . . . . . . . . . . . . 135


3.5.3 Effect of CMAKE BUILD TYPE on the Build Process . . . . . . . . . . 136
3.5.4 Multi-Configuration Generators and CMAKE BUILD TYPE . . . . . . 137
3.5.5 Customizing Build Types . . . . . . . . . . . . . . . . . . . . . . . . . 138
3.5.6 Advanced Control of Build Options . . . . . . . . . . . . . . . . . . . 139
3.5.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140

4 Working with Libraries in CMake (Static & Shared) 141


4.1 Difference Between Static and Shared Libraries . . . . . . . . . . . . . . . . . 141
4.1.1 Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
4.1.2 Build Process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
4.1.3 Key Differences Between Static and Shared Libraries . . . . . . . . . . 143
4.1.4 Advantages and Disadvantages . . . . . . . . . . . . . . . . . . . . . . 144
4.1.5 Use Cases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
4.1.6 CMake Configuration for Static and Shared Libraries . . . . . . . . . . 146
4.1.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
4.2 Creating a Static Library (add library(MyLib STATIC)) . . . . . . . . 148
4.2.1 Understanding Static Libraries in CMake . . . . . . . . . . . . . . . . 148
4.2.2 Basic Syntax of add library() . . . . . . . . . . . . . . . . . . . 148
4.2.3 Detailed Example: Creating a Static Library . . . . . . . . . . . . . . . 149
4.2.4 Linking the Static Library . . . . . . . . . . . . . . . . . . . . . . . . 152
4.2.5 Using target include directories() . . . . . . . . . . . . . 152
4.2.6 Handling Dependencies in Static Libraries . . . . . . . . . . . . . . . . 153
4.2.7 Using CMake Variables for Source Files . . . . . . . . . . . . . . . . . 153
4.2.8 Installing the Static Library . . . . . . . . . . . . . . . . . . . . . . . 154
4.2.9 Advantages of Static Libraries . . . . . . . . . . . . . . . . . . . . . . 154
4.2.10 Disadvantages of Static Libraries . . . . . . . . . . . . . . . . . . . . 154
4.2.11 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
7

4.3 Creating a Shared Library (add library(MyLib SHARED)) . . . . . . . . 156


4.3.1 Understanding Shared Libraries in CMake . . . . . . . . . . . . . . . . 156
4.3.2 Basic Syntax of add library() for Shared Libraries . . . . . . . . 157
4.3.3 Detailed Example: Creating a Shared Library . . . . . . . . . . . . . . 158
4.3.4 Linking the Shared Library . . . . . . . . . . . . . . . . . . . . . . . . 160
4.3.5 Handling RPATH and Shared Library Location . . . . . . . . . . . . . 161
4.3.6 Using target include directories() . . . . . . . . . . . . . 162
4.3.7 Versioning Shared Libraries . . . . . . . . . . . . . . . . . . . . . . . 162
4.3.8 Advantages of Shared Libraries . . . . . . . . . . . . . . . . . . . . . 163
4.3.9 Disadvantages of Shared Libraries . . . . . . . . . . . . . . . . . . . . 163
4.3.10 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
4.4 Linking Libraries (target link libraries) . . . . . . . . . . . . . . . . 165
4.4.1 Understanding the Purpose of target link libraries() . . . . 165
4.4.2 Linking Static Libraries . . . . . . . . . . . . . . . . . . . . . . . . . 166
4.4.3 Linking Shared Libraries . . . . . . . . . . . . . . . . . . . . . . . . . 167
4.4.4 Specifying Link Dependencies . . . . . . . . . . . . . . . . . . . . . . 167
4.4.5 Linking Multiple Libraries . . . . . . . . . . . . . . . . . . . . . . . . 169
4.4.6 Linking System Libraries . . . . . . . . . . . . . . . . . . . . . . . . . 169
4.4.7 Handling Transitive Dependencies . . . . . . . . . . . . . . . . . . . . 170
4.4.8 Linking Libraries with Custom Paths . . . . . . . . . . . . . . . . . . 170
4.4.9 Best Practices for Linking Libraries . . . . . . . . . . . . . . . . . . . 171
4.4.10 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
4.5 Controlling Symbol Visibility (PUBLIC, PRIVATE, INTERFACE) . . . . . . 173
4.5.1 Introduction to Symbol Visibility in CMake . . . . . . . . . . . . . . . 173
4.5.2 Understanding PUBLIC, PRIVATE, and INTERFACE . . . . . . . . . 173
4.5.3 Controlling Include Directories with
target include directories() . . . . . . . . . . . . . . . . 174
8

4.5.4 Controlling Linking with target link libraries() . . . . . . . 176


4.5.5 Controlling Compile Options with target compile options() . 177
4.5.6 Choosing the Right Visibility for Your Library . . . . . . . . . . . . . 178
4.5.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179

5 Organizing Large-Scale Projects with CMake 180


5.1 Understanding Multi-File Project Structure . . . . . . . . . . . . . . . . . . . 180
5.1.1 Introduction to Multi-File Project Structure . . . . . . . . . . . . . . . 180
5.1.2 Evolution of Project Structure . . . . . . . . . . . . . . . . . . . . . . 181
5.1.3 Setting Up a Multi-File Project with CMake . . . . . . . . . . . . . . . 183
5.1.4 Benefits of a Multi-File Project Structure . . . . . . . . . . . . . . . . 184
5.1.5 Best Practices for Structuring Large-Scale Projects . . . . . . . . . . . 185
5.1.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
5.2 Creating Subprojects (add subdirectory) . . . . . . . . . . . . . . . . . . 187
5.2.1 Introduction to Subprojects in CMake . . . . . . . . . . . . . . . . . . 187
5.2.2 Understanding add subdirectory() . . . . . . . . . . . . . . . . 187
5.2.3 Structuring a Project with Subprojects . . . . . . . . . . . . . . . . . . 188
5.2.4 Defining Subprojects in CMake . . . . . . . . . . . . . . . . . . . . . 189
5.2.5 Benefits of Using add subdirectory() for Subprojects . . . . . . 191
5.2.6 Using EXCLUDE FROM ALL for Optional Subprojects . . . . . . . . . 192
5.2.7 Best Practices for Managing Subprojects in CMake . . . . . . . . . . . 193
5.2.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
5.3 Using find package() to Locate External Libraries . . . . . . . . . . . . . 194
5.3.1 Introduction to find package() . . . . . . . . . . . . . . . . . . . 194
5.3.2 Understanding find package() . . . . . . . . . . . . . . . . . . . 194
5.3.3 Finding and Linking External Libraries . . . . . . . . . . . . . . . . . 195
5.3.4 Understanding find package() Search Mechanism . . . . . . . . . 197
5.3.5 Using CONFIG and MODULE Modes . . . . . . . . . . . . . . . . . . 197
9

5.3.6 Handling Missing Dependencies Gracefully . . . . . . . . . . . . . . . 198


5.3.7 Best Practices for Using find package() . . . . . . . . . . . . . . 198
5.3.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
5.4 Using FetchContent to Download Dependencies at Build Time . . . . . . . 200
5.4.1 Introduction to FetchContent . . . . . . . . . . . . . . . . . . . . 200
5.4.2 Understanding FetchContent . . . . . . . . . . . . . . . . . . . . 201
5.4.3 Example: Fetching and Using an External Library . . . . . . . . . . . 202
5.4.4 Using FetchContent with CMake Packages . . . . . . . . . . . . . 203
5.4.5 Handling Already Installed Dependencies . . . . . . . . . . . . . . . . 204
5.4.6 Using FetchContent with Non-Git Sources . . . . . . . . . . . . . 205
5.4.7 Best Practices for Using FetchContent . . . . . . . . . . . . . . . 206
5.4.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
5.5 Using ExternalProject Add for External Project Integration . . . . . . . 208
5.5.1 Introduction to ExternalProject Add . . . . . . . . . . . . . . . 208
5.5.2 UnderstandingExternalProject Add . . . . . . . . . . . . . . . 208
5.5.3 Example: Building an External Library with ExternalProject Add 210
5.5.4 Building and Installing External Projects . . . . . . . . . . . . . . . . 211
5.5.5 Handling Dependencies Between External Projects . . . . . . . . . . . 212
5.5.6 Using ExternalProject Add with CMake Targets . . . . . . . . . 212
5.5.7 Differences Between ExternalProject Add and FetchContent 213
5.5.8 Best Practices for Using ExternalProject Add . . . . . . . . . . 214
5.5.9 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215

6 Dependency Management in CMake 216


6.1 Using find package() to Locate Installed Libraries . . . . . . . . . . . . . 216
6.1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
6.1.2 Understanding find package() . . . . . . . . . . . . . . . . . . . 216
6.1.3 Locating a Library Using find package() . . . . . . . . . . . . . 217
10

6.1.4 Config-Mode vs. Module-Mode in find package() . . . . . . . . 219


6.1.5 Handling Missing Dependencies Gracefully . . . . . . . . . . . . . . . 220
6.1.6 Specifying Custom Paths for Dependencies . . . . . . . . . . . . . . . 221
6.1.7 Using find package() in a Complete Project Example . . . . . . . 221
6.1.8 Best Practices for Using find package() . . . . . . . . . . . . . . 223
6.1.9 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
6.2 Creating Config.cmake Files for Custom Libraries . . . . . . . . . . . . . 224
6.2.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
6.2.2 What is a Config.cmake File? . . . . . . . . . . . . . . . . . . . . 224
6.2.3 Structure of a Config.cmake File . . . . . . . . . . . . . . . . . . . 224
6.2.4 Where Should the Config.cmake File Be Installed? . . . . . . . . . 226
6.2.5 Using find package() to Find a Custom Library . . . . . . . . . . 227
6.2.6 Handling Multiple Versions of a Custom Library . . . . . . . . . . . . 228
6.2.7 Supporting Optional Features and Components . . . . . . . . . . . . . 228
6.2.8 Best Practices for Creating Config.cmake Files . . . . . . . . . . . 229
6.2.9 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
6.3 Using FetchContent to Fetch Dependencies Dynamically . . . . . . . . . . 231
6.3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
6.3.2 What is FetchContent? . . . . . . . . . . . . . . . . . . . . . . . . 231
6.3.3 Basic Syntax of FetchContent . . . . . . . . . . . . . . . . . . . . 232
6.3.4 Example: Using FetchContent to Fetch a Dependency . . . . . . . 233
6.3.5 Fetching Dependencies from Different Sources . . . . . . . . . . . . . 234
6.3.6 Managing Dependency Versions . . . . . . . . . . . . . . . . . . . . . 235
6.3.7 Using FetchContent for Multiple Dependencies . . . . . . . . . . 236
6.3.8 Benefits and Limitations of FetchContent . . . . . . . . . . . . . . 237
6.3.9 Best Practices for Using FetchContent . . . . . . . . . . . . . . . 238
6.3.10 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
11

6.4 Integrating pkg-config and find library() . . . . . . . . . . . . . . . 240


6.4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
6.4.2 What is pkg-config? . . . . . . . . . . . . . . . . . . . . . . . . . 240
6.4.3 Integrating pkg-config with CMake . . . . . . . . . . . . . . . . . 241
6.4.4 Using find library() to Find Libraries . . . . . . . . . . . . . . 243
6.4.5 Combining pkg-config and find library() . . . . . . . . . . 244
6.4.6 Best Practices for Using pkg-config and find library() . . . 246
6.4.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
6.5 Working with vcpkg and Conan for Package Management . . . . . . . . . . 248
6.5.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
6.5.2 What are vcpkg and Conan? . . . . . . . . . . . . . . . . . . . . . . 248
6.5.3 Integrating vcpkg with CMake . . . . . . . . . . . . . . . . . . . . . 249
6.5.4 Integrating Conan with CMake . . . . . . . . . . . . . . . . . . . . . 251
6.5.5 Comparing vcpkg and Conan . . . . . . . . . . . . . . . . . . . . . 253
6.5.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255

7 Working with CMake GUI & CLI 256


7.1 Using the CMake GUI on Windows and Linux . . . . . . . . . . . . . . . . . . 256
7.1.1 Overview of the CMake GUI . . . . . . . . . . . . . . . . . . . . . . . 256
7.1.2 Installing the CMake GUI . . . . . . . . . . . . . . . . . . . . . . . . 257
7.1.3 The CMake GUI Interface . . . . . . . . . . . . . . . . . . . . . . . . 258
7.1.4 Using the CMake GUI on Linux . . . . . . . . . . . . . . . . . . . . . 261
7.1.5 Using the CMake GUI on Windows . . . . . . . . . . . . . . . . . . . 261
7.1.6 Troubleshooting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
7.1.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
7.2 Command-Line Interface (CLI) with CMake . . . . . . . . . . . . . . . . . . . 263
7.2.1 Overview of the CMake CLI . . . . . . . . . . . . . . . . . . . . . . . 263
7.2.2 Basic CMake Command-Line Workflow . . . . . . . . . . . . . . . . . 264
12

7.2.3 Important CMake CLI Commands and Options . . . . . . . . . . . . . 266


7.2.4 Advanced CMake CLI Usage . . . . . . . . . . . . . . . . . . . . . . 268
7.2.5 Troubleshooting Common Issues . . . . . . . . . . . . . . . . . . . . . 269
7.2.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
7.3 Configuring Different Generators (-G "Ninja", -G "Unix Makefiles",
...) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
7.3.1 What is a Generator in CMake? . . . . . . . . . . . . . . . . . . . . . 271
7.3.2 Common Generators in CMake . . . . . . . . . . . . . . . . . . . . . 272
7.3.3 Choosing the Right Generator . . . . . . . . . . . . . . . . . . . . . . 276
7.3.4 Specifying Multiple Generators . . . . . . . . . . . . . . . . . . . . . 277
7.3.5 Troubleshooting Generator Issues . . . . . . . . . . . . . . . . . . . . 278
7.3.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278
7.4 Managing Options and Settings with ccmake . . . . . . . . . . . . . . . . . . 279
7.4.1 What is ccmake? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
7.4.2 Installing and Using ccmake . . . . . . . . . . . . . . . . . . . . . . 280
7.4.3 Basic Workflow with ccmake . . . . . . . . . . . . . . . . . . . . . . 280
7.4.4 Managing Cache Variables in ccmake . . . . . . . . . . . . . . . . . 282
7.4.5 Differences Between ccmake, CMake GUI, and CLI . . . . . . . . . 284
7.4.6 Example ccmake Workflow . . . . . . . . . . . . . . . . . . . . . . . 284
7.4.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285

8 CMake and Different Development Environments 287


8.1 Integration with Visual Studio . . . . . . . . . . . . . . . . . . . . . . . . . . 287
8.1.1 Why Integrate CMake with Visual Studio? . . . . . . . . . . . . . . . 288
8.1.2 Generating Visual Studio Project Files with CMake . . . . . . . . . . . 289
8.1.3 Understanding CMake and Visual Studio Configurations . . . . . . . . 290
8.1.4 Using Visual Studio to Build CMake Projects . . . . . . . . . . . . . . 291
8.1.5 Debugging CMake Projects in Visual Studio . . . . . . . . . . . . . . 292
13

8.1.6 Modifying CMake Configuration for Visual Studio Projects . . . . . . 292


8.1.7 Benefits of Using CMake with Visual Studio . . . . . . . . . . . . . . 293
8.1.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
8.2 Integration with CLion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
8.2.1 Why Integrate CMake with CLion? . . . . . . . . . . . . . . . . . . . 295
8.2.2 Setting Up CLion for CMake Projects . . . . . . . . . . . . . . . . . . 296
8.2.3 CLion’s CMake Configuration and Options . . . . . . . . . . . . . . . 297
8.2.4 Building and Running CMake Projects in CLion . . . . . . . . . . . . 299
8.2.5 Debugging CMake Projects in CLion . . . . . . . . . . . . . . . . . . 300
8.2.6 Running Tests in CLion . . . . . . . . . . . . . . . . . . . . . . . . . 301
8.2.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
8.3 Integration with Qt Creator . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
8.3.1 Why Use CMake with Qt Creator? . . . . . . . . . . . . . . . . . . . . 303
8.3.2 Setting Up a CMake Project in Qt Creator . . . . . . . . . . . . . . . . 304
8.3.3 Managing CMake Configuration in Qt Creator . . . . . . . . . . . . . 305
8.3.4 Building and Running CMake Projects in Qt Creator . . . . . . . . . . 306
8.3.5 Debugging CMake Projects in Qt Creator . . . . . . . . . . . . . . . . 307
8.3.6 Running Tests in Qt Creator . . . . . . . . . . . . . . . . . . . . . . . 308
8.3.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309
8.4 Integration with Xcode on macOS . . . . . . . . . . . . . . . . . . . . . . . . 310
8.4.1 Why Use CMake with Xcode? . . . . . . . . . . . . . . . . . . . . . . 310
8.4.2 Setting Up CMake with Xcode . . . . . . . . . . . . . . . . . . . . . . 311
8.4.3 Generating Xcode Project Files with CMake . . . . . . . . . . . . . . 312
8.4.4 Building and Running the CMake Project in Xcode . . . . . . . . . . . 314
8.4.5 Debugging CMake Projects in Xcode . . . . . . . . . . . . . . . . . . 315
8.4.6 Managing Dependencies with CMake in Xcode . . . . . . . . . . . . . 316
8.4.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
14

8.5 Working with VS Code and CMake Tools . . . . . . . . . . . . . . . . . . . 319


8.5.1 Installing and Setting Up VS Code for C++ Development . . . . . . . . 319
8.5.2 Creating and Configuring a CMake Project in VS Code . . . . . . . . . 320
8.5.3 Debugging with CMake Tools in VS Code . . . . . . . . . . . . . . . 323
8.5.4 Additional Features of CMake Tools in VS Code . . . . . . . . . . . . 325
8.5.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326

9 Using CTest for Project Testing 327


9.1 Introduction to CTest and Its Importance . . . . . . . . . . . . . . . . . . . . . 327
9.1.1 What is CTest? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
9.1.2 Why is CTest Important? . . . . . . . . . . . . . . . . . . . . . . . . . 328
9.1.3 Key Features of CTest . . . . . . . . . . . . . . . . . . . . . . . . . . 329
9.1.4 CTest vs Other Testing Tools . . . . . . . . . . . . . . . . . . . . . . . 330
9.1.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
9.2 Writing Tests with add test() . . . . . . . . . . . . . . . . . . . . . . . . . 332
9.2.1 What is add test()? . . . . . . . . . . . . . . . . . . . . . . . . . . 332
9.2.2 Basic Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
9.2.3 Running Tests with CTest . . . . . . . . . . . . . . . . . . . . . . . . 334
9.2.4 Advanced Use of add test() . . . . . . . . . . . . . . . . . . . . . 334
9.2.5 Best Practices for Writing Tests with add test() . . . . . . . . . . . 336
9.2.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
9.3 Running Tests (ctest) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
9.3.1 What is ctest? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
9.3.2 Basic Usage of ctest . . . . . . . . . . . . . . . . . . . . . . . . . . 338
9.3.3 Running Specific Tests . . . . . . . . . . . . . . . . . . . . . . . . . . 339
9.3.4 Test Output and Reporting . . . . . . . . . . . . . . . . . . . . . . . . 340
9.3.5 Running Tests with Timeouts . . . . . . . . . . . . . . . . . . . . . . 341
9.3.6 Running Tests in Parallel . . . . . . . . . . . . . . . . . . . . . . . . . 341
15

9.3.7 Test Results and Exit Codes . . . . . . . . . . . . . . . . . . . . . . . 342


9.3.8 Integration with Continuous Integration (CI) Tools . . . . . . . . . . . 342
9.3.9 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
9.4 Unit Testing with Google Test . . . . . . . . . . . . . . . . . . . . . . . . . . 344
9.4.1 What is Google Test? . . . . . . . . . . . . . . . . . . . . . . . . . . . 344
9.4.2 Integrating Google Test with CMake . . . . . . . . . . . . . . . . . . . 345
9.4.3 Writing Unit Tests with Google Test . . . . . . . . . . . . . . . . . . . 346
9.4.4 Running Unit Tests with Google Test . . . . . . . . . . . . . . . . . . 349
9.4.5 Using Google Mock with Google Test . . . . . . . . . . . . . . . . . . 349
9.4.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
9.5 Generating Test Reports and Analyzing Results . . . . . . . . . . . . . . . . . 351
9.5.1 Why Test Reports Matter . . . . . . . . . . . . . . . . . . . . . . . . . 351
9.5.2 Generating Simple Test Output with ctest . . . . . . . . . . . . . . . 352
9.5.3 Generating XML Reports with ctest . . . . . . . . . . . . . . . . . 352
9.5.4 Integrating with CI Tools . . . . . . . . . . . . . . . . . . . . . . . . . 354
9.5.5 Analyzing Test Results . . . . . . . . . . . . . . . . . . . . . . . . . . 355
9.5.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357

10 Packaging Projects with CPack 358


10.1 Introduction to CPack and Its Role . . . . . . . . . . . . . . . . . . . . . . . . 358
10.2 Creating Installation Packages (.deb, .rpm, .msi, .tar.gz) . . . . . . . . 362
10.3 Configuring CPackConfig.cmake . . . . . . . . . . . . . . . . . . . . . . 368
10.3.1 What is CPackConfig.cmake? . . . . . . . . . . . . . . . . . . . . 368
10.3.2 Creating and Using CPackConfig.cmake . . . . . . . . . . . . . . 368
10.3.3 Key Configuration Options in CPackConfig.cmake . . . . . . . . 371
10.3.4 Advanced Usage: Customizing the Packaging Process . . . . . . . . . 373
10.3.5 Final Thoughts on CPackConfig.cmake . . . . . . . . . . . . . . 374
10.4 Supporting Multiple Operating Systems . . . . . . . . . . . . . . . . . . . . . 375
16

10.4.1 Challenges of Supporting Multiple OSes . . . . . . . . . . . . . . . . 375


10.4.2 Cross-Platform Packaging with CPack . . . . . . . . . . . . . . . . . . 376
10.4.3 Final Thoughts on Supporting Multiple Operating Systems . . . . . . . 380
10.5 Distributing Projects to End Users . . . . . . . . . . . . . . . . . . . . . . . . 382
10.5.1 Understanding Distribution Channels . . . . . . . . . . . . . . . . . . 382
10.5.2 Final Thoughts on Distributing Projects . . . . . . . . . . . . . . . . . 387

11 CMake and CI/CD Integration 388


11.1 Using CMake with GitHub Actions . . . . . . . . . . . . . . . . . . . . . . . 388
11.1.1 What is GitHub Actions? . . . . . . . . . . . . . . . . . . . . . . . . . 388
11.1.2 Why Use CMake with GitHub Actions? . . . . . . . . . . . . . . . . . 389
11.1.3 Setting Up GitHub Actions for CMake Projects . . . . . . . . . . . . . 389
11.1.4 Handling Multiple Operating Systems and Environments . . . . . . . . 393
11.1.5 Advanced Topics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 394
11.1.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
11.2 Integrating CMake with GitLab CI/CD . . . . . . . . . . . . . . . . . . . . . . 396
11.3 Working with Jenkins and CMake . . . . . . . . . . . . . . . . . . . . . . . . 403
11.3.1 What is Jenkins? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
11.3.2 Why Use Jenkins with CMake? . . . . . . . . . . . . . . . . . . . . . 403
11.3.3 Setting Up Jenkins for CMake Projects . . . . . . . . . . . . . . . . . 404
11.3.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
11.4 Automating Builds on Cloud Platforms . . . . . . . . . . . . . . . . . . . . . . 412
11.4.1 Why Automate Builds on Cloud Platforms? . . . . . . . . . . . . . . . 412
11.4.2 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420

12 Optimizing Build Performance with CMake 421


12.1 Using ccache and distcc to Speed Up Compilation . . . . . . . . . . . . . 421
12.1.1 Introduction to Build Performance Optimization . . . . . . . . . . . . 421
17

12.1.2 Combining ccache and distcc . . . . . . . . . . . . . . . . . . . . 427


12.1.3 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 428
12.2 Working with Unity Builds . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
12.2.1 What is a Unity Build? . . . . . . . . . . . . . . . . . . . . . . . . . . 429
12.2.2 How Unity Builds Work . . . . . . . . . . . . . . . . . . . . . . . . . 430
12.2.3 Benefits of Unity Builds . . . . . . . . . . . . . . . . . . . . . . . . . 430
12.2.4 Drawbacks of Unity Builds . . . . . . . . . . . . . . . . . . . . . . . . 431
12.2.5 Setting Up Unity Builds in CMake . . . . . . . . . . . . . . . . . . . . 432
12.2.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
12.3 Reducing Link Time with Link Time Optimization (LTO) . . . . . . . . . . . 437
12.3.1 What is Link Time Optimization (LTO)? . . . . . . . . . . . . . . . . . 437
12.3.2 Benefits of Link Time Optimization . . . . . . . . . . . . . . . . . . . 438
12.3.3 Drawbacks and Considerations . . . . . . . . . . . . . . . . . . . . . . 439
12.3.4 Enabling LTO in CMake . . . . . . . . . . . . . . . . . . . . . . . . . 441
12.3.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
12.4 Optimizing Compilation Flags CMAKE CXX FLAGS RELEASE) . . . . . . . . 444
12.4.1 What are Compilation Flags? . . . . . . . . . . . . . . . . . . . . . . . 444
12.4.2 Why Focus on CMAKE CXX FLAGS RELEASE? . . . . . . . . . . . . 445
12.4.3 Key Optimization Flags for CMAKE CXX FLAGS RELEASE . . . . . . 445
12.4.4 Combining Compilation Flags for Release Builds . . . . . . . . . . . . 449
12.4.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 450
12.5 Using Ninja for Faster Builds . . . . . . . . . . . . . . . . . . . . . . . . . . 451
12.5.1 What is Ninja? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451
12.5.2 Why Use Ninja? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451
12.5.3 How to Use Ninja with CMake . . . . . . . . . . . . . . . . . . . . . . 453
12.5.4 Fine-Tuning Ninja for Maximum Performance . . . . . . . . . . . . . 455
12.5.5 Ninja vs. Make: Why Ninja is Faster . . . . . . . . . . . . . . . . . . 457
18

12.5.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 458

13 Writing Custom CMake Modules 459


13.1 Creating Custom Modules (FindMyLib.cmake) . . . . . . . . . . . . . . . 459
13.1.1 Understanding the Purpose of FindMyLib.cmake . . . . . . . . . . 459
13.1.2 Key Structure of FindMyLib.cmake . . . . . . . . . . . . . . . . . 460
13.1.3 Example of FindMyLib.cmake . . . . . . . . . . . . . . . . . . . . 463
13.1.4 How to Use FindMyLib.cmake in a CMake Project . . . . . . . . . 464
13.1.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465
13.2 Defining Custom CMake Commands (macro, function) . . . . . . . . . . 466
13.2.1 Introduction to Custom CMake Commands . . . . . . . . . . . . . . . 466
13.2.2 Using macro() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 466
13.2.3 Using function() . . . . . . . . . . . . . . . . . . . . . . . . . . . 468
13.2.4 Differences Between macro() and function() . . . . . . . . . . 470
13.2.5 Best Practices for Custom Commands . . . . . . . . . . . . . . . . . . 470
13.2.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471
13.3 Managing Environment Variables Inside CMake Modules . . . . . . . . . . . . 472
13.3.1 Introduction to Environment Variables in CMake . . . . . . . . . . . . 472
13.3.2 Accessing Environment Variables in CMake . . . . . . . . . . . . . . . 473
13.3.3 Modifying Environment Variables in CMake . . . . . . . . . . . . . . 474
13.3.4 Managing Environment Variables in CMake Modules . . . . . . . . . . 475
13.3.5 Persistent Changes to Environment Variables . . . . . . . . . . . . . . 476
13.3.6 Best Practices for Managing Environment Variables . . . . . . . . . . 477
13.3.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
13.4 Improving Reusability of CMake Code . . . . . . . . . . . . . . . . . . . . . . 479
13.4.1 Introduction to Reusability in CMake . . . . . . . . . . . . . . . . . . 479
13.4.2 Modularizing CMake Code . . . . . . . . . . . . . . . . . . . . . . . . 480
13.4.3 Reusing Functions and Macros . . . . . . . . . . . . . . . . . . . . . . 482
19

13.4.4 Use of find package() for External Dependencies . . . . . . . . . 483


13.4.5 Using CMake Config Files for Reusability . . . . . . . . . . . . . . . . 484
13.4.6 Best Practices for Reusability . . . . . . . . . . . . . . . . . . . . . . 485
13.4.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 486

14 Cross-Platform Support and Compatibility 487


14.1 Writing Cross-Platform CMakeLists.txt for Windows, Linux, macOS . . . . . . 487
14.1.1 Overview of Cross-Platform CMake . . . . . . . . . . . . . . . . . . . 488
14.1.2 Basic Structure of CMakeLists.txt . . . . . . . . . . . . . . . . . . . . 488
14.1.3 Platform-Specific Handling in CMake . . . . . . . . . . . . . . . . . . 489
14.1.4 Handling File Paths . . . . . . . . . . . . . . . . . . . . . . . . . . . . 491
14.1.5 Cross-Platform Tools and Libraries . . . . . . . . . . . . . . . . . . . 492
14.1.6 CMake Configuration Options . . . . . . . . . . . . . . . . . . . . . . 493
14.1.7 Best Practices for Cross-Platform CMake . . . . . . . . . . . . . . . . 493
14.1.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 494
14.2 Handling Platform-Specific Library Differences (#ifdef WIN32, #ifdef
linux ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 495
14.2.1 Overview of Platform-Specific Code in C++ . . . . . . . . . . . . . . . 495
14.2.2 Using Preprocessor Directives for Platform-Specific Code . . . . . . . 496
14.2.3 Platform-Specific Libraries and APIs . . . . . . . . . . . . . . . . . . 497
14.2.4 Combining Platform-Specific Code with CMake . . . . . . . . . . . . 500
14.2.5 Managing External Dependencies with Platform-Specific Requirements 501
14.2.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
14.3 Building Multi-Platform Applications . . . . . . . . . . . . . . . . . . . . . . 503
14.3.1 Overview of Multi-Platform Development . . . . . . . . . . . . . . . . 503
14.3.2 Setting Up CMake for Multi-Platform Builds . . . . . . . . . . . . . . 504
14.3.3 Managing Multi-Platform Dependencies . . . . . . . . . . . . . . . . . 507
14.3.4 Platform-Specific Features and Optimizations . . . . . . . . . . . . . . 508
20

14.3.5 Continuous Integration (CI) for Multi-Platform Builds . . . . . . . . . 509


14.3.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510

15 CMake and Different Compilers 511


15.1 Working with GCC, Clang, and MSVC . . . . . . . . . . . . . . . . . . . . . . 511
15.1.1 Introduction to Compiler Support in CMake . . . . . . . . . . . . . . . 511
15.1.2 How CMake Detects and Selects a Compiler . . . . . . . . . . . . . . 512
15.1.3 Working with GCC (GNU Compiler Collection) . . . . . . . . . . . . 513
15.1.4 Working with Clang (LLVM) . . . . . . . . . . . . . . . . . . . . . . . 514
15.1.5 Working with MSVC (Microsoft Visual C++ Compiler) . . . . . . . . 515
15.1.6 Managing Cross-Compiler Compatibility . . . . . . . . . . . . . . . . 516
15.1.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 517
15.2 Configuring Compilation Flags (CMAKE CXX FLAGS) . . . . . . . . . . . . . 519
15.2.1 Introduction to Compilation Flags in CMake . . . . . . . . . . . . . . 519
15.2.2 Setting Global Compilation Flags . . . . . . . . . . . . . . . . . . . . 519
15.2.3 Setting Build-Type Specific Flags . . . . . . . . . . . . . . . . . . . . 520
15.2.4 Setting Compiler-Specific Flags . . . . . . . . . . . . . . . . . . . . . 521
15.2.5 Using target compile options() (Recommended) . . . . . . . 522
15.2.6 Using add compile definitions() for Preprocessor Flags . . . 523
15.2.7 Setting Linker Flags (CMAKE EXE LINKER FLAGS) . . . . . . . . . 523
15.2.8 Example: Complete CMakeLists.txt with Compiler-Specific Flags . . . 524
15.2.9 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 525
15.3 Checking Compiler Feature Support (CheckCXXCompilerFlag) . . . . . . 526
15.3.1 Introduction to Compiler Feature Checking . . . . . . . . . . . . . . . 526
15.3.2 Why Check Compiler Flag Support? . . . . . . . . . . . . . . . . . . . 526
15.3.3 Using check cxx compiler flag() . . . . . . . . . . . . . . . . 527
15.3.4 Applying Flag Checks in CMake . . . . . . . . . . . . . . . . . . . . . 528
15.3.5 Checking Flags for Specific Compilers . . . . . . . . . . . . . . . . . 529
21

15.3.6 Checking Compiler Features Instead of Flags . . . . . . . . . . . . . . 530


15.3.7 Alternative: CheckCXXCompilerFlag vs. try compile() . . . 531
15.3.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 532
15.4 Handling Compiler-Specific Errors and Warnings . . . . . . . . . . . . . . . . 533
15.4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533
15.4.2 Understanding Compiler Errors and Warnings . . . . . . . . . . . . . . 533
15.4.3 Enabling Stricter Warnings . . . . . . . . . . . . . . . . . . . . . . . . 534
15.4.4 Treating Warnings as Errors (-Werror, /WX) . . . . . . . . . . . . . 535
15.4.5 Handling Compiler-Specific Errors with Preprocessor Directives . . . . 536
15.4.6 Suppressing Unwanted Warnings . . . . . . . . . . . . . . . . . . . . 537
15.4.7 Using target compile options() vs. CMAKE CXX FLAGS . . 538
15.4.8 Example: Comprehensive Handling of Compiler Warnings . . . . . . . 539
15.4.9 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 540

16 Troubleshooting and Debugging CMake Issues 542


16.1 Understanding CMake Error Messages . . . . . . . . . . . . . . . . . . . . . . 542
16.1.1 The Structure of a CMake Error Message . . . . . . . . . . . . . . . . 543
16.1.2 Common Categories of CMake Errors . . . . . . . . . . . . . . . . . . 544
16.1.3 Debugging CMake Errors . . . . . . . . . . . . . . . . . . . . . . . . 546
16.1.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547
16.2 Debugging with message(STATUS) and message(DEBUG) . . . . . . . . 548
16.2.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 548
16.2.2 The message() Command in CMake . . . . . . . . . . . . . . . . . 548
16.2.3 Using message(STATUS) for Debugging . . . . . . . . . . . . . . . 549
16.2.4 Using message(DEBUG) for More Granular Debugging . . . . . . . 552
16.2.5 Best Practices for Using message(STATUS) and message(DEBUG) 553
16.2.6 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 554
16.3 Tracking Environment Variables and Build Settings . . . . . . . . . . . . . . . 556
22

16.3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 556


16.3.2 Tracking Environment Variables in CMake . . . . . . . . . . . . . . . 556
16.3.3 Tracking Build Settings in CMake . . . . . . . . . . . . . . . . . . . . 559
16.3.4 Debugging Using CMake Cache . . . . . . . . . . . . . . . . . . . . . 561
16.3.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 562
16.4 Solutions to Common CMake Errors . . . . . . . . . . . . . . . . . . . . . . . 564
16.4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 564
16.4.2 CMake Configuration Errors and Solutions . . . . . . . . . . . . . . . 564
16.4.3 Dependency and Package Errors . . . . . . . . . . . . . . . . . . . . . 566
16.4.4 Compiler and Linker Errors . . . . . . . . . . . . . . . . . . . . . . . 568
16.4.5 CMake Generator Errors . . . . . . . . . . . . . . . . . . . . . . . . . 570
16.4.6 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 571

Appendices 579
Appendix A: CMake Command Reference . . . . . . . . . . . . . . . . . . . . . . . 579
Appendix B: CMake Best Practices . . . . . . . . . . . . . . . . . . . . . . . . . . . 582
Appendix C: CMake Troubleshooting Guide . . . . . . . . . . . . . . . . . . . . . . 585
Appendix D: CMake Project Examples . . . . . . . . . . . . . . . . . . . . . . . . . 587
Appendix E: CMake Tools and Integrations . . . . . . . . . . . . . . . . . . . . . . 589

References 591
Author’s Introduction

One of the most challenging aspects of C++ programming is the compilation process. Unlike
many modern languages that come with built-in package managers and streamlined build
systems, C++ requires developers to have a deep understanding of compilation, linking,
dependency management, and platform-specific configurations. This complexity often
discourages students and even experienced developers, leading them to abandon C++ in favor of
languages with simpler build processes. However, while C++ may have a steep learning curve in
this regard, mastering the right tools can dramatically improve the development experience and
unlock the full potential of the language.
This is where CMake comes in. CMake is not just another build system; it is a powerful
meta-build tool that simplifies and standardizes the configuration, compilation, and linking
processes across multiple platforms and compilers. In large-scale projects, particularly those
targeting multiple operating systems (Windows, macOS, Linux) or architectures (x86, ARM,
embedded systems), managing build files manually can be overwhelming. CMake provides an
elegant solution by allowing developers to define their build processes in a
platform-independent manner, generating appropriate build scripts for a variety of compilers
and environments.
Many C++ programmers hesitate to learn CMake, thinking of it as an additional layer of
complexity rather than a solution. However, once you understand its workflow, CMake becomes
an indispensable tool that simplifies project setup, dependency management, and integration with
external libraries. Whether you are working on small projects or large-scale software systems,

23
24

using CMake can save you countless hours of manual configuration and troubleshooting.
In this book, I will guide you through the fundamentals of CMake, from basic setup to
advanced configurations. You will learn how to efficiently manage source files, handle
third-party dependencies, optimize compilation settings, and create robust cross-platform
builds. By the end of this journey, you will have a solid grasp of CMake, allowing you to focus
more on writing great C++ code rather than struggling with build issues.
I strongly encourage every C++ developer to invest time in mastering CMake. It is not just a
tool—it is an essential skill that will make your C++ development process more efficient,
scalable, and enjoyable.

Stay Connected
For more discussions and valuable content about Modern C++ Pointers, I invite you to follow
me on LinkedIn:
https://linkedin.com/in/aymanalheraki
You can also visit my personal website:
https://simplifycpp.org

Ayman Alheraki
Chapter 1

Introduction to CMake

1.1 Why Do You Need CMake?

1.1.1 Introduction
Software development, particularly in languages like C++, involves multiple stages, including
writing source code, compiling it into object files, linking those files to create an executable, and
finally deploying the application. As projects grow in complexity, managing these tasks
efficiently becomes increasingly difficult. While simple programs with a few source files can be
compiled manually using compiler commands, larger projects require automated build systems
to handle dependencies, multiple files, libraries, and different build configurations.
Historically, developers have relied on manual Makefiles, Autotools, and other
platform-specific build scripts to manage the compilation process. However, these traditional
methods come with significant limitations, particularly when it comes to cross-platform
compatibility, maintainability, and scalability.
CMake is a modern, flexible, and cross-platform build system generator that simplifies the
process of compiling, linking, and managing C++ projects. Instead of manually writing complex

25
26

and platform-dependent Makefiles or build scripts, developers define their project’s structure
in a simple and declarative manner using CMakeLists.txt, and CMake generates the appropriate
build system for the target platform.
This section explores the need for CMake by identifying the challenges associated with
traditional build systems and demonstrating how CMake provides a powerful solution.

1.1.2 The Challenges of Traditional Build Systems


1. Platform-Specific Build Scripts

One of the biggest challenges in software development is ensuring that a project can be
compiled and executed on multiple operating systems. Many projects need to support
Windows, Linux, macOS, and even embedded platforms.

When using traditional build methods, developers often have to write separate build scripts
for each platform:

• Windows: Batch scripts (.bat), PowerShell scripts (.ps1), or Visual Studio


project files

• Linux/macOS: GNU Makefiles (Makefile), shell scripts (.sh), or Autotools


configurations

Each of these build scripts is tailored to the specific operating system and compiler used.
A Makefile that works on Linux with GCC might not work on Windows without
modifications, and a Visual Studio project file cannot be easily ported to Linux. This
fragmentation leads to increased maintenance overhead and makes cross-platform
development cumbersome.

2. Complex Dependency Management


27

Modern C++ projects often rely on multiple external libraries, such as Boost, OpenCV,
Qt, Eigen, GLFW, and SQLite. Managing these dependencies manually presents several
challenges:

• Locating the library: Developers must specify the correct paths for headers and
compiled binaries.
• Handling different versions: Different systems may have different versions of a
library installed, leading to potential compatibility issues.
• Static vs. dynamic linking: Some projects require static linking, while others need
shared libraries, leading to different linking options.
• Managing transitive dependencies: A library may depend on other libraries,
complicating the linking process.

Traditional methods require developers to write complex shell scripts or pkg-config


configurations to handle these dependencies. Errors in locating or linking a dependency
can lead to frustrating build failures.

3. Lack of Maintainability and Scalability


As projects grow, managing a build system manually becomes increasingly difficult.
Adding new source files to a project means modifying existing Makefiles or build
scripts, which increases the risk of introducing errors.
For example, a manually written Makefile may require:

OBJS = main.o module1.o module2.o


CC = g++
CFLAGS = -Wall -O2

app: $(OBJS)
$(CC) $(CFLAGS) -o app $(OBJS)
28

Every time a new source file is added, it must be explicitly listed in OBJS, making
maintenance error-prone. Large projects with hundreds of source files require a more
dynamic and automated approach to handling build configurations.

4. Difficulty in Multi-Platform Builds

A project developed on Linux using Makefiles and GCC may not compile on Windows
without modification. Differences in compilers, library locations, and system APIs
require additional effort to ensure cross-platform compatibility.

Maintaining multiple separate build configurations for different operating systems


increases complexity and maintenance overhead. This issue is particularly relevant for
open-source projects where contributors may use different operating systems, requiring a
flexible and portable build system.

1.1.3 How CMake Solves These Challenges


CMake provides a high-level abstraction for defining build configurations, allowing developers
to describe what should be built rather than specifying how to build it. CMake then generates
the appropriate build system for the target platform, handling platform-specific details
automatically.

1. Cross-Platform Compatibility

CMake is designed to be platform-independent. A single CMakeLists.txt file can be


used to generate build configurations for multiple operating systems. CMake supports
various build generators, including:

• Makefiles (for Linux and macOS)

• Ninja (for fast parallel builds)

• Visual Studio project files (for Windows development)


29

• Xcode project files (for macOS development)

• MSBuild, NMake, and others

This allows developers to write once and build anywhere, eliminating the need for
maintaining multiple build scripts for different platforms.

2. Simplified Dependency Management

CMake provides built-in functionality for locating and integrating third-party libraries.
Instead of manually specifying library paths, developers can use:

• find package() – Automatically locates installed libraries such as OpenGL,


Boost, and Qt.

• FetchContent – Fetches external libraries and integrates them at build time.

• ExternalProject Add – Fetches and compiles external projects.

This streamlines dependency management and reduces the risk of version mismatches or
missing dependencies.

3. Automatic System Detection

CMake detects the compiler, available system libraries, and hardware capabilities
automatically. This allows developers to write portable build configurations without
worrying about platform-specific details.

For example, CMake can check for the presence of certain libraries and enable features
accordingly:

find_package(OpenGL REQUIRED)
find_package(Boost 1.71 REQUIRED)
30

If the required libraries are not found, CMake can provide meaningful error messages,
guiding users to install the necessary dependencies.

4. Scalability for Large Projects


CMake supports modular project structures, allowing large projects to be divided into
multiple subdirectories, each with its own CMakeLists.txt. This improves
maintainability and organization.
CMake also supports out-of-source builds, preventing source directories from being
cluttered with build artifacts.

1.1.4 Conclusion
CMake addresses the limitations of traditional build systems by providing a cross-platform,
maintainable, and scalable approach to building C++ projects. It simplifies dependency
management, multi-platform support, and automatic configuration detection, making it the
preferred choice for modern C++ development.
With CMake, developers can focus on writing code instead of dealing with the intricacies of
manually managing builds, dependencies, and platform-specific configurations.
31

1.2 CMake vs. Traditional Build Systems (Make, Autotools,


Ninja, etc.)

1.2.1 Introduction
The process of building software from source code involves compiling, linking, and organizing
dependencies to create an executable or library. As software projects grow in complexity,
managing the build process manually becomes inefficient and error-prone.
Traditionally, developers relied on build systems such as Make, Autotools, and Ninja to
automate the compilation process. However, these systems come with limitations in
cross-platform support, maintainability, and flexibility.
CMake was developed to overcome these limitations by providing a higher-level build
system generator that abstracts platform-specific complexities. Unlike traditional build systems
that require developers to write platform-specific scripts, CMake allows them to define their
projects once and generate the appropriate build files for multiple platforms and compilers.
This section provides an in-depth comparison between CMake and traditional build systems,
highlighting their strengths, weaknesses, and use cases.

1.2.2 Traditional Build Systems: Overview and Limitations


Before comparing CMake with other build systems, it is important to understand how traditional
build systems work and where they fall short.

1. Make (GNU Make)

Make is one of the earliest and most widely used build systems. It is primarily used in
Unix-like operating systems and relies on Makefiles to define build rules and
dependencies.
32

How Make Works


Make processes a Makefile, which specifies how to compile and link a program. The
Makefile contains:

• Targets: The files to be built (e.g., object files, executables).


• Dependencies: The source files required to build a target.
• Commands: The compilation and linking commands to execute.

Example Makefile for a Simple C++ Project

CC = g++
CFLAGS = -Wall -O2
OBJ = main.o module.o

app: $(OBJ)
$(CC) $(CFLAGS) -o app $(OBJ)

%.o: %.cpp
$(CC) $(CFLAGS) -c $< -o $@

To build the project, the user runs:

make

This compiles the source files and links them into an executable.

Advantages of Make

• Simple and widely available: Make is installed by default on most Unix-like


systems.
33

• Parallel execution: Supports multi-threaded builds using make -j.

• Customizability: Allows developers to define their own build rules.

Disadvantages of Make

• Platform-dependent: Make is Unix-centric and requires modifications to work on


Windows.

• Manual dependency tracking: Developers must explicitly list source files and
dependencies.

• Difficult to maintain: Large projects require complex Makefiles, making


maintenance difficult.

2. Autotools (GNU Autoconf, Automake, Libtool)

Autotools is a suite of tools designed to enhance portability across Unix-like systems by


automatically generating Makefiles based on system configuration.

How Autotools Works

Autotools follows a three-step process:

1. Autoconf (configure.ac) – Creates a configure script to detect


system-specific settings.

2. Automake (Makefile.am) – Generates platform-specific Makefiles.

3. Libtool – Handles shared and static library creation across different platforms.

To build a project using Autotools, the user runs:


34

./configure
make
make install

Advantages of Autotools

• Better portability: Generates system-specific Makefiles.


• Automatic feature detection: Checks compiler settings, libraries, and
dependencies.
• Standardized build process: Commonly used in open-source projects.

Disadvantages of Autotools

• Complex configuration: Requires multiple scripts and configuration files.


• Slow build process: Running ./configure can be time-consuming.
• Difficult Windows support: Primarily designed for Unix; requires additional tools
like MinGW or Cygwin for Windows compatibility.

3. Ninja
Ninja is a build system optimized for speed and efficiency. Unlike Make and Autotools,
which handle dependency resolution and build configuration, Ninja is designed purely for
executing build tasks as quickly as possible.
How Ninja Works
Ninja relies on a build.ninja file, which describes how source files should be
compiled and linked. However, developers do not write these files manually—they are
typically generated by higher-level tools like CMake or Meson.
35

Example Ninja Build File

rule compile
command = g++ -c $in -o $out
build main.o: compile main.cpp

Advantages of Ninja

• Extremely fast: Optimized for incremental builds.


• Parallel execution: Utilizes multiple CPU cores efficiently.
• Widely used in large projects: Used by Chromium and LLVM.

Disadvantages of Ninja

• Not standalone: Requires an external build generator like CMake.


• Less human-readable: Ninja build files are machine-generated and difficult to
modify manually.

1.2.3 How CMake Compares to Traditional Build Systems


CMake is fundamentally different from traditional build systems because it is a build system
generator rather than a build system itself. Instead of managing the build process directly,
CMake generates platform-specific build files for Make, Ninja, Visual Studio, and others.

1. Key Advantages of CMake


36

Feature Make Autotools Ninja CMake


Cross-platform Limited Unix- Limited Full support (Windows,
support focused Linux, macOS)
Dependency Manual Checks for None Built-in
management libraries (find package(),
FetchContent)
Multi-platform No No No Supports Make, Ninja,
build generation Visual Studio, Xcode
Automatic No Yes No Detects compiler, OS,
system detection libraries
Ease of use Medium Complex Requires Simple
generator CMakeLists.txt
Scalability for Hard Hard Fast but Supports modularization
large projects manual (add subdirectory())
Build speed Slow Slow Very fast Fast, supports Ninja

2. Example of a CMake Build System

A simple CMakeLists.txt replaces complex Makefiles:

cmake_minimum_required(VERSION 3.16)
project(MyApp)

add_executable(MyApp main.cpp)

To build the project:


37

cmake -S . -B build
cmake --build build

CMake automatically detects the compiler, generates the appropriate build system
(Makefiles, Ninja, Visual Studio), and compiles the project efficiently.

1.2.4 Conclusion
While traditional build systems like Make, Autotools, and Ninja have been widely used, they
come with limitations in portability, dependency management, and maintainability.
CMake provides a modern, flexible, and cross-platform solution that simplifies the build
process by generating native build files for different systems. With its ability to handle
dependencies, detect system configurations, and support multiple build backends, CMake
has become the industry standard for managing C++ projects.
38

1.3 Installing CMake on Different Operating Systems


(Windows, Linux, macOS)

1.3.1 Introduction
CMake is a powerful and flexible build system generator that plays a crucial role in simplifying
the process of building C++ projects across different platforms. The installation process is
straightforward, but due to the variety of operating systems and user preferences, there are
multiple methods to install it. This section will provide a detailed guide on how to install CMake
on the most widely used operating systems: Windows, Linux, and macOS.
The installation process for CMake can involve precompiled binary installers, package
managers, or even manual compilation from source. The method chosen depends on the
user's specific needs, such as ensuring the latest version, ease of use, or whether the user prefers
a command-line or graphical interface for installation.
Regardless of the installation method, once CMake is installed, it will allow you to generate
build files for various platforms, manage dependencies, and enable a seamless integration with a
variety of development tools. This section will walk you through each installation method for
different operating systems, and provide verification steps to ensure CMake is installed and
functioning correctly.

1.3.2 Installing CMake on Windows


Windows provides several methods for installing CMake, including using an official installer,
package managers such as Chocolatey, or manual methods via Scoop. Here’s a breakdown of
each method.

1. Using the Official CMake Installer

One of the easiest and most common methods for installing CMake on Windows is by
39

using the official CMake installer provided by Kitware, the creators of CMake. This
method ensures that you have the latest stable version of CMake, and it allows for an easy
installation process with a graphical user interface (GUI).

• Step 1: Download the Installer

1. Go to the official CMake website:


https://cmake.org/download/
2. Download the Windows installer for your system architecture (either x64 or
x86). Generally, x64 is the most common for modern systems.

• Step 2: Run the Installer

1. After the download is complete, double-click the .msi file to launch the
CMake installation wizard.
2. During the installation, you will be presented with different options. The key
option is to add CMake to the system PATH. This is a critical step because it
allows you to run CMake from the command line (Command Prompt or
PowerShell) without needing to specify its full path. You can select the option
”Add CMake to the system PATH for all users”.
3. Proceed with the default installation settings and click Next through the
installation wizard until the installation is complete.

• Step 3: Verify the Installation


Once the installation is complete, it is important to verify that CMake has been
installed correctly.

1. Open Command Prompt or PowerShell.


2. Type the following command to check the CMake version:
40

cmake --version

If CMake has been installed correctly, you should see the version of CMake
displayed in the terminal, similar to:

cmake version 3.x.x


CMake suite maintained and supported by Kitware
,→ (https://cmake.org).

If you see this output, CMake has been installed and is ready to use.

• Installing CMake via Chocolatey (Alternative Method)


Chocolatey is a package manager for Windows, and it provides a simple way to
install software through the command line. If you have Chocolatey installed, you can
install CMake using the following steps.

• Step 1: Install Chocolatey


If you don’t have Chocolatey installed yet, open PowerShell as Administrator and
run the following command to install Chocolatey:

Set-ExecutionPolicy Bypass -Scope Process -Force;


,→ [System.Net.ServicePointManager]::SecurityProtocol =
,→ [System.Net.ServicePointManager]::SecurityProtocol -bor 3072;
,→ iex ((New-Object
,→ System.Net.WebClient).DownloadString('https://community.chocolatey.or

• Step 2: Install CMake Using Chocolatey


Once Chocolatey is installed, you can install CMake with a single command:
41

choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'

This will automatically install CMake and add it to your system’s PATH so that you
can use it from the command line.

• Step 3: Verify the Installation


After the installation completes, open Command Prompt or PowerShell and run:

cmake --version

You should see the version of CMake that was installed.

• Installing CMake via Scoop (Alternative Method)


Scoop is another command-line-based package manager for Windows that allows
users to install software easily. If you prefer using Scoop, follow these steps.

• Step 1: Install Scoop


Open PowerShell and run the following command to install Scoop:

iwr -useb get.scoop.sh | iex

• Step 2: Install CMake Using Scoop


Once Scoop is installed, you can install CMake by running:

scoop install cmake

• Step 3: Verify the Installation


After the installation is complete, verify it by running:
42

cmake --version

1.3.3 Installing CMake on Linux


Linux distributions offer different ways to install CMake. You can use package managers for
quick installations, or if you want to ensure you’re getting the latest version, you can compile
CMake from source.

1. Installing CMake via Package Managers

• Ubuntu/Debian-based Distributions
If you're using a Debian-based distribution such as Ubuntu, CMake can easily be
installed using the APT package manager:

sudo apt update


sudo apt install cmake

Once the installation completes, you can verify it by running:

cmake --version

• Fedora-based Distributions
For Fedora or similar distributions, the DNF package manager is used:

sudo dnf install cmake

You can check the installation with:


43

cmake --version

• Arch Linux (Manjaro, EndeavourOS, etc.)


If you're using Arch Linux or an Arch-based distribution like Manjaro, you can
install CMake using the Pacman package manager:

sudo pacman -S cmake

Verify it using:

cmake --version

• Installing CMake from Source (For All Linux Distributions)


Package managers often provide older versions of CMake. If you need the latest
version, you can compile CMake from source.
– Step 1: Download CMake Source
Visit the official CMake GitHub repository or the CMake website to download
the latest source code. Use the following command to download the source:

wget
,→ https://github.com/Kitware/CMake/releases/latest/download/cmake-3

Extract the downloaded file:

tar -xvzf cmake-3.x.x.tar.gz


cd cmake-3.x.x

– Step 2: Compile and Install CMake


First, you need to prepare the environment:
44

./bootstrap

Next, compile the source code:

make -j$(nproc)

Finally, install CMake on your system:

sudo make install

– Step 3: Verify Installation


Once the installation is complete, check that CMake was installed successfully:

cmake --version

1.3.4 Installing CMake on macOS


macOS offers multiple methods for installing CMake, including Homebrew, MacPorts, or
manual installation via a graphical installer.

1. Installing CMake via Homebrew (Recommended)

Homebrew is the most popular package manager for macOS, and it simplifies software
installation.

• Step 1: Install Homebrew


If Homebrew is not already installed, you can install it by running the following
command in Terminal:
45

/bin/bash -c "$(curl -fsSL


,→ https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

• Step 2: Install CMake Using Homebrew


After Homebrew is installed, you can install CMake with the following command:

brew install cmake

• Step 3: Verify the Installation


Once installed, check the version of CMake:

cmake --version

• Installing CMake via MacPorts


MacPorts is an alternative package manager for macOS that can also be used to
install CMake.

• Step 1: Install MacPorts


Download and install MacPorts from the official website:
https://www.macports.org/install.php
• Step 2: Install CMake Using MacPorts
After installing MacPorts, use the following command to install CMake:

sudo port install cmake

• Step 3: Verify the Installation


You can check the installation by running:
46

cmake --version

• Installing CMake via the Official macOS Installer


If you prefer a GUI-based approach, CMake also offers an official .dmg installer for
macOS.

• Step 1: Download the Installer

1. Visit the official CMake website:


https://cmake.org/download/
2. Download the macOS Universal Binary (.dmg) file.

• Step 2: Install CMake

1. Open the .dmg file and drag the CMake.app into the /Applications folder.
2. To enable command-line usage, open CMake.app, go to Tools > How to
Install For Command Line Use, and follow the steps provided.

• Step 3: Verify the Installation


After completing the steps, verify the installation by running:

cmake --version

1.3.5 Conclusion
The installation of CMake varies depending on the operating system being used, but regardless
of the method, CMake provides the necessary tools to streamline and automate the build process
for C++ projects. Whether you use an installer, a package manager, or build from source, the
goal remains the same: to ensure that CMake is set up properly so that you can manage and
configure your project builds across various platforms.
47

Once CMake is installed successfully, you can begin using it to generate platform-specific build
files, configure your project, and manage complex builds in a consistent and efficient manner. In
the next section, we will explore CMake’s basic commands and structure, which will lay the
groundwork for mastering CMake in the context of real-world projects.
48

1.4 Verifying CMake Installation and Running It

1.4.1 Introduction
After you have installed CMake on your system, it is essential to ensure that the installation was
successful and that CMake is functioning properly. This process is vital because any issues with
the installation or configuration of CMake could lead to problems when building C++ projects.
Verifying CMake’s installation helps to confirm that the required binaries, environment variables,
and necessary configuration files have been set up correctly.
CMake is a powerful build system generator, and if installed and configured properly, it should
work seamlessly across various platforms like Windows, Linux, and macOS. This section walks
you through how to verify your CMake installation and offers guidance on how to run it for the
first time.

1.4.2 Verifying CMake Installation


After installation, you need to confirm that CMake has been correctly installed and can be
accessed from the command line interface (CLI). The easiest way to verify the installation is by
checking its version. This ensures that CMake is available in your system’s PATH and that the
correct version is installed. Let’s go over the steps to check for CMake’s version across different
operating systems.

1. Checking the Version of CMake

1. On Windows:

• Open the Command Prompt (cmd) or PowerShell window.


• Type the following command:
49

cmake --version

• If CMake has been installed successfully, you will see the version number of
CMake. For example:

cmake version 3.x.x


CMake suite maintained and supported by Kitware
,→ (https://cmake.org).

This indicates that CMake is installed and ready to use. If you encounter an error
message such as ”command not found” or ”CMake is not recognized as an internal
or external command,” this suggests that either the installation has failed or the
system’s PATH environment variable is not set correctly.
2. On Linux/macOS:
• Open a Terminal window.
• Run the following command:

cmake --version

• If CMake is correctly installed, you should see an output similar to the one on
Windows:

cmake version 3.x.x

If you see an error indicating that cmake is not found, you may need to recheck the
installation process and ensure that the cmake binary is properly linked to your
system’s PATH.

2. Troubleshooting CMake Installation


50

If you receive an error message indicating that the cmake command cannot be found,
here are some troubleshooting steps:

• Ensure CMake is Added to PATH: When you install CMake, it is important to add
the CMake executable to your system’s PATH environment variable. If this step was
missed during installation, you can manually add CMake to your PATH:

– On Windows: You can add CMake to the PATH through the Environment
Variables settings. To do this, go to System Properties > Advanced >
Environment Variables, then edit the System PATH and add the directory
where CMake is installed (e.g., C:\Program Files\CMake\bin).
– On Linux/macOS: Open the shell configuration file (.bashrc or .zshrc,
depending on your shell) and add the following line to include CMake in your
PATH:

export PATH="/path/to/cmake/bin:$PATH"

After editing the file, run source ˜/.bashrc or source ˜/.zshrc to


apply the changes.

• Verify Installation Location: Ensure that CMake was installed in the correct
directory. If you installed it via a package manager, it may have been installed in a
non-standard directory, especially on Linux or macOS. Verify that the installation
path is valid and contains the cmake binary.

• Reinstall CMake: If none of the above solutions work, you may need to reinstall
CMake. Be sure to follow the installation instructions carefully to avoid errors, and
make sure to include the option to add CMake to the system PATH during the
installation.
51

1.4.3 Running CMake for the First Time


Once you have confirmed that CMake is properly installed and the version is correctly displayed,
the next step is to test CMake by running it on a simple project. This will help you verify that
CMake is functioning as expected and that it can generate build files for your C++ projects.

1. Setting Up a Simple C++ Project for Testing

Before you can run CMake, it is best to set up a basic C++ project. This allows you to test
CMake’s functionality by creating a minimal project and using CMake to generate the
necessary build files. The following example demonstrates a very simple C++ program
and how to use CMake with it.

1. Create a Project Directory: First, create a directory to house the project files. Open
a terminal or command prompt and create a new directory:

mkdir MyTestProject
cd MyTestProject

2. Create a Simple C++ Source File: Inside the MyTestProject directory, create
a simple C++ source file named main.cpp:

// main.cpp
#include <iostream>

int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}
52

3. Create the CMakeLists.txt Configuration File: Now, create the


CMakeLists.txt file in the same directory, which will tell CMake how to build
your project. Create a file called CMakeLists.txt with the following content:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.0)

project(MyTestProject)

add_executable(MyTestProject main.cpp)

In this example:
• cmake minimum required(VERSION 3.0) specifies that the project
requires at least version 3.0 of CMake.
• project(MyTestProject) defines the project name as
MyTestProject.
• add executable(MyTestProject main.cpp) tells CMake to
generate an executable named MyTestProject from the main.cpp source
file.

This configuration file is very basic, but it covers the essential elements of a typical
CMake project.

2. Run CMake to Generate Build Files


Now that you have a basic project and a CMakeLists.txt file, it's time to run CMake
to generate the build system files (such as Makefiles or Visual Studio project files).

1. Create a Build Directory: To keep the build files separate from the source code, it
is common practice to create a separate build directory. Create a new directory inside
your project folder called build:
53

mkdir build
cd build

2. Run CMake to Configure the Project: Inside the build directory, run the
following CMake command to configure your project:

cmake ..

This command tells CMake to look for the CMakeLists.txt file in the parent
directory (..) and generate the build system files based on the configuration in that
file. If everything is set up correctly, CMake will display output showing the
configuration process, where it detects the system’s compilers, checks for required
tools, and prepares the necessary build files.
The output will look something like:

-- The C compiler identification is GNU 9.3.0


-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/gcc
-- Check for working CXX compiler: /usr/bin/g++
-- Configuring done
-- Generating done
-- Build files have been written to: /path/to/MyTestProject/build

If there are any errors during this process, CMake will output detailed messages that
can help you diagnose the issue. Common issues include missing dependencies,
incorrect file paths, or unsupported compilers.

3. Building the Project


54

Once the configuration step is complete, you can now build the project. CMake has
generated the necessary build files, so you can use the appropriate build tool to compile
the code.

1. On Linux/macOS (Using Makefiles): If CMake generated Makefiles for your


system, use the following command to compile the project:

make

This command will invoke the build system (Make) to compile the main.cpp file
and generate the executable MyTestProject.

2. On Windows (Using Visual Studio Project Files): If you are using Windows and
CMake generated Visual Studio project files, you can open the .sln file generated
by CMake and build the project directly in Visual Studio. Alternatively, you can use
the MSBuild command to build from the command line:

MSBuild MyTestProject.sln

4. Running the Executable

Once the build completes successfully, you can run the executable generated by CMake.

• On Linux/macOS: In the terminal, run:

./MyTestProject

The program should output:


55

Hello, CMake!

• On Windows: If you are using Visual Studio or the command line, you can simply
run the MyTestProject.exe executable.

1.4.4 Troubleshooting Common Issues

While running CMake for the first time, there are a few common issues that you might
encounter:

• Missing or Incorrect Compiler: CMake requires a working C++ compiler to build your
project. If CMake cannot detect a valid compiler, it will show an error. Make sure that a
C++ compiler (like GCC, Clang, or MSVC) is properly installed and accessible. On
Linux/macOS, you can check the installed compiler version using gcc --version or
clang --version. On Windows, make sure the Visual Studio build tools are
installed correctly.

• Permissions Issues: On Linux and macOS, you may encounter permission-related errors
if you do not have write access to certain directories. Ensure that you are running CMake
with the appropriate permissions or try running with sudo if necessary.

• CMake Cache Conflicts: CMake caches configuration data to avoid reprocessing the
same information multiple times. However, if you make changes to the project structure or
the CMakeLists.txt file, the cached configuration may cause issues. You can delete
the CMakeCache.txt file in your build directory and rerun the CMake command to
clear the cache.
56

1.4.5 Conclusion
Verifying CMake installation and running it for the first time is a crucial step in setting up your
development environment. By checking the CMake version and running it on a simple C++
project, you can ensure that everything is working as expected. If you encounter issues,
troubleshooting steps such as checking the system PATH, verifying the compiler installation, or
clearing the CMake cache can help resolve common problems. Once CMake is verified and
working, you can proceed to more advanced topics, such as configuring larger projects and
utilizing CMake’s advanced features to improve your build system and project management
workflow.
57

1.5 Your First CMake Project

1.5.1 Introduction
The previous sections provided an understanding of the importance of CMake, how it differs
from traditional build systems, and how to install it on various operating systems. Now, it’s time
to take the next step and create your very first CMake project. This hands-on guide will walk
you through the entire process, from setting up a simple C++ program to configuring the
necessary build files, and ultimately compiling and running the project. By the end of this
section, you will be comfortable with the fundamental aspects of working with CMake, which
will form the foundation for tackling more advanced CMake features in subsequent sections.
Creating a project with CMake is straightforward and involves defining how your source code is
compiled and linked, specifying compiler options, and ensuring that CMake generates the
correct build system files for your chosen platform. With CMake, the complexity of build
system generation is abstracted away, which saves time and reduces the possibility of errors in
managing project configurations.

1.5.2 Setting Up a Simple C++ Project


To begin, you will create a minimal C++ project using CMake. This will include:

1. A basic C++ source file.

2. A CMakeLists.txt configuration file.

3. Building the project using CMake-generated files.

1. Project Structure
The basic structure for your project will include the source code and a CMake
configuration file. Start by creating the project directory on your local machine. The
directory should look like this:
58

MyFirstCMakeProject/
CMakeLists.txt
main.cpp

Here’s what each part of this structure represents:

• CMakeLists.txt: This file is the heart of CMake. It contains instructions that CMake
uses to configure the build process. It defines things like which source files to
compile, which compiler options to use, and how to organize the output.

• main.cpp: This is your source code, containing the C++ code that will be compiled
into an executable.

2. Writing the C++ Code (main.cpp)

Let’s start by writing the code for the C++ program. Create the main.cpp file inside the
MyFirstCMakeProject directory. Here is a simple “Hello World” program to begin
with:

// main.cpp
#include <iostream>

int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}

This is a basic C++ program that prints ”Hello, CMake!” to the console. It contains the
minimal code needed to ensure the program compiles successfully and serves as a simple
test case for understanding how CMake is used to build a C++ project.
59

1.5.3 Creating the CMakeLists.txt File


Next, you need to create the CMakeLists.txt file. This is the configuration file that CMake
will use to configure the project. It tells CMake how to process the source code and generate the
build files for compiling and linking the project.

1. Create a file named CMakeLists.txt in the root of your project directory.

2. Inside CMakeLists.txt, add the following code:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

# Define the project


project(MyFirstCMakeProject)

# Add the executable target with the main.cpp file


add_executable(MyFirstCMakeProject main.cpp)

Let’s break down each part of this CMakeLists.txt file:

• cmake minimum required(VERSION 3.10): This line specifies the minimum


version of CMake that is required to configure and build the project. It’s important to use
an appropriate version of CMake to ensure compatibility with all CMake features.

• project(MyFirstCMakeProject): This defines the name of the project, which


will also be used to name the executable created by CMake.

• add executable(MyFirstCMakeProject main.cpp): This is the crucial


instruction that tells CMake to generate an executable named
MyFirstCMakeProject from the source file main.cpp. This line specifies that
main.cpp is the source code to be compiled into the executable.
60

This is the simplest form of a CMakeLists.txt file, but as your projects become more
complex, this file will grow to include other configurations, such as libraries, dependencies,
compiler flags, etc.

1.5.4 Configuring the Build System with CMake


Now that you have your project set up, it’s time to configure the build system. This involves
using CMake to generate the build files needed for compiling the project. To keep things
organized, it is best practice to create a separate build directory. This prevents the project
directory from getting cluttered with generated files.

1. Creating a Separate Build Directory

1. Navigate to your project’s root directory (where CMakeLists.txt is located).


2. Inside your project directory, create a new directory called

build

mkdir build
cd build

This will be the directory where all the build-related files will be placed, such as Makefiles
or Visual Studio project files.

2. Running CMake to Generate Build Files


Now that you have the build directory, run the following command from within the
build directory to configure the project:
61

cmake ..

This command tells CMake to look for the CMakeLists.txt file in the parent
directory (..) and configure the project according to the specifications in that file.

Upon running the command, CMake will inspect the environment and attempt to detect
which compiler and tools to use for building the project. After processing the
CMakeLists.txt file, it will generate the necessary files for the build system. On a
typical system, this could include Makefiles, Visual Studio project files, or Ninja files,
depending on your platform.

Example output from the command might look like:

-- The C compiler identification is GNU 9.3.0


-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/gcc
-- Check for working CXX compiler: /usr/bin/g++
-- Configuring done
-- Generating done
-- Build files have been written to:
,→ /path/to/MyFirstCMakeProject/build

At this point, CMake has successfully configured the project and written the necessary
build files in the build directory. These build files describe the process CMake will use
to compile your project.

1.5.5 Building the Project


With the build files generated, you can now compile your project using the generated build
system.
62

1. Building on Linux/macOS with Make

If the build system is configured to use Make, which is common on Linux and macOS
systems, you can compile the project by running the following command:

make

This will invoke the make utility, which reads the generated Makefile and begins the
process of compiling your project. Make will compile the main.cpp source file into an
object file and link it to create the final executable.

The output will indicate the progress of the build process, and once the build is complete,
you should see something like:

[100%] Built target MyFirstCMakeProject

This means the build was successful, and the MyFirstCMakeProject executable has
been created.

2. Building on Windows with Visual Studio

If you’re on a Windows system and CMake generated Visual Studio project files, you have
the option to either open the generated .sln solution file in Visual Studio and build the
project through the IDE or use the command line.

To build the project from the command line, use MSBuild as follows:

MSBuild MyFirstCMakeProject.sln

This command tells MSBuild to use the Visual Studio build system to compile and link
the project. After the build completes, you will have an executable ready to run.
63

1.5.6 Running the Executable


Once the build completes successfully, it’s time to run the executable and see the output.

1. Running on Linux/macOS

On Linux and macOS, the executable is typically located in the build directory. To run
the executable, use the following command:

./MyFirstCMakeProject

This will execute the program, and you should see the output:

Hello, CMake!

This confirms that the build was successful, and your program has run as expected.

2. Running on Windows

If you are using Visual Studio, you can run the executable directly from the IDE by
pressing the ”Start” button. Alternatively, after building the project, you can navigate to
the Debug or Release folder (depending on your build configuration) and double-click
the executable MyFirstCMakeProject.exe to run it.

1.5.7 Understanding the Build Process


To understand how the whole process fits together, let’s review the steps involved when you run
CMake and build the project.

1. CMake Configuration: When you run cmake .., CMake reads the
CMakeLists.txt file in the parent directory. It checks for the system’s environment,
64

such as the available compiler and toolchain, and configures the project according to the
options specified in the CMakeLists.txt file.

2. Build Generation: CMake generates build files that describe how to compile and link the
project. This could be Makefiles, Visual Studio project files, or Ninja files, depending on
your platform and configuration.

3. Compilation: When you run make or MSBuild, the build system compiles your source
code files into object files and links them to form the final executable.

4. Execution: Once the executable is built, you can run it and see the output. If all the steps
are followed correctly, you should see your program’s output displayed on the terminal or
IDE.

1.5.8 Conclusion
In this section, you learned how to create a basic CMake project, write the necessary C++ code,
configure the project using the CMakeLists.txt file, and then build and run the project. You
should now understand the basic workflow involved in working with CMake and be able to
create simple projects.
This foundational knowledge will serve as a springboard for diving into more advanced CMake
features, such as managing external libraries, building multi-target projects, handling
dependencies, and customizing build options. Understanding the basics of how CMake
configures and generates build files is crucial to unlocking the full potential of CMake in larger,
more complex projects.
Chapter 2

Fundamentals of CMakeLists.txt

2.1 Understanding the Structure of CMakeLists.txt


The CMakeLists.txt file is the cornerstone of every CMake-based project. It serves as the
configuration script that CMake uses to generate build files, such as Makefiles or project files for
IDEs like Visual Studio, Xcode, or others. Understanding the structure of this file is critical to
mastering CMake and efficiently managing your build process.
This section will explore the various components of the CMakeLists.txt file, including its
organization, commands, and how different sections work together to define the build process
for a project. We will go over everything from simple declarations to advanced usage in
multi-directory projects, allowing you to use CMake to its full potential.

2.1.1 Introduction to CMakeLists.txt


The CMakeLists.txt file contains a series of CMake commands that specify how the
project should be built. These commands define everything from the minimum required version
of CMake to the actual files that will be compiled and linked into executables and libraries. The

65
66

beauty of CMake lies in its flexibility and portability; once a project is set up correctly, the same
CMakeLists.txt file can generate build files for different platforms without any
modification.
A simple project might only have one CMakeLists.txt file located at the root of the project
directory. However, for larger projects with multiple modules or libraries, each directory might
have its own CMakeLists.txt file that CMake will read recursively.
Here is an example of the simplest CMakeLists.txt file, which declares a minimum version
of CMake, defines a project, and specifies an executable target:

cmake_minimum_required(VERSION 3.10)

project(MyProject)

add_executable(MyExecutable main.cpp)

This minimal setup creates a project called MyProject and an executable named
MyExecutable, built from the source file main.cpp.

2.1.2 Key Sections of a CMakeLists.txt File


A well-structured CMakeLists.txt file usually follows a consistent pattern, starting with
some basic setup, followed by project-specific configuration, and ending with target definitions
and external dependencies. We will now go over the different sections you may encounter in a
typical CMakeLists.txt file.

1. Minimum CMake Version Declaration

Every CMakeLists.txt file begins with the declaration of the minimum version of
CMake required to process it. This is essential because different versions of CMake may
support different sets of features, syntax, or functionality. By specifying the minimum
67

version, you ensure that the build system will only be configured using a version of
CMake that is compatible with your project's requirements.

For example:

cmake_minimum_required(VERSION 3.10)

This line tells CMake that the project requires at least version 3.10 of CMake to work
correctly. If the user tries to configure the project with an older version of CMake, an error
will be generated.

2. Project Declaration

The project() command is another foundational element of a CMakeLists.txt


file. This command declares the project name, version, and optionally, the programming
languages that the project uses. It is often one of the first commands that appear after the
cmake minimum required() declaration.

For instance, the following line declares a project named MyProject that uses the C++
language:

project(MyProject VERSION 1.0 LANGUAGES CXX)

In this case:

• MyProject is the project name.

• VERSION 1.0 declares the project version (although version numbers are often
omitted for smaller projects).

• LANGUAGES CXX specifies that the project uses the C++ language. This is optional
in CMake 3.0 and later, as CMake automatically assumes C and C++.
68

3. Build Configuration

This section defines various settings and configuration variables that affect the overall
build process. The settings in this section can include the programming language
standards, compiler flags, and whether to use debug or release builds.

For example:

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_BUILD_TYPE Debug)

• set(CMAKE CXX STANDARD 17) ensures that the C++17 standard is used for
compiling C++ code. This is equivalent to passing the -std=c++17 flag to the
compiler.

• set(CMAKE BUILD TYPE Debug) specifies that the project should be built in
the Debug configuration, which will include debugging information and disable
optimizations.

Additionally, other build configuration settings can include enabling/disabling certain


features, adjusting optimizations, or controlling platform-specific settings.

4. Defining Executables and Libraries

One of the most important parts of a CMakeLists.txt file is the definition of


executables and libraries. These are the actual targets that will be compiled and linked by
the build system.

• Executables: To define an executable, the add executable() command is used.


This command tells CMake to create an executable target and specifies the source
files needed to compile it.
69

For example:

add_executable(MyExecutable src/main.cpp)

In this case, MyExecutable is the name of the executable, and src/main.cpp is the
source file that will be compiled into it.

• Libraries: Similarly, to create a library, the add library() command is used.


You can create both static and shared libraries. By default, the add library()
command creates a static library, but you can specify SHARED or MODULE for
dynamic/shared libraries.

For example:

add_library(MyLibrary SHARED src/my_library.cpp)

Here, MyLibrary is a shared library built from the src/my library.cpp file.

5. Target Properties and Dependencies

Once the targets (executables or libraries) are defined, you can modify their properties,
link them with other libraries, and define include directories. CMake provides commands
like target include directories(), target link libraries(), and
target compile options() to achieve this.

• Include Directories: To specify additional directories where the compiler should


look for header files, use the target include directories() command.

Example:
70

target_include_directories(MyExecutable PRIVATE
,→ ${PROJECT_SOURCE_DIR}/include)

This command tells CMake to include the include directory, which is located at the
root of the project (${PROJECT SOURCE DIR} is a CMake variable that holds the root
project directory).

• Linking Libraries: The target link libraries() command links the target
with other libraries. In this case, we are linking MyExecutable with
MyLibrary.

Example:

target_link_libraries(MyExecutable PRIVATE MyLibrary)

This makes sure that MyExecutable will be linked with MyLibrary when it is built.

• Compiler Options: You can also set compiler-specific options for individual targets
using target compile options().

Example:

target_compile_options(MyExecutable PRIVATE -Wall)

This would enable all compiler warnings for MyExecutable.

6. Handling External Dependencies


CMake makes it easy to manage external dependencies, such as third-party libraries or
tools. You can use commands like find package() to search for pre-installed libraries
71

or ExternalProject to fetch and build external projects during the configuration


process.

For example, to find and link the Boost library, you would use the find package()
command as follows:

find_package(Boost REQUIRED)
target_link_libraries(MyExecutable Boost::Boost)

In this example, find package(Boost REQUIRED) searches for the Boost library
and ensures it is found. If Boost is not found, CMake will stop with an error. The
target link libraries() command then links Boost::Boost to
MyExecutable.

7. Handling Multiple Directories and Projects

For large projects, you may want to break the project into smaller, manageable
submodules. CMake allows you to include other CMakeLists.txt files from
subdirectories by using the add subdirectory() command.

Example:

add_subdirectory(lib)
add_subdirectory(app)

In this example, CMake will process the CMakeLists.txt files in the lib and app
subdirectories. Each of these directories can have its own targets and build configuration,
making it easier to modularize the build process. The main CMakeLists.txt file
remains clean and high-level, delegating the detailed configuration to these subdirectories.

8. Comments and Documentation


72

While not a functional part of the build process, comments are extremely important for
documenting the CMakeLists.txt file. CMake allows single-line comments using the
# symbol, and multiline comments can be handled by using an if() block with
endif().

For example:

# This is a simple comment in CMake

For more complex explanations:

if(FALSE)
# This block is not executed, but useful for documentation
endif()

Comments can clarify the purpose of certain sections or describe why specific options are
used, helping future developers (or yourself) understand the rationale behind the
configuration.

2.1.3 Best Practices for Organizing CMakeLists.txt

• Minimal Root CMakeLists.txt: Keep the root CMakeLists.txt minimal and


high-level. Focus on defining the project and calling add subdirectory() for
modules or libraries.

• Avoid Hardcoding Paths: Instead of hardcoding paths, use variables and CMake’s
built-in path handling functions to make your project portable. This ensures your build
configuration works across different environments and operating systems.
73

• Use Variables Wisely: Define and use variables to store file paths, flags, and other
project-specific settings. This makes the configuration more flexible and easier to
maintain.

• Modularize Large Projects: For large projects, break them into smaller subprojects and
use add subdirectory() to include these submodules in the build process. This
keeps your CMakeLists.txt files clean and modular.

• Write Clear and Descriptive Comments: It’s important to explain complex sections of
the CMakeLists.txt file. A well-commented file makes it easier to understand the
build process, especially when dealing with large or complex projects.

2.1.4 Conclusion
The CMakeLists.txt file is an essential part of every CMake-based project. Understanding
its structure and commands gives you the power to manage and customize the build process for
your C++ projects. By organizing the file into well-defined sections—such as setting up the
minimum CMake version, declaring the project, defining targets, handling dependencies, and
configuring the build—you ensure that your project is flexible, maintainable, and portable.
With the knowledge from this section, you should be able to write and understand the basic
structure of a CMakeLists.txt file, setting you on the path toward becoming proficient in
CMake and mastering your C++ build system.
74

2.2 Defining the Minimal CMake Project


In this section, we will go through the essential steps required to define a minimal CMake project.
A minimal CMake project is the simplest form of a project that can be built using CMake. It is
useful as an introductory example for beginners and as a baseline for more complex projects as
you begin to incorporate additional CMake functionality. Understanding this minimal structure
will give you a solid foundation for working with more advanced features in later sections.

2.2.1 What Makes a CMake Project ”Minimal”?


A minimal CMake project contains only the essential components required for building a basic
executable. It avoids unnecessary complexities and focuses on the core elements needed to
configure a CMake-based build system. The components include:

1. Minimum required version of CMake.

2. Project declaration (defining the project’s name and version).

3. Defining an executable target.

4. Specifying source files.

5. Basic configuration (such as setting the C++ standard).

This structure is sufficient to compile and link a simple C++ program. Once these basic elements
are understood, you can gradually extend the configuration to handle more complex tasks like
linking external libraries, defining multiple targets, or creating shared/static libraries.
Let’s begin by examining the key components and how to define them in a minimal CMake
project.
75

2.2.2 CMake File Structure


A minimal CMake project typically has the following folder structure:

/MyMinimalProject
CMakeLists.txt
main.cpp

• CMakeLists.txt: The configuration file used by CMake to define the build instructions.

• main.cpp: A simple source file that will be compiled into an executable.

2.2.3 CMakeLists.txt File for the Minimal Project


The CMakeLists.txt file in the minimal project is simple but contains all the necessary
components to create a working CMake-based build system. Below is an example of the file:

cmake_minimum_required(VERSION 3.10)

# Define the project


project(MyMinimalProject VERSION 1.0 LANGUAGES CXX)

# Set the C++ standard


set(CMAKE_CXX_STANDARD 17)

# Add the executable


add_executable(MyMinimalExecutable main.cpp)

Let’s break this down step by step:

1. cmake minimum required()


76

cmake_minimum_required(VERSION 3.10)

This command sets the minimum version of CMake required to process the project. It
ensures that the CMake version being used is at least 3.10, which is necessary for certain
features that might be used in the configuration. This line is essential because it
guarantees that your CMake file will not break or behave unexpectedly on older versions
of CMake that do not support newer commands or features.

The minimum required version should be chosen carefully based on the features your
project needs. It is a good practice to specify a version that is compatible with the features
you plan to use, but also widely available across different environments.

2. project()

project(MyMinimalProject VERSION 1.0 LANGUAGES CXX)

The project() command defines the name, version, and language of the project. In
this case, MyMinimalProject is the name of the project, and 1.0 is the version
number. The LANGUAGES CXX argument specifies that the project is written in C++
(CMake defaults to C and C++ if no languages are specified, but explicitly stating it can
prevent potential confusion).

This command also sets some project-wide variables, such as PROJECT NAME (which
holds the name of the project) and PROJECT VERSION (which holds the version
number). These variables can be used later in the build process or in the project
documentation.

3. set(CMAKE CXX STANDARD 17)


77

set(CMAKE_CXX_STANDARD 17)

The set() command is used here to define the C++ standard version for the project. In
this case, we are specifying that the project should be compiled using the C++17 standard.
This is equivalent to passing the -std=c++17 flag to the C++ compiler. By setting this
in the CMake configuration, we ensure that the C++17 features are enabled across the
project.
You can change this value to 11, 14, 20, etc., depending on the version of C++ you wish
to use in your project. This is an important setting because CMake will automatically
propagate the standard across all targets in the project, reducing the need for repetitive
compiler flags.

4. add executable()

add_executable(MyMinimalExecutable main.cpp)

The add executable() command defines an executable target that will be built from
the provided source files. In this case, we are creating an executable named
MyMinimalExecutable from the main.cpp source file.
This is the key command that ties together your source files and defines the primary output
of the build process—an executable program.

• MyMinimalExecutable: This is the name of the executable that will be


generated after building the project. This name can be any valid name for an
executable file.
• main.cpp: This is the source file that CMake will compile and link to generate the
executable. CMake automatically determines the dependencies between source files
and includes them in the build process.
78

5. Building the Project

Once you have the CMakeLists.txt file set up and the source files in place, you can
now build your project using CMake. Here are the steps to build a minimal CMake project
from the command line:

1. Create a Build Directory: It is a best practice to create a separate build directory


outside the source directory. This keeps your source directory clean and allows for
out-of-source builds.

mkdir build
cd build

2. Run CMake: Run CMake from the build directory, specifying the path to the root
directory of the project (where the CMakeLists.txt file is located):

cmake ..

This command will generate the necessary build system files (such as Makefiles or
Visual Studio project files) based on the configuration in the CMakeLists.txt
file.

3. Build the Project: Once the build files have been generated, you can build the
project using the appropriate build tool. If you're using Makefiles, for example, you
can use the make command:

make

4. Run the Executable: After the build process completes, you can run the executable:
79

./MyMinimalExecutable

This should output the result of your main.cpp program, which, in this case, might
simply be a ”Hello, World!” message or any other code you include in the main.cpp
file.

2.2.4 Understanding the Minimal CMake Project


Let’s take a moment to reflect on why this setup is considered minimal, and why it works for a
basic project:

• Simplicity: The project is small and simple, containing only one executable target and
one source file. This minimal structure is useful for learning and testing the most basic
functionality of CMake.

• Automatic Dependency Management: By using add executable() and the


CMAKE CXX STANDARD variable, CMake takes care of the underlying complexity of
finding dependencies and ensuring that the correct compiler flags are applied for
compiling and linking the project.

• Portability: Once you have a minimal CMake project set up, it is portable. By adjusting
only the CMakeLists.txt file, you can generate build files for different platforms,
such as Linux, Windows, and macOS, without having to modify your source code or
project structure.

• Extensibility: Although this project is minimal, it serves as a foundation for extending the
project as you add more features. For instance, you can later add more source files, link
external libraries, define multiple targets, or introduce more advanced CMake features like
custom build commands or conditional logic.
80

2.2.5 Next Steps and Expansion


While the minimal CMake project provides the basic building blocks for a CMake-based build
system, there are several directions in which you can expand the project:

1. Adding More Source Files: If your project grows and you need to organize your code
into multiple files, you can simply add more source files to the add executable()
command.

Example:

add_executable(MyMinimalExecutable main.cpp utils.cpp)

2. Handling External Dependencies: As you add external libraries or dependencies, CMake


provides powerful commands like find package() to locate and link these libraries.

3. Building Libraries: If your project requires shared or static libraries, you can use the
add library() command to define libraries in addition to executables.

4. Organizing Source Files: For larger projects, consider organizing source files into
directories. You can then use add subdirectory() to manage different parts of the
project.

2.2.6 Conclusion
In this section, we defined the minimal CMake project, which includes the essential components
necessary to build a simple C++ program. By creating a CMakeLists.txt file with the
minimum required CMake version, project declaration, executable definition, and compiler
settings, you have established a basic build system that is portable, extensible, and easy to
maintain.
81

Understanding this minimal structure serves as a foundation for more complex projects. Once
you have mastered this, you can start adding features like multiple targets, external
dependencies, custom build commands, and other advanced CMake functionality. This is just
the first step toward building more sophisticated and scalable CMake projects.
82

2.3 Essential CMake Commands


(cmake minimum required, project,
add executable)
In this section, we will focus on three essential CMake commands that are foundational to every
CMake project. These commands—cmake minimum required, project, and
add executable—are crucial to setting up and configuring a CMake project. Understanding
their purpose, syntax, and how to use them correctly is key to creating functional and efficient
CMake build systems.
These commands are the building blocks that help define the minimum requirements for your
project, specify the project’s name and version, and define executable targets. Let’s explore each
of these in detail.

2.3.1 cmake minimum required


1. Purpose
The cmake minimum required command is used to specify the minimum required
version of CMake that is needed to process a CMakeLists.txt file. This command
ensures that your CMake configuration will only run on a version of CMake that supports
the features and syntax your project requires.
This command is especially important for maintaining compatibility with newer CMake
features, as older versions of CMake may not support all the commands and options
available in the latest versions. By explicitly defining the minimum version, you can avoid
running into issues when your project is being configured on systems with older versions
of CMake.

2. Syntax
83

cmake_minimum_required(VERSION <version>)

• VERSION <version>: Specifies the minimum version of CMake that is


required. Replace <version> with the desired CMake version (for example,
3.10 or 3.15).

3. Example

cmake_minimum_required(VERSION 3.10)

In this example, the project will require at least CMake version 3.10. If CMake is run with
an older version, an error will occur, and the build process will not proceed. This is
important to ensure that your CMakeLists.txt file uses only features and commands that
are supported by the specified version or later.

4. Why is cmake minimum required Important?

• Compatibility: It guarantees that your CMakeLists.txt file will run on systems with
a version of CMake that supports all the commands used in the script. If the
minimum version is not specified, CMake will assume that any version of CMake is
valid, which could lead to compatibility issues.
• Error Prevention: By specifying the minimum required version, you prevent
unexpected errors related to incompatible features and behaviors that may be
introduced in future versions of CMake.
• Clarity: It provides clear documentation about the version of CMake needed to
build the project, which helps anyone working with the project (especially in a team
or open-source context) understand which version of CMake is compatible with the
project.
84

2.3.2 project

1. Purpose

The project command is used to define the project's name, version, and the
programming languages that the project uses. This command essentially declares the
project's identity and tells CMake how to configure the build process accordingly. It is
typically one of the first commands in a CMakeLists.txt file after the
cmake minimum required command.

2. Syntax

project(<name> [<language1> <language2> ...] [VERSION <version>]


,→ [DESCRIPTION <description>])

• <name>: The name of the project. This is the primary identifier for the project and
is often used to define the output executable or library names.

• <language1> <language2> ...: A list of programming languages used


in the project (e.g., CXX for C++, C for C). If this is omitted, CMake assumes the
project uses both C and C++ by default.

• VERSION <version>: Optionally defines the project version (e.g., 1.0).

• DESCRIPTION <description>: Optionally provides a brief description of


the project.

3. Example

project(MyProject VERSION 1.0 LANGUAGES CXX)


85

In this example, we define a project named MyProject, with a version of 1.0, and we
specify that the project uses the C++ programming language (CXX).

4. Key Features of the project Command

• Project Name: The name specified in the project command is stored in the
PROJECT NAME variable, and this name is used throughout the project
configuration process. For example, the output executables and libraries will often
take the project name as part of their default names.

• Project Version: The version is stored in the PROJECT VERSION variable and can
be used for version-specific logic in the CMakeLists.txt file, such as selecting
different compiler flags, dependencies, or features based on the version of the
project.

• Language Declaration: By specifying LANGUAGES, you make it clear to CMake


which compilers to use for the project. For example, LANGUAGES CXX tells
CMake to use a C++ compiler. If not specified, CMake assumes C and C++ by
default. The list of languages allows you to include other programming languages
(like Fortran, CUDA, or Python) depending on the needs of your project.

2.3.3 add executable


1. Purpose

The add executable command is used to define an executable target for your project.
This is the primary command for specifying the compilation of a source file or set of
source files into an executable program. It ties together the source code and tells CMake to
generate the corresponding binary after compilation.

This command can be thought of as the key step in creating an application or a runnable
program. It is typically followed by additional configuration to link libraries or specify
86

custom compile options.

2. Syntax

add_executable(<name> [source1] [source2] ...)

• <name>: The name of the executable that will be generated. This name will be the
resulting file’s name (on Linux or macOS, the executable will not have an extension,
but on Windows, it will have a .exe extension).
• [source1] [source2] ...: A list of source files that will be compiled into
the executable. These can be C++ source files (.cpp), header files (.h), or other
files needed for the build process.

3. Example

add_executable(MyApp main.cpp utils.cpp)

In this example, CMake will compile the source files main.cpp and utils.cpp into
an executable called MyApp. After running cmake and make (or an equivalent build
tool), an executable file named MyApp will be generated.

4. Key Considerations When Using add executable

• Target Name: The <name> parameter in add executable defines the name of
the output executable. It is best to use a name that clearly represents the program’s
purpose.
• Source Files: The source files listed in add executable are compiled together
to produce the final binary. It is important to ensure that the correct set of files is
included for the project to build successfully.
87

• Multiple Source Files: You can list multiple source files within the
add executable command, and CMake will handle their compilation. For larger
projects, it is also common to organize source files into directories and use CMake
variables or file(GLOB ...) to automatically collect source files.

• Dependencies: After defining an executable, you will often link it to other libraries
or dependencies using the target link libraries() command. This ensures
that the executable has access to the necessary functionality provided by external
libraries.

2.3.4 Putting It All Together: A Simple Example


Let’s take a look at a simple CMakeLists.txt that incorporates all three commands:
cmake minimum required, project, and add executable.

cmake_minimum_required(VERSION 3.10)

project(SimpleApp VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

add_executable(SimpleApp main.cpp utils.cpp)

Explanation:

1. cmake minimum required(VERSION 3.10): Specifies that the project requires


at least CMake version 3.10.

2. project(SimpleApp VERSION 1.0 LANGUAGES CXX): Declares the project


with the name SimpleApp, sets its version to 1.0, and specifies that it uses C++.
88

3. set(CMAKE CXX STANDARD 17): Specifies that C++17 should be used for
compiling the project.

4. add executable(SimpleApp main.cpp utils.cpp): Compiles


main.cpp and utils.cpp into an executable named SimpleApp.

This project can be built by creating a separate build directory, running CMake, and then
building the executable.

2.3.5 Conclusion
In this section, we examined three essential CMake commands that form the backbone of a basic
CMake project: cmake minimum required, project, and add executable.

• cmake minimum required ensures compatibility with the appropriate CMake


version.

• project defines the project’s name, version, and programming language(s), establishing
the identity of the project.

• add executable links source files together to create an executable target.

Mastering these commands is critical for any developer working with CMake. They help
structure your project and ensure that the build process is compatible with different CMake
versions and setups, making your project easier to manage and maintain.
These commands form the foundation on which more advanced CMake features—such as library
management, external dependencies, and complex configurations—are built. By understanding
these fundamental commands, you can begin creating simple CMake projects, and as your needs
grow, you can expand the configuration to accommodate more complex requirements.
89

2.4 CMake Variable Types (CACHE, ENV, LOCAL)


In CMake, variables play an essential role in controlling the configuration and build process.
However, not all variables in CMake behave the same way. Understanding the different types of
variables in CMake—CACHE, ENV, and LOCAL—is crucial for managing the scope and
behavior of these variables, especially as your project grows and becomes more complex.
Each variable type has different rules for its scope, persistence, and how it interacts with other
parts of the build system. This section explores the three primary types of variables you will
encounter when working with CMake, along with practical examples of their usage.

2.4.1 CACHE Variables


1. Purpose
CACHE variables are used to store values that should persist across multiple runs of
CMake. These variables are typically used for configuration settings that need to be set
once, either by the user or during an initial setup, and then remain consistent throughout
the project lifecycle.
A key characteristic of CACHE variables is that they are stored in the CMake cache file
(CMakeCache.txt). This file can be inspected or modified between CMake runs, and
it allows users to control values without having to modify the CMakeLists.txt file
directly.

2. Syntax

set(<variable> <value> CACHE <type> <docstring> [FORCE])

• <variable>: The name of the variable.


• <value>: The value to assign to the variable.
90

• CACHE: Indicates that the variable is a cache variable.

• <type>: The type of the variable (e.g., STRING, PATH, BOOL, FILEPATH).

• <docstring>: A description of the variable, which is helpful for documentation


purposes and will be displayed in the CMake GUI or when using cmake-gui or
ccmake.

• [FORCE]: Optional. Forces the variable to be set even if it has already been defined
in the cache.

3. Example

set(MY_PROJECT_PATH "/path/to/my/project" CACHE PATH "Path to the


,→ main project directory")

In this example, MY PROJECT PATH is a CACHE variable. The value


/path/to/my/project is set for the variable, and the type is PATH (which indicates
that it is a file or directory path). The docstring provides additional information about the
variable for users to understand its purpose.

4. Use Cases for CACHE Variables

• User-defined configurations: If you want to allow the user to set certain variables
during configuration (e.g., through the CMake GUI or via the command line), you
can use CACHE variables. These are ideal for settings that are configurable, such as
the installation directory, path to external dependencies, or build options.

• Persistent settings: Cache variables persist between different runs of CMake,


making them ideal for configuration settings that don’t change frequently, such as
the path to installed libraries or version numbers.
91

• Controlling build options: You can use CACHE variables to allow users to toggle
features (e.g., whether to enable a particular module or build type) during the CMake
configuration phase.

5. Modifying CACHE Variables


To modify a CACHE variable, you can either:

1. Use the cmake-gui or ccmake interface to modify the variable.


2. Set it directly from the command line by passing the variable to CMake:

cmake -DMY_PROJECT_PATH="/new/path/to/project" ..

3. If you want to force a value to be set for a cache variable, even if it has already been
set, you can use the FORCE option:

set(MY_PROJECT_PATH "/new/path" CACHE PATH "Updated project path"


,→ FORCE)

2.4.2 ENV Variables


1. Purpose
ENV variables are used to access environment variables within CMake. These variables
provide a way for your build system to interact with the host operating system's
environment and are often used to pass system-level settings or configuration details to the
CMake build process.
Environment variables are not set by CMake but are inherited from the operating system’s
environment or shell. You can use ENV variables to read environment settings, such as
paths to compilers, library directories, or system configuration details.
92

2. Syntax

set(<variable> $ENV{<env_variable>})

• <variable>: The name of the CMake variable you want to assign.

• $ENV{<env variable>}: Accesses the environment variable


<env variable>. This is a special syntax that retrieves the value of the
environment variable.

3. Example

set(MY_LIBRARY_PATH $ENV{LIBRARY_PATH})

In this example, the CMake variable MY LIBRARY PATH will be set to the value of the
environment variable LIBRARY PATH. The value of LIBRARY PATH is typically set by
the system and contains directories where libraries are located.

4. Use Cases for ENV Variables

• System-level configuration: If you want your CMake configuration to automatically


read certain system-level environment variables, you can use ENV variables. For
instance, this is useful when dealing with tools or compilers that are set by the
operating system or environment.

• Accessing environment-specific settings: When building on different systems or


environments (e.g., development, staging, production), you may want to access
different paths, settings, or credentials based on environment variables that change
between systems.
93

• Portable builds: ENV variables help make your build system more portable across
different machines by automatically picking up paths and settings defined in the
environment.

5. Common Environment Variables

• PATH: Contains directories for executable binaries. You can use this to find tools
like compilers.

• LD LIBRARY PATH or DYLD LIBRARY PATH: Specifies directories for shared


libraries on Linux and macOS.

• CXX and CC: Used to specify the C++ and C compilers.

• HOME: Represents the user's home directory and can be used for paths to
configuration files, data directories, etc.

2.4.3 LOCAL Variables


1. Purpose

LOCAL variables are used to define variables that exist only within the scope of the
current directory or block (such as a function() or macro()). These variables are
not visible outside of the scope in which they are defined, ensuring that they do not
interfere with other parts of the build configuration.

LOCAL variables are the default type of variable in CMake. When you use the set()
command without explicitly specifying a variable type, it creates a LOCAL variable. These
variables are temporary and are discarded once the scope in which they are defined ends.

2. Syntax
94

set(<variable> <value>)

• <variable>: The name of the variable.

• <value>: The value to assign to the variable.

3. Example

set(MY_LOCAL_VAR "This is a local variable")

In this example, MY LOCAL VAR is a LOCAL variable. It is available only within the
current scope (e.g., within the CMakeLists.txt file or a specific function or block)
and cannot be accessed outside of it.

4. Use Cases for LOCAL Variables

• Temporary values: Use LOCAL variables when you need to store temporary values
that will only be used within a specific part of the CMakeLists.txt file, such as
within a loop or function.

• Avoiding conflicts: Since LOCAL variables are confined to the scope in which they
are created, they help avoid conflicts with other variables defined in different parts of
the project. This is especially useful when writing functions or macros that need to
operate without altering the global state.

• Scope control: LOCAL variables provide better control over where a variable is
accessible. They don’t ”leak” into other parts of the project, which can help keep the
build configuration clean and organized.
95

2.4.4 Summary of Variable Types

Variable Type Scope Persistence Typical Use Cases


CACHE Global (across all Persistent User-defined configurations,
directories) settings, options
ENV Global (system Persistent Access system environment
environment) settings, system paths
LOCAL Local to the current Temporary Temporary, non-persistent values
scope within a block

• CACHE variables are used for persistent values that need to be available across multiple
CMake runs and can be modified by the user.

• ENV variables are used to access system or environment variables from the host operating
system.

• LOCAL variables are temporary and only exist within the scope in which they are defined,
making them ideal for internal values that don’t need to persist beyond the current
configuration step.

2.4.5 Conclusion
Understanding the different variable types in CMake—CACHE, ENV, and LOCAL—is essential
for effective project configuration and build management. By selecting the right variable type for
the job, you can control the scope, persistence, and visibility of configuration values in a way
that helps keep your build system clean and maintainable.

• Use CACHE for user-configurable settings that should persist across multiple CMake runs.
96

• Use ENV for environment-specific values that need to be accessed during the build
process.

• Use LOCAL for temporary values that are needed only in a specific scope.

By mastering these variable types, you can ensure that your CMake configuration is both flexible
and efficient.
97

2.5 Using message() for Debugging and Output


In CMake, managing the configuration of your project can become complex, especially as the
number of variables, dependencies, and build options increases. In such scenarios, debugging
and providing feedback during the CMake configuration process is essential. One of the most
useful tools for this task is the message() command.
The message() command in CMake is a simple yet powerful way to output information
during the configuration and build process. It can be used to display debugging information,
warnings, errors, or general status updates. By understanding how to use message()
effectively, you can gain better insight into your project's build configuration, detect issues early,
and track the flow of the build process.
This section will cover the basics of the message() command, its various usage options, and
how to use it for debugging and providing meaningful output during the configuration process.

2.5.1 Purpose of the message() Command


The primary purpose of the message() command is to allow you to display messages to the
user during the CMake configuration phase. These messages are typically used for:

• Debugging: Printing variable values, checking paths, or verifying conditions during the
configuration phase.

• Status Updates: Informing users or developers about the progress or state of the build
configuration.

• Warnings and Errors: Alerting users to issues that need attention or stopping the
configuration process if critical errors occur.

The message() command can output messages at different levels of severity, allowing you to
categorize the output based on its importance.
98

2.5.2 Syntax of message()


The basic syntax of the message() command is as follows:

message([<mode>] <message>)

• <mode>

: Optional. Defines the severity level of the message. It can be one of the following:

– STATUS: Default. Prints a regular informational message.

– WARNING: Prints a warning message. It shows in yellow and can indicate a


non-fatal issue.

– AUTHOR WARNING: A warning message intended only for the author (developer),
not the user. It is similar to WARNING but can be filtered out by users in a
non-interactive setup.

– SEND ERROR: Prints an error message and halts the configuration process. This is
used to indicate a critical issue that prevents further configuration.

– FATAL ERROR: Similar to SEND ERROR, but it immediately stops the


configuration process, making it impossible to continue. This is used when a critical
error prevents any further progress.

– DEPRECATION: Used to warn the user about the use of deprecated features or
practices in the CMake configuration.

• <message>: The text or string that you want to print. This can include variables, paths,
or any other information you want to display.
99

2.5.3 Basic Examples of message()


1. Simple Message

message("This is a simple message")

This will print the message "This is a simple message" in the standard output
during the configuration phase.

2. Using STATUS for Informational Messages

By default, message() uses the STATUS mode, which is suitable for informational
messages that are not critical to the build process.

message(STATUS "Configuring project...")

This will display the message "Configuring project..." in the output, typically
with a green color to denote that it's informational.

3. Using WARNING for Warnings

You can use WARNING to display a warning message. This is helpful when you want to
inform users of a potential issue that does not block the build process but might require
attention.

message(WARNING "Warning: The path to the library is not set


,→ correctly!")

This will print a yellow-colored warning message that informs users about a possible
issue, but it won't stop the build configuration.
100

4. Using SEND ERROR for Errors

If you encounter a situation that must be addressed before proceeding with the
configuration, you can use SEND ERROR to display an error message. This will not stop
the configuration immediately but will mark the build as having an error, preventing the
generation of makefiles or build files.

message(SEND_ERROR "Error: Missing required dependency!")

This will print the error message and continue with the configuration process, but the error
will be recorded, and no build files will be generated.

5. Using FATAL ERROR for Critical Errors

If the configuration cannot proceed due to a critical error, you can use FATAL ERROR.
This will immediately stop the configuration process and prevent any further steps.

message(FATAL_ERROR "Critical error: Cannot find the required C++


,→ compiler!")

When this message is encountered, CMake will stop immediately, and no build files will
be generated. This is useful for situations where proceeding without resolving the error
would lead to a broken or incomplete build.

6. Using DEPRECATION for Deprecated Features

If you are working with deprecated features or commands in your CMakeLists.txt


file, you can use DEPRECATION to alert the user about the deprecated feature.

message(DEPRECATION "Warning: The `add_custom_command` is deprecated,


,→ consider using `add_custom_target`.")
101

This will display a message indicating that a certain feature is deprecated, helping guide
users or developers toward better practices.

2.5.4 Using Variables in message()


A powerful feature of the message() command is its ability to print variable values. By
including CMake variables in the message string, you can dynamically generate output based on
the current configuration state.

1. Displaying Variable Values

set(MY_VAR "Hello, CMake!")


message(STATUS "The value of MY_VAR is: ${MY_VAR}")

This will output:

The value of MY_VAR is: Hello, CMake!

Using ${} allows you to reference the value of a variable and incorporate it into the
message.

2. Debugging with Variables

You can use message() to debug variable values during the configuration process. This
is particularly useful when you want to track the values of important variables at different
points in the CMakeLists.txt file.

set(MY_VAR "Some value")


message(STATUS "Before: MY_VAR = ${MY_VAR}")
# Modify the variable
102

set(MY_VAR "New value")


message(STATUS "After: MY_VAR = ${MY_VAR}")

This will output:

Before: MY_VAR = Some value


After: MY_VAR = New value

This helps in tracking changes to variables as the CMake configuration progresses.

2.5.5 Controlling Output Visibility


Sometimes, you may want to control the visibility of the messages during the configuration
process. For example, you might want to suppress some messages unless you explicitly enable
debug output.

1. Controlling Verbosity with CMAKE VERBOSE MAKEFILE

One way to control verbosity is by setting the CMAKE VERBOSE MAKEFILE variable.
When set to TRUE, it enables more detailed output during the build process, which can be
helpful for debugging the build steps themselves. However, this does not directly control
message() output, but it can help control the level of detail you get from the build
process.

set(CMAKE_VERBOSE_MAKEFILE TRUE)

2. Controlling Debug Output with CMAKE MESSAGE LOG LEVEL


103

Another way to control output visibility is by setting the CMAKE MESSAGE LOG LEVEL
variable. This determines the threshold of message severity that is displayed. You can
choose to show only errors, warnings, or detailed status messages.

set(CMAKE_MESSAGE_LOG_LEVEL "WARNING")

This would display only warnings and errors, suppressing informational messages.
Chapter 3

Building Projects with CMake

3.1 Understanding ”Configure,” ”Generate,” and ”Build”


Steps

CMake is a powerful tool that automates the process of building and managing complex
projects. However, before the actual build process takes place, CMake goes through a few
preliminary steps: configure, generate, and build. These three distinct phases are
essential to the CMake workflow and understanding their roles is crucial for effectively
using CMake to manage your C++ projects. In this section, we will delve into each of
these steps and explore what they involve, how they relate to each other, and why they are
important.

3.1.1 Overview of the CMake Workflow

The CMake build process typically involves three primary steps:

104
105

1. Configure: This is where CMake inspects your environment, reads the


CMakeLists.txt files, and generates necessary configuration files that are
tailored to your system and project. This phase is about defining the build
environment, checking dependencies, and setting up necessary flags and options for
the build process.

2. Generate: After the configuration step, CMake generates the build system files.
These files are specific to the generator you selected (such as Makefiles, Visual
Studio project files, or Xcode project files). The generated files are used by the build
tools to carry out the actual compilation and linking of the project.

3. Build: This step is where the actual compilation and linking of your project take
place. It involves invoking a build tool (like make, ninja, or the native build
system for IDEs such as Visual Studio or Xcode) to perform the build based on the
files generated in the previous step.

3.1.2 The ”Configure” Step

The configure step is the initial phase of working with CMake, and it is where CMake sets
up everything needed to generate the build files. During configuration, CMake performs
the following tasks:

• Reads CMakeLists.txt Files: The CMakeLists.txt file contains the


project's build instructions. CMake processes these files to understand what needs to
be built, which libraries are required, and what dependencies need to be resolved. If
there are any find package() or find program() calls, CMake will attempt
to locate these packages and executables on your system.

• Checks the Environment: CMake inspects your system environment to determine


the necessary tools and libraries for building the project. It checks for compilers
106

(e.g., gcc, clang, or MSVC), system libraries, required tools, and other software
dependencies. If a required dependency is missing, CMake will either notify you or
attempt to download or build it.

• Sets Configuration Variables: Configuration variables (like compiler flags, paths to


libraries, and options for features like multi-threading or debugging) are set during
this step. You can provide values for these variables via the CMake command line,
environment variables, or by editing the CMakeLists.txt file.

• Generates Cache Variables: The configuration step creates a CMakeCache.txt


file in the build directory. This file contains key configuration information and
variable values, which persist across CMake runs. If you change settings or modify
paths, they can be reflected in the cache and used in subsequent builds.

1. Example of Running the configure Step


The configure step can be initiated using the CMake command line interface (CLI)
as follows:

cmake <path-to-source>

For example:

cmake ../my_project

This will trigger CMake to process the CMakeLists.txt files in the specified
directory and configure the project for the current system. Once complete, CMake
will have generated the necessary build system files for the next step.

2. Common CMake Configuration Options


Some commonly used configuration options include:
107

• -DCMAKE BUILD TYPE=Release: Specifies the build type, such as


Release, Debug, or RelWithDebInfo.
• -DCMAKE INSTALL PREFIX=<path>: Sets the installation directory.
• -DUSE FOO=ON: Enables or disables specific features or packages.

3.1.3 The ”Generate” Step

Once the configuration step is complete, the next phase is the generate step. In this phase,
CMake generates the files required by the build system. The generation process is
determined by the generator you select, which could be a build tool or IDE-specific file
format.
CMake supports several types of generators, such as:

• Makefiles: This is the most common generator for Linux and macOS environments.
It produces a Makefile that can be used with the make tool to compile and link
the project.
• Ninja: A small, fast build system that is an alternative to make. If you specify -G
Ninja, CMake will generate build.ninja files for use with the ninja build
tool.
• IDE-Specific Generators: These are used to generate project files for various IDEs
like Visual Studio, Xcode, or CodeBlocks. For example, on Windows, running
cmake -G "Visual Studio 16 2019" .. will generate Visual Studio
project files that you can open directly in the IDE.
• Unix Makefiles: These are the default generator on many Unix-like systems,
producing a set of Makefile scripts that can be used to invoke make.

Example of Running the generate Step


108

The generation step is invoked automatically as part of the configuration phase when you
run the CMake command. For example:

cmake -G "Unix Makefiles" ../my_project

This command will configure and then generate Makefile build files in the build
directory.

3.1.4 The ”Build” Step

After the configuration and generation phases are complete, you move to the build step.
This is the phase in which the actual compilation, linking, and final build of your project
occur.

During this step, the build tool (such as make, ninja, or Visual Studio) uses the files
generated in the previous phase to build the project. The build tool will execute the
instructions specified in the generated files to compile the source code, link the object files
into executables, and create libraries as defined in the CMakeLists.txt file.

1. Running the Build Step

• With Makefiles: If you used cmake -G "Unix Makefiles", you can


build the project by running make in the build directory:

make

• With Ninja: If you used the Ninja generator, you would use the ninja tool
to build the project:
109

ninja

• With IDEs (e.g., Visual Studio): If you generated project files for an IDE like
Visual Studio, you can build the project directly from within the IDE interface
or use the command line:

msbuild MyProject.sln

2. Build Targets and Customization


You can also build specific targets or configure additional steps in your build process.
For example:

make install

This will install the project if you have set up the installation rules in your
CMakeLists.txt file using commands like install().

3.1.5 Relationship Between Configure, Generate, and Build

While the configure, generate, and build steps are distinct, they are interdependent and
occur in sequence:

1. Configure: Set up the project, inspect the system, define variables, and check
dependencies.
2. Generate: Create the necessary build files (such as Makefile, ninja, or
IDE-specific files) based on the configuration.
3. Build: Use the generated build files to compile and link the project into executables
or libraries.
110

It’s important to note that the configure step often only needs to be run once unless you
make changes to the configuration (such as adding new source files, changing build
options, or modifying dependencies). The generate step is run after configuration to
generate the appropriate build system files, and the build step can be run multiple times
during the development cycle, especially when making incremental changes to the project.

3.1.6 Re-running the Configuration Steps

If you need to modify your build configuration (for example, to change compiler flags or
enable/disable features), you can re-run the configure and generate steps. When this
happens, CMake will read the configuration files again, update the cache, and regenerate
the build system files.
Sometimes, changes to the CMakeLists.txt files or other source files will require
cleaning the build directory before re-running the configuration. CMake supports
incremental builds, but certain changes might require a fresh configuration.

Example of Re-running CMake

cmake ../my_project

This will reconfigure the project. If the configuration or generator has changed, CMake
will regenerate the necessary files.

3.1.7 Summary of the Configure, Generate, and Build Phases


111

Step Description When to Run


Configure CMake inspects the system and Whenever you change project
prepares the build configuration files. settings, dependencies, or
configurations.
Generate CMake generates the build system After configuration, whenever
files (Makefiles, Visual Studio build system files need to be
project files, etc.). generated or regenerated.
Build The actual compilation and linking Repeatedly during development
process occurs using the generated as changes are made to the project
build system files. code.

3.1.8 Conclusion

Understanding the three key steps of the CMake workflow—configure, generate, and
build—is critical for efficiently managing and building C++ projects with CMake. These
steps are interdependent and serve distinct purposes in the overall process:

• Configure: Set up the project environment and check dependencies.


• Generate: Create the appropriate build system files for your platform.
• Build: Compile the project and create the desired outputs.

By following this workflow, you can efficiently manage complex builds, handle
dependencies, and customize your project setup according to your system and
development environment.
112

3.2 Running cmake with Different Generators (Ninja,


Makefile, Visual Studio)

CMake supports a variety of generators that allow you to configure and generate build
files for different platforms, build systems, and Integrated Development Environments
(IDEs). These generators define the type of build system that CMake will create for your
project. Understanding how to run cmake with different generators—such as Ninja,
Makefile, and Visual Studio—is essential for customizing your build process to suit your
development environment.
In this section, we will explore how to use CMake with different generators and how they
affect the project setup and build process. We'll walk through the specifics of working
with each of these popular build systems, explaining their strengths and providing
practical examples.

3.2.1 Overview of CMake Generators

When you run CMake, one of the key options you specify is the generator. The generator
determines what kind of build files CMake will produce. Each generator corresponds to a
specific build system or IDE, and selecting the right one ensures that CMake can interact
seamlessly with your development environment.
Some of the most common generators include:

• Ninja: A fast, small, and efficient build system.


• Makefile: The traditional Unix-based build system that uses make to build the
project.
• Visual Studio: A set of generators that produce project files for various versions of
Microsoft Visual Studio.
113

These generators offer flexibility in terms of performance, platform compatibility, and user
preference. Let’s dive into how to configure CMake to use each of these generators and
the scenarios in which they are most useful.

3.2.2 Running cmake with the Ninja Generator

Ninja is a small, fast build system with a focus on performance. It is often used in
environments where speed is important and works especially well for large projects. Ninja
operates by processing small build files that contain just enough information to trigger the
necessary build steps, making it significantly faster than traditional build systems in many
cases.

1. Why Choose Ninja?

• Fast: Ninja is known for its speed in incremental builds. It minimizes the work
done by only rebuilding parts of the project that have changed.
• Minimalistic: Unlike other build systems that generate large build files, Ninja
generates concise and efficient build files, resulting in reduced I/O and faster
execution.
• Cross-Platform: Ninja can be used on multiple platforms (Linux, macOS, and
Windows), making it a great choice for cross-platform projects.

2. Using Ninja with CMake


To use Ninja as your build system, you need to first install Ninja (if it’s not already
installed on your system). Once installed, you can specify Ninja as the generator
when running the cmake command:

cmake -G Ninja <path-to-source>


114

This command tells CMake to generate Ninja build files in the build directory, based
on the source code in the specified directory. After configuration, you can then use
Ninja to build the project:

ninja

3. Example: Using Ninja with a Project


Consider a project located in ˜/projects/my app. To configure and build this
project using Ninja, you would run the following commands:

mkdir build
cd build
cmake -G Ninja ../my_app
ninja

This sequence of commands will configure the project, generate the Ninja build files,
and then execute the build process.

3.2.3 Running cmake with the Makefile Generator

Make is a well-known build system in Unix-like environments. It uses Makefiles to


determine how to build and link the project. The Makefile generator is the most
common choice for Linux and macOS systems, as make is a standard tool in these
environments.

1. Why Choose Makefile?

• Standard: Make is widely supported on Unix-based systems and is the default


build system for many projects.
115

• Flexibility: Makefiles are extremely flexible and customizable, offering


extensive control over the build process.
• Toolchain Integration: Make integrates well with a variety of compilers and
build tools, making it easy to work with complex toolchains and custom
configurations.

2. Using Makefile with CMake


To use make as the generator, specify the Unix Makefiles generator in the
cmake command:

cmake -G "Unix Makefiles" <path-to-source>

After CMake generates the necessary Makefiles, you can build the project using the
make command:

make

3. Example: Using Makefiles with a Project


Let’s say you have a project at ˜/projects/my app. To configure and build this
project using Makefiles, you would run:

mkdir build
cd build
cmake -G "Unix Makefiles" ../my_app
make

This will configure the project, generate the Makefile, and compile the project using
the make tool.
116

3.2.4 Running cmake with the Visual Studio Generator

1. Visual Studio is one of the most widely used IDEs for C++ development,
particularly on Windows. CMake provides generators for multiple versions of Visual
Studio, which allow you to create Visual Studio project files (e.g., .sln,
.vcxproj) for your project. This is particularly useful for developers who prefer
the Visual Studio environment for building and debugging C++ projects.
2. Why Choose Visual Studio?

• IDE Support: Visual Studio is a powerful IDE that provides an extensive set of
features such as debugging, profiling, and an intuitive graphical interface for
project management.
• Native Windows Development: Visual Studio is the standard development
environment for C++ on Windows, making it ideal for targeting
Windows-specific APIs and libraries.
• Advanced Features: Visual Studio provides features like IntelliSense, a visual
debugger, and integrated testing tools that improve productivity.

3. Using Visual Studio with CMake


To generate Visual Studio project files with CMake, specify the appropriate version
of Visual Studio as the generator. For example, to generate project files for Visual
Studio 2019, you would run:

cmake -G "Visual Studio 16 2019" <path-to-source>

This will create .sln files that can be opened directly in Visual Studio. After the
project is generated, you can open the .sln file in Visual Studio and build the
project from the IDE.
4. Example: Using Visual Studio with a Project
117

Let’s say you have a project located in C:\projects\my app. To configure and
generate Visual Studio project files, use:

mkdir build
cd build
cmake -G "Visual Studio 16 2019" C:\projects\my_app

This will generate a Visual Studio solution file (e.g., my app.sln) in the build
directory. You can then open this .sln file in Visual Studio and build the project.

3.2.5 Choosing the Right Generator for Your Project

Choosing the correct generator depends on your specific requirements and development
environment. Here are some guidelines to help you decide which generator to use:

• Ninja: Ideal for fast, efficient builds, especially in large projects. It’s a good choice
if you prioritize build speed and want a cross-platform solution.

• Makefile: Best for traditional Unix-based systems (Linux, macOS). It’s the default
for many open-source projects and is widely supported.

• Visual Studio: Perfect for Windows-based development using the Visual Studio IDE.
If you need to work in a Microsoft-centric development environment, generating
Visual Studio project files is the way to go.

Additionally, consider the complexity of your project. For simple, small projects, any
generator will work fine. For large projects with complex dependencies or custom build
steps, you might prefer Ninja or Makefile due to their simplicity and speed. Visual Studio
is best suited for projects that benefit from deep IDE integration, such as debugging or
visual design tools.
118

3.2.6 Conclusion

Understanding how to run cmake with different generators—Ninja, Makefile, and


Visual Studio—is essential for tailoring the build process to your development
environment. Each generator has its advantages and is best suited for different use cases:

• Ninja offers fast and efficient builds, making it ideal for large projects.
• Makefile is the traditional choice for Unix-based systems and offers flexibility and
control over the build process.
• Visual Studio is a powerful IDE for Windows development and integrates
seamlessly with CMake for generating project files.

By selecting the appropriate generator, you can streamline the build process and integrate
CMake more effectively into your existing workflow. Whether you are developing on a
Linux, macOS, or Windows platform, CMake provides the tools you need to manage and
build your C++ projects efficiently.
119

3.3 Managing Source Files and Output Executables

When working with CMake, understanding how to effectively manage source files and
define output executables is a critical part of setting up your build process. CMake
simplifies this task by providing clear, intuitive mechanisms to specify the organization of
source files and control where the final executables and libraries are placed. This section
will explain how to manage source files and control output executables in a CMake
project, covering basic commands like add executable(), add library(), and
the use of source groups.

3.3.1 Overview of Source Files in CMake

Source files are the building blocks of your project—they contain the code that will be
compiled into the final executable or library. CMake offers a variety of ways to manage
these files, from defining them explicitly to leveraging wildcard patterns and automatic file
discovery. Whether your project has a simple structure or a more complex, multi-directory
setup, CMake provides tools to help organize and include source files efficiently.

CMake supports different types of source files for various build targets:

• C++ Source Files (.cpp, .cc, .cxx)

• Header Files (.h, .hpp)

• Resource Files (e.g., .rc on Windows, .qml files in Qt-based projects)

The key here is to specify the source files correctly to ensure that they are compiled into
the appropriate output.
120

3.3.2 Defining Source Files for Executables

The most fundamental operation in CMake is defining the source files that will be
compiled into an executable. This is achieved using the add executable() command.
The general syntax is:

add_executable(<name> <source1> <source2> ... <sourceN>)

• <name>: The name of the executable you want to create.

• <source1>, <source2>, ... <sourceN>: The list of source files (e.g., .cpp
files) that will be compiled to create the executable.

1. Example: Simple Executable Definition


Consider a project where you have the following C++ files:

main.cpp
foo.cpp
foo.h

To define an executable named my app using main.cpp and foo.cpp, you


would write:

add_executable(my_app main.cpp foo.cpp)

This tells CMake to create an executable named my app from the source files
main.cpp and foo.cpp. After running the build process, the output will be an
executable file named my app (or my app.exe on Windows).
121

2. Handling Header Files


Header files are typically included in source files and don’t require direct
specification in add executable(). However, it’s a good practice to group them
into logical directories for better organization. CMake automatically handles headers
as long as they are included in the relevant source files.

3.3.3 Organizing Source Files Using file() and


aux source directory()

For larger projects with multiple directories, you might want to automate the process of
collecting source files. CMake provides the file() and aux source directory()
commands to facilitate this.

1. Using aux source directory()


The aux source directory() command scans a directory and adds all source
files within that directory to a variable. It can be particularly useful for large projects
with many source files located in subdirectories.
Example:

aux_source_directory(src SOURCES)
add_executable(my_app ${SOURCES})

This command will search the src directory for all .cpp files and add them to the
SOURCES variable. The executable my app will then be built from all the source
files found in the src directory.
2. Using file(GLOB ...)
Alternatively, you can use file(GLOB ...) to collect all files matching a
specific pattern, such as all .cpp files in a given directory:
122

file(GLOB SOURCES "src/*.cpp")


add_executable(my_app ${SOURCES})

The file(GLOB ...) command is useful when you have files in a directory that
follow a specific naming pattern. However, it’s generally recommended to avoid
overusing this command, as it can make the build system harder to maintain when
files are added or removed.

3.3.4 Defining Output Executables and Directories

Once you have specified the source files for your project, you need to define where the
resulting executable should be placed. CMake provides various commands and options to
control this.

1. Setting the Output Directory for Executables


By default, CMake places the output executables in the CMAKE BINARY DIR
directory (which is usually the build directory). However, you can customize the
location of the executable with the RUNTIME OUTPUT DIRECTORY property:

set_target_properties(my_app PROPERTIES RUNTIME_OUTPUT_DIRECTORY


,→ ${CMAKE_BINARY_DIR}/bin)

This sets the output directory for the executable my app to a subdirectory called
bin inside the build directory.

2. Example: Organizing Executables in a bin Directory


For example, if you want all your executables to be placed in a bin directory within
the build folder, you can add the following to your CMakeLists.txt:
123

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

add_executable(my_app main.cpp foo.cpp)


add_executable(my_app2 main2.cpp bar.cpp)

This will result in my app and my app2 being placed in the bin directory inside
your build folder, making it easier to manage the outputs of your project.

3.3.5 Managing Multiple Executables and Targets

Many projects may require the creation of multiple executables from different sets of
source files. In CMake, each executable or library you define is treated as a target, and
you can manage them separately.

1. Adding Multiple Executables


To add another executable, simply call add executable() with a different target
name and source files:

add_executable(my_app main.cpp foo.cpp)


add_executable(my_app2 main2.cpp bar.cpp)

Each target is built independently, and CMake will ensure that all source files are
compiled correctly and linked into the appropriate executables.

2. Organizing Executables into Directories


If you have a large number of executables, it can be helpful to organize them into
subdirectories. You can use add subdirectory() to create logical groupings in
your project:
124

my_project/
CMakeLists.txt
bin/
CMakeLists.txt
my_app.cpp
my_app2.cpp
lib/
libfoo.cpp

In the top-level CMakeLists.txt, you could add:

add_subdirectory(bin)

In the bin/CMakeLists.txt, you would then define the executables:

add_executable(my_app my_app.cpp)
add_executable(my_app2 my_app2.cpp)

This modular structure helps keep the build process organized, especially when
dealing with large projects.

3.3.6 Defining Libraries for Reusability

In addition to executables, CMake also makes it easy to define libraries. Libraries are
reusable collections of code that can be linked with other projects or executables.

1. Creating a Static Library


To create a static library, use the add library() command with the STATIC
option:
125

add_library(my_lib STATIC foo.cpp bar.cpp)

This creates a static library named my lib.a (on Unix-like systems) or


my lib.lib (on Windows), which can be linked to other executables or libraries.

2. Creating a Shared Library


To create a shared library (dynamic link library, DLL, or .so), use the SHARED
option with add library():

add_library(my_lib SHARED foo.cpp bar.cpp)

This creates a shared library that can be dynamically loaded by executables at


runtime.

3. Linking Libraries to Executables


Once a library is created, it can be linked to an executable or another library using
the target link libraries() command:

target_link_libraries(my_app my_lib)

This links the static or shared library my lib to the executable my app.

3.3.7 Conclusion

Managing source files and output executables in CMake is an essential part of setting up a
successful build system. By using commands like add executable(),
add library(), and target link libraries(), you can define which source
files to compile, where to place your output executables, and how to manage libraries
within your project. Additionally, CMake offers powerful tools like file() and
126

aux source directory() to help automate the discovery and organization of


source files, especially in larger projects. By mastering these techniques, you’ll be able to
build robust, organized, and efficient CMake projects that scale with the complexity of
your C++ codebase.
127

3.4 Running cmake --build and cmake --install

Once you’ve configured your CMake project and generated the appropriate build files, the
next step is to actually build and install your project. These tasks are essential to the
process of turning your source code into a usable application or library. CMake simplifies
the build and install process with the cmake --build and cmake --install
commands, respectively.
In this section, we’ll delve into how to run these commands, what they do, and how they
fit into the broader CMake workflow.

3.4.1 Running cmake --build

The cmake --build command is used to compile and link the project, essentially
performing the build step. This command simplifies the process by abstracting away the
complexities of interacting directly with the build system (such as make, ninja, or
MSBuild), and it ensures consistency across different platforms and environments.

1. Overview of cmake --build


After running the initial cmake command to configure your project, you’ll typically
be left with build files (such as Makefiles, Ninja build files, or Visual Studio solution
files). The cmake --build command is then used to invoke the appropriate tool
(like make or ninja) to perform the actual compilation and linking process.
The basic syntax for cmake --build is:

cmake --build <build-directory> [options]

• <build-directory>: This is the path to the build directory where CMake


has generated the build files (e.g., build/).
128

• [options]: These are optional flags that you can pass to modify the build
process (such as building a specific target or specifying the number of parallel
jobs).
2. Running cmake --build Without Arguments
In its simplest form, you can run cmake --build without any additional
arguments to build the default target (typically the project’s primary executable or
library):

cmake --build build/

This command will invoke the correct build system for your platform (e.g., make,
ninja, or MSBuild) and will build the default target. If you are in the build
directory, you can simply run:

cmake --build .

CMake will handle determining the correct tool and invoking it with the necessary
arguments to perform the build.
3. Specifying Build Targets
You can specify which target you want to build by using the --target option.
This is useful if you want to build a specific target, such as a particular executable or
library, rather than the default target.
For example, to build a specific executable (e.g., my app), you would run:

cmake --build build/ --target my_app

This command will only build the my app target, skipping the other targets in the
project. This is particularly useful in larger projects with multiple components, as it
allows you to build only the necessary parts of the project.
129

4. Building with Parallel Jobs


To speed up the build process, you can specify the number of parallel jobs to use
with the -- -j option (this is passed to the underlying build tool, like make or
ninja). This can dramatically reduce build times, especially in large projects with
many files to compile.
For example, to use 4 parallel jobs, you can run:

cmake --build build/ -- -j4

This tells the build system to use 4 processors to compile the project in parallel.

5. Cleaning the Build


If you want to clean up intermediate build files, CMake provides the --target
clean option. This is equivalent to running make clean or the relevant clean
command for the build tool you’re using.

cmake --build build/ --target clean

This removes the object files and other intermediate files, but leaves the
CMake-generated files (such as Makefiles or Visual Studio project files) intact.

3.4.2 Running cmake --install

After building the project, the next step is typically to install it. The cmake
--install command allows you to copy the built files (executables, libraries, headers,
etc.) to their final locations on the system, making them ready for use or distribution. This
step is crucial when you want to deploy your project or make it available for other
software to link to.
130

1. Overview of cmake --install


The cmake --install command copies the built project to a specific
installation directory. The installation process uses paths defined by CMake
variables such as CMAKE INSTALL PREFIX, which specifies where the files will
be installed. If you have not customized this variable, the default installation path is
/usr/local on Unix-like systems (Linux/macOS) and C:\Program Files
on Windows.
The basic syntax for cmake --install is:

cmake --install <build-directory> [options]

• <build-directory>: The path to the build directory where the project


was built.
• [options]: Optional flags to customize the installation process.

2. Specifying the Installation Directory


You can specify a custom installation directory by setting the
CMAKE INSTALL PREFIX variable. This can be done either in the
CMakeLists.txt file or via the command line.
To set the installation directory during configuration (before building), use the
following command:

cmake -DCMAKE_INSTALL_PREFIX=/path/to/install/directory
,→ <path-to-source>

Alternatively, you can specify the installation directory during the cmake
--install command itself:
131

cmake --install build/ --prefix /path/to/install/directory

3. Running cmake --install


Once the build process is complete, you can install the project with:

cmake --install build/

This will copy the necessary files (such as executables, libraries, and headers) from
the build directory to the installation directory defined by
CMAKE INSTALL PREFIX.

4. Installing Specific Targets


If your project has multiple installable targets (e.g., both an executable and a library),
you can specify which target to install by using the --target option:

cmake --install build/ --target my_app

This installs only the my app executable, not any other components.

5. Installing with Multiple Configurations (For Multi-Configuration Generators)


When working with multi-configuration generators like Visual Studio or Xcode, you
can specify which build configuration (such as Debug or Release) to install using the
--config option:

cmake --install build/ --config Release

This command installs the project built in the Release configuration.


132

3.4.3 Customizing the Installation Process

CMake allows you to define custom installation rules for specific files or directories using
the install() command within your CMakeLists.txt. This command provides
flexibility in deciding what gets installed and where.

For example, if you want to install an executable and a library, you can define the
following in your CMakeLists.txt:

install(TARGETS my_app DESTINATION bin)


install(TARGETS my_lib DESTINATION lib)
install(DIRECTORY include/ DESTINATION include)

This would install:

• my app to the bin directory.

• my lib to the lib directory.

• The contents of the include/ directory to the include directory.

By customizing the install() commands, you can control the installation of


executables, libraries, headers, and other project files to the appropriate locations on the
system.

3.4.4 Conclusion

The cmake --build and cmake --install commands are key components of the
CMake build process.
133

• cmake --build compiles and links the project, invoking the underlying build
system (such as make, ninja, or MSBuild) to produce the final executables,
libraries, or other targets.
• cmake --install copies the built project files to a specified installation
directory, making them ready for use or distribution.

By understanding how these commands work and how to configure them for your needs,
you can effectively manage the build and installation of your CMake-based projects.
Whether you're working on a small application or a large library, CMake provides a
flexible and consistent way to build and install your software across multiple platforms.
134

3.5 Controlling Build Options via CMAKE BUILD TYPE

CMake provides a powerful mechanism for controlling the behavior of the build process
through various configuration options, one of the most crucial being
CMAKE BUILD TYPE. This variable determines the type of build configuration you want
to generate, such as a debug build, release build, or a custom build type. This section will
explore how to effectively control build options using CMAKE BUILD TYPE, the impact
of different build types, and how to customize and fine-tune the configuration of your
builds.

3.5.1 Overview of CMAKE BUILD TYPE

The CMAKE BUILD TYPE variable is one of the primary ways to control how your
project is built. It specifies the type of build configuration that CMake should use when
generating the build system. Typically, this is a setting you configure before you generate
your build files, and it can affect several important aspects of the build, such as
optimization levels, debugging information, and compiler flags.

The CMAKE BUILD TYPE variable is primarily used with single-configuration


generators, such as Makefiles, Ninja, and Unix-style build systems. For
multi-configuration generators like Visual Studio or Xcode, the build type is typically
specified as part of the build process rather than the configuration process, and
CMAKE BUILD TYPE does not have the same effect.

Typical Build Types

CMake supports several common build types, each of which comes with different
optimizations and debugging settings. The most commonly used build types are:
135

• Debug: This build type generates debugging information and disables optimizations
to help with debugging. It's suitable when you need to inspect your code in a
debugger or need detailed information about your program’s state during execution.

– Compiler flags: -g (GCC/Clang), /Zi (MSVC)


– No optimizations enabled

• Release: This build type is optimized for performance. It enables compiler


optimizations and disables debugging information. This is the default build type for
production-ready code.

– Compiler flags: -O2 or -O3 (GCC/Clang), /O2 (MSVC)


– No debugging symbols included

• RelWithDebInfo: This build type strikes a balance between performance and


debugging. It enables optimizations but also includes debugging information. It’s a
good choice if you need performance but still want to debug the program if
necessary.

– Compiler flags: -O2 -g (GCC/Clang), /O2 /Zi (MSVC)

• MinSizeRel: This build type is focused on minimizing the size of the compiled
binary while still providing optimizations. It’s often used for embedded systems or
other scenarios where small binaries are essential.

– Compiler flags: -Os (GCC/Clang), /O1 (MSVC)

3.5.2 Setting CMAKE BUILD TYPE

To set the build type in CMake, you can define CMAKE BUILD TYPE during the
configuration phase. This is typically done from the command line when you run the
cmake command to generate the build files.
136

1. Basic Example
For example, to configure the build for a Debug build type, you can run:

cmake -DCMAKE_BUILD_TYPE=Debug /path/to/source

This command tells CMake to configure the project for debugging, meaning it will
generate the appropriate build system with debugging flags and without
optimizations.
Similarly, to generate a Release build type, you would use:

cmake -DCMAKE_BUILD_TYPE=Release /path/to/source

This will configure the project for release, ensuring that compiler optimizations are
enabled and debugging symbols are removed.

2. Other Build Types


You can set other build types by using CMAKE BUILD TYPE with the following
commands:

cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo /path/to/source


cmake -DCMAKE_BUILD_TYPE=MinSizeRel /path/to/source

Each of these will configure CMake to use the corresponding set of compiler flags
and options.

3.5.3 Effect of CMAKE BUILD TYPE on the Build Process

Setting the CMAKE BUILD TYPE variable not only determines the compiler flags for
optimization and debugging but also influences several other aspects of the build:
137

• Compiler Options: The compiler flags vary between build types, such as
optimizations (-O3), debugging symbols (-g), and additional runtime checks (e.g.,
-fsanitize=address for sanitizers).
• Linker Flags: Depending on the build type, CMake may add different linker options.
For example, a release build might include additional flags to strip debugging
information or reduce the size of the final executable.
• Debugging Symbols: In the Debug build type, CMake ensures that debugging
symbols are included, which are necessary for debugging tools like gdb or lldb.
In contrast, in Release builds, debugging symbols are typically omitted to
improve performance and reduce the size of the binary.
• Optimization: Release builds enable higher levels of optimization, leading to faster
execution times, while debug builds avoid optimization to make stepping through
code easier in a debugger. RelWithDebInfo provides a middle ground by
optimizing the code while still including some debug information.
• Conditional Code: Some code in the project might only be included in specific
build types. For example, CMake allows conditional inclusion of certain code
depending on the build type, using constructs like if(CMAKE BUILD TYPE
MATCHES "Debug").

3.5.4 Multi-Configuration Generators and CMAKE BUILD TYPE

As mentioned earlier, CMAKE BUILD TYPE is mainly used with single-configuration


generators like Makefiles or Ninja. For multi-configuration generators such as Visual
Studio or Xcode, the build type is usually determined during the actual build process, not
during the configuration phase.
For example, with Visual Studio, you can select the build configuration (such as Debug,
Release, or others) when opening the project in the Visual Studio IDE. You don’t need to
138

specify the build type during the configuration step with Visual Studio because the IDE
handles it dynamically.

Example for Visual Studio

When generating build files for Visual Studio, you do not need to specify the build type at
the configuration step:

cmake -G "Visual Studio 16 2019" /path/to/source

After running this command, you can open the generated .sln file in Visual Studio and
select the build configuration (Debug, Release, etc.) from the IDE’s build settings.

Similarly, with Xcode, you can specify the configuration directly within Xcode once the
project has been generated.

3.5.5 Customizing Build Types

CMake also allows you to customize build types by defining your own custom
configurations. For example, you may want to create a special configuration that combines
optimizations and additional warnings or debugging checks for a particular use case.

To do this, you can define custom build types by adding to the CMAKE CXX FLAGS
variable in your CMakeLists.txt. For example:

set(CMAKE_CXX_FLAGS_CUSTOM "-O2 -Wall -DDEBUG")

Then, when running the configuration, you can specify this custom type:
139

set(CMAKE_CXX_FLAGS_CUSTOM "-O2 -Wall -DDEBUG")

This approach provides flexibility, especially in complex projects or when dealing with
specialized requirements like performance profiling or testing.

3.5.6 Advanced Control of Build Options

CMake provides additional mechanisms to control build behavior beyond just


CMAKE BUILD TYPE. Here are some of the most commonly used variables that work
alongside CMAKE BUILD TYPE:

• CMAKE CXX FLAGS DEBUG: This variable allows you to add or modify flags
specifically for the Debug build type.
• CMAKE CXX FLAGS RELEASE: This variable lets you adjust flags for the Release
build type.
• CMAKE CXX FLAGS RELWITHDEBINFO: Adjusts flags for the RelWithDebInfo
build type.
• CMAKE CXX FLAGS MINSIZEREL: Used to modify flags for the MinSizeRel
build type.

For example, you might want to modify the compiler flags to add more strict debugging or
security checks for the Debug build:

set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}
,→ -fsanitize=address")

This will enable AddressSanitizer in the debug configuration, helping to catch memory
errors.
140

3.5.7 Conclusion

CMAKE BUILD TYPE is a powerful tool in CMake for controlling the type of build you
generate, whether you're working on a development/debugging phase or preparing for a
production release. By setting CMAKE BUILD TYPE, you can adjust compiler and linker
flags, optimization levels, and debugging information to match the needs of your project.
For single-configuration generators, setting the CMAKE BUILD TYPE is crucial for
customizing the build behavior. For multi-configuration generators like Visual Studio and
Xcode, this step is less critical, as these IDEs let you select the build configuration during
the actual build process. By combining CMAKE BUILD TYPE with other CMake features
like custom flags and multi-stage builds, you can fine-tune your project’s build process
and make it more efficient and easier to maintain.
Chapter 4

Working with Libraries in CMake


(Static & Shared)

4.1 Difference Between Static and Shared Libraries

In CMake, when building C++ projects, libraries play an essential role in modularizing the
code, making it reusable, and simplifying the build process. These libraries can be either
static libraries or shared libraries, and understanding the differences between them is
key to managing dependencies and ensuring the correct build setup for your project. This
section dives into the key differences between static and shared libraries, their use cases,
and how they affect the build process.

4.1.1 Definition

• Static Libraries: A static library, also known as an archive in C++, is a collection


of object files that are bundled into a single file. During the build process, the linker

141
142

copies the object code from the static library directly into the executable. As a result,
the executable does not need the static library at runtime because all the necessary
code is included within the executable itself.
• Shared Libraries: Also known as dynamic libraries or DLLs (Dynamic Link
Libraries) on Windows, shared libraries contain code that is linked at runtime
rather than during the compile time. The executable or other libraries that use the
shared library will dynamically load it during execution. This means the executable
depends on the shared library being present in the system at runtime to function
correctly.

4.1.2 Build Process

• Static Libraries:
– The static library is compiled from the source code into object files. These
object files are then bundled together into a single library file, typically with a
.a extension on Unix-like systems and .lib on Windows.
– During linking, the entire library is copied into the executable. This process
results in larger executable files but does not require the library to be available
during runtime.
– Static libraries are commonly used for applications that require all dependencies
to be packaged into a single executable.
• Shared Libraries:
– A shared library is compiled and linked in a way that only the symbol
information (function names, data structures, etc.) is placed in the executable.
The actual code for these functions is placed in the shared library itself.
– The executable is not directly linked to the code but to the dynamic library file.
The linking to the library happens at runtime when the program is executed.
143

– Shared libraries are typically used in systems where multiple applications or


components can benefit from the same code base, reducing the size of the
executables and enabling the sharing of common functionality.

4.1.3 Key Differences Between Static and Shared Libraries

Aspect Static Libraries Shared Libraries


Linking Time Linked during compile time. Linked during runtime.
Dependency No dependency at runtime (code is Requires the shared library to be
at Runtime embedded). available at runtime.
File Size Larger executable size (includes the Smaller executables (library code is
library code). external).
Performance Faster startup time (no need to load Slightly slower startup time
libraries). (requires loading the library at
runtime).
Memory Higher memory usage, as each Lower memory usage, as the
Usage process has its own copy of the same shared library is used across
library code. multiple processes.
Updates/ Requires recompilation of the Easy to update; only the shared
Versioning executable if the library changes. library file needs to be updated.
Platform Platform-specific (a static library is Also platform-specific, but can be
Dependency specific to a platform). shared across different applications.
Portability Less portable; each executable More portable; can be shared across
contains its own copy of the library. multiple systems.
144

4.1.4 Advantages and Disadvantages

1. Static Libraries
Advantages:

• Standalone Executable: The executable is independent and contains all the


necessary code. This makes it easier to distribute because there are no external
dependencies.
• Faster Execution: Since the linking is done at compile time, the application
may have slightly faster execution times compared to shared libraries, which
require runtime linking.
• No Versioning Issues: With static libraries, the version of the library used
during compilation is always the one included in the executable, avoiding
potential issues with incompatible versions of shared libraries.

Disadvantages:

• Larger Executables: The final executable file size is larger because it includes
all the code from the static libraries.
• No Shared Memory: Each running instance of the application gets its own
copy of the static library code, leading to higher memory usage when the
program is running.
• Updates Require Rebuilding: If the library code is updated, all executables
that use it must be recompiled and redistributed.

2. Shared Libraries
Advantages:

• Smaller Executables: The executable file is smaller because it doesn't contain


the code from the shared libraries; it only contains references to them.
145

• Shared Memory: Multiple running instances of a program can share the same
loaded version of a shared library, which reduces overall memory usage.
• Easier Updates: When a shared library is updated, all executables that depend
on it will benefit from the update immediately without needing recompilation.

Disadvantages:

• Dependency Management: Shared libraries must be available at runtime. This


can lead to dependency hell when different applications require different
versions of the same library.
• Slower Startup: There is a slight overhead when loading shared libraries at
runtime.
• Potential Compatibility Issues: If an application expects a particular version
of a shared library, updating the library may cause compatibility issues unless
proper versioning is handled.

4.1.5 Use Cases

• Static Libraries:

– Ideal for standalone applications that do not need to rely on external libraries
at runtime.
– Useful in embedded systems, where minimizing external dependencies is
crucial.
– Used when you want to distribute a single file containing all necessary
components, such as in a proprietary application or for performance-sensitive
applications.

• Shared Libraries:
146

– Perfect for applications that share common functionality. This allows


multiple programs to link to the same library, reducing redundancy and making
updates easier.
– Common in large-scale enterprise applications where different components
need to use the same underlying functionality.
– Beneficial in systems where memory usage and disk space need to be
minimized, as shared libraries can be loaded once and used by multiple
processes.

4.1.6 CMake Configuration for Static and Shared Libraries

In CMake, you can specify whether to create a static or shared library using the
add library() command. Here’s how you would do that:

• Static Library:

add_library(my_library STATIC src/my_library.cpp)

• Shared Library:

add_library(my_library SHARED src/my_library.cpp)

You can also specify different build types (Release, Debug) and link libraries conditionally
based on the type of build being performed.

4.1.7 Conclusion

Choosing between static and shared libraries depends on the specific requirements of your
project. Static libraries are useful for reducing external dependencies, making the build
147

simpler, and ensuring that the application is completely self-contained. Shared libraries,
on the other hand, are more efficient in terms of memory and disk space, especially when
the same code is used across multiple applications or processes.
In CMake, it is straightforward to configure either type of library, but it is important to
understand the implications of your choice to make informed decisions about the
architecture of your project.
148

4.2 Creating a Static Library (add library(MyLib


STATIC))

In this section, we will explore how to create a static library in CMake. A static library is
a collection of object files bundled together into a single file. This file can be linked into
executables at compile-time, resulting in larger executables but ensuring that no external
dependencies are required at runtime.

Creating a static library in CMake is simple, but understanding the process and
configuration is crucial to ensure it integrates seamlessly into your build system. This
section will cover the add library() command in CMake, the steps to create a static
library, and the typical workflow involved in using static libraries within a C++ project.

4.2.1 Understanding Static Libraries in CMake

A static library is a collection of precompiled object files (typically .o or .obj files)


packed into a single archive file (usually .a on Unix-like systems or .lib on Windows).
The code in a static library is copied into the final executable at compile time, making
the executable self-contained.

In CMake, the add library() command is used to create libraries, and the STATIC
keyword specifies that the library will be a static library.

4.2.2 Basic Syntax of add library()

The basic syntax for creating a static library in CMake is:


149

add_library(<library_name> STATIC <source_files>)

Where:

• <library name>: The name you want to give to your library (e.g., MyLib).
• STATIC: Specifies that the library is a static library.
• <source files>: The list of source files to include in the library, such as .cpp
files.

For example, to create a static library MyLib from the source files my lib.cpp and
my lib utils.cpp, you would write:

add_library(MyLib STATIC my_lib.cpp my_lib_utils.cpp)

4.2.3 Detailed Example: Creating a Static Library

Let’s walk through an example to create a static library MyLib in a CMake project:

1. Create the directory structure:

MyProject/
CMakeLists.txt
src/
CMakeLists.txt
my_lib.cpp
my_lib_utils.cpp
main.cpp
150

2. Top-level CMakeLists.txt: At the top level, you’ll specify the minimum


required version of CMake, the project name, and include the src directory for the
actual library creation.

cmake_minimum_required(VERSION 3.10)
project(MyProject)

add_subdirectory(src) # Include the 'src' directory to build the


,→ library

3. src/CMakeLists.txt: In the src/CMakeLists.txt, you create the static


library from the source files my lib.cpp and my lib utils.cpp:

# Create a static library 'MyLib' from the source files


add_library(MyLib STATIC my_lib.cpp my_lib_utils.cpp)

# Specify the include directories if needed


target_include_directories(MyLib PUBLIC
,→ ${CMAKE_CURRENT_SOURCE_DIR})

4. src/my lib.cpp: This is the main source file for the static library.

// my_lib.cpp
#include "my_lib.h"

void MyLib::doSomething() {
// Implement some functionality here
}

5. src/my lib utils.cpp: Another source file that adds utility functions to the
library.
151

// my_lib_utils.cpp
#include "my_lib_utils.h"

int MyLibUtils::add(int a, int b) {


return a + b;
}

6. main.cpp: The main executable that links to the static library MyLib.

#include <iostream>
#include "my_lib.h"
#include "my_lib_utils.h"

int main() {
MyLib lib;
lib.doSomething();

MyLibUtils utils;
std::cout << "Sum: " << utils.add(3, 4) << std::endl;

return 0;
}

7. Building the Project: After configuring CMake, you can build the project:

mkdir build
cd build
cmake ..
make

This process will compile the static library and link it to the main executable.
152

4.2.4 Linking the Static Library

Once the static library is created, you need to link it to the executable that depends on it.
In CMake, this is done using the target link libraries() command.

For example, in the CMakeLists.txt file for the main project:

add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib) # Link the static library

This will ensure that MyApp links against the static library MyLib during the linking
phase, incorporating the object files from MyLib into the final executable.

4.2.5 Using target include directories()

When creating a static library, you often want to make sure that the header files used by
the library are accessible to other parts of the project. The
target include directories() command is used to specify the include
directories for the library.

In the src/CMakeLists.txt file, you can add:

target_include_directories(MyLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

• PUBLIC: This keyword makes the include directory available both to the static
library and to any target that links against the library (like MyApp).

• PRIVATE: If the include directory is only needed internally by the static library, you
can use PRIVATE instead.
153

4.2.6 Handling Dependencies in Static Libraries

If your static library has dependencies on other libraries (e.g., third-party libraries), you
need to link those libraries within your CMake configuration.

For example, if MyLib depends on another library OtherLib, you would modify the
CMake configuration as follows:

# Create a static library 'MyLib'


add_library(MyLib STATIC my_lib.cpp my_lib_utils.cpp)

# Link the static library 'OtherLib' to 'MyLib'


target_link_libraries(MyLib PRIVATE OtherLib)

This ensures that when MyLib is linked to an executable or another library, the
OtherLib will also be included as a dependency.

4.2.7 Using CMake Variables for Source Files

Instead of hardcoding the list of source files, you can use CMake variables to collect them,
especially if you have a large number of files or want to avoid manual updates.

For example:

set(SOURCES
my_lib.cpp
my_lib_utils.cpp
)

add_library(MyLib STATIC ${SOURCES})


154

This approach is useful when organizing larger projects with multiple source files.

4.2.8 Installing the Static Library

If you want to install the static library for use outside the current project, you can use the
install() command. This is often done to create a CMake package for others to use.
For example:

install(TARGETS MyLib DESTINATION lib)


install(FILES my_lib.h my_lib_utils.h DESTINATION include)

This will install the MyLib static library to the lib directory and the headers to the
include directory.

4.2.9 Advantages of Static Libraries

• Self-contained: The executable does not require external dependencies at runtime,


making distribution easier.
• Performance: Static linking can result in faster program startup time because all
code is embedded into the executable.
• No Versioning Issues: The specific version of the library used during compilation is
included in the executable, avoiding potential issues caused by mismatched versions
at runtime.

4.2.10 Disadvantages of Static Libraries

• Larger Executables: The executable becomes larger since it includes all the
necessary object files from the static library.
155

• No Shared Memory: Each running instance of the program gets its own copy of the
library code, leading to higher memory consumption.
• Rebuild Required: If the static library is updated, you need to rebuild and
redistribute all executables that depend on it.

4.2.11 Conclusion

Creating a static library in CMake is a straightforward process that involves using the
add library() command with the STATIC keyword. Static libraries are a great
choice when you want self-contained executables and don’t mind the larger file sizes.
They are particularly useful for smaller applications or when you want to avoid runtime
dependencies. By following the steps in this section, you’ll be able to create, link, and
manage static libraries effectively in your C++ projects with CMake.
156

4.3 Creating a Shared Library (add library(MyLib


SHARED))

In this section, we will explore the process of creating a shared library in CMake, which
is also referred to as a dynamic library. Shared libraries differ from static libraries in that
they are linked at runtime, rather than being statically included within the executable.
This allows for smaller executables, easier updates, and shared code across different
applications, but it also introduces some challenges, such as dependency management at
runtime.

This section will guide you through the creation of shared libraries using CMake, explain
the key differences between static and shared libraries, and provide best practices for
managing and linking shared libraries in CMake projects.

4.3.1 Understanding Shared Libraries in CMake

A shared library (also called a dynamic library) is a collection of object files that are
linked at runtime. When an executable or another shared library is linked to a shared
library, the actual code is not included in the executable. Instead, a reference is created,
and the code from the shared library is loaded when the application is run.

• On Unix-like systems (Linux, macOS), shared libraries typically have .so (Shared
Object) extensions.

• On Windows, they are usually referred to as .dll (Dynamic Link Libraries).

The key advantage of using shared libraries is that they can be shared between multiple
programs or processes, which helps reduce memory usage and the overall size of
executables. Additionally, shared libraries can be updated independently without requiring
157

the applications that use them to be recompiled, as long as the interface remains
compatible.

In CMake, shared libraries are created using the add library() command with the
SHARED keyword.

4.3.2 Basic Syntax of add library() for Shared Libraries

The basic syntax to create a shared library in CMake is:

add_library(<library_name> SHARED <source_files>)

Where:

• <library name>: The name of the shared library you want to create (e.g.,
MyLib).

• SHARED: Specifies that the library will be a shared library.

• <source files>: A list of source files, typically .cpp files, to be compiled


into the library.

For example, to create a shared library MyLib from the source files my lib.cpp and
my lib utils.cpp, you would use:

add_library(MyLib SHARED my_lib.cpp my_lib_utils.cpp)


158

4.3.3 Detailed Example: Creating a Shared Library

Let’s walk through a detailed example of creating a shared library MyLib in a


CMake-based project. We’ll also link this shared library to an executable and explore the
necessary configurations.

1. Create the directory structure:

MyProject/
CMakeLists.txt
src/
CMakeLists.txt
my_lib.cpp
my_lib_utils.cpp
main.cpp

2. Top-level CMakeLists.txt: At the top level, specify the minimum required


version of CMake, the project name, and include the src directory to build the
shared library.

cmake_minimum_required(VERSION 3.10)
project(MyProject)

add_subdirectory(src) # Include the 'src' directory to build the


,→ library

3. src/CMakeLists.txt: In the src/CMakeLists.txt, you create the


shared library from the source files my lib.cpp and my lib utils.cpp.
159

# Create a shared library 'MyLib' from the source files


add_library(MyLib SHARED my_lib.cpp my_lib_utils.cpp)

# Specify the include directories if needed


target_include_directories(MyLib PUBLIC
,→ ${CMAKE_CURRENT_SOURCE_DIR})

4. src/my lib.cpp: This is the main source file for the shared library.

// my_lib.cpp
#include "my_lib.h"

void MyLib::doSomething() {
// Implement some functionality here
}

5. src/my lib utils.cpp: Another source file that adds utility functions to the
library.

// my_lib_utils.cpp
#include "my_lib_utils.h"

int MyLibUtils::add(int a, int b) {


return a + b;
}

6. main.cpp: The main executable that will link to the shared library MyLib.

#include <iostream>
#include "my_lib.h"
#include "my_lib_utils.h"
160

int main() {
MyLib lib;
lib.doSomething();

MyLibUtils utils;
std::cout << "Sum: " << utils.add(3, 4) << std::endl;

return 0;
}

7. Building the Project: After configuring the CMake project, you can build it using
the following commands:

mkdir build
cd build
cmake ..
make

This process will compile the shared library MyLib, create the corresponding shared
object (.so or .dll), and link it to the executable MyApp.

4.3.4 Linking the Shared Library

Once the shared library is created, you need to link it to the executable that depends on it.
This is done using the target link libraries() command in CMake.

For example, in the CMakeLists.txt file for the main project:


161

add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib) # Link the shared library

This command tells CMake to link the MyApp executable with the MyLib shared library
during the linking phase.

4.3.5 Handling RPATH and Shared Library Location

Since shared libraries are loaded at runtime, you must ensure that the shared library is
available to the executable when it runs. This is typically managed using RPATH
(runtime library search path), which tells the system where to look for shared libraries.
You can configure RPATH in CMake with the following commands:

set(CMAKE_INSTALL_RPATH "$ORIGIN") # Set RPATH relative to the


,→ executable's location

• $ORIGIN means that the library will be searched for in the directory where the
executable is located.
• Alternatively, you can specify an absolute path or relative path to the shared library.

When installing the shared library, CMake can also set the proper install paths for the
shared library and the executable:

install(TARGETS MyLib DESTINATION lib)


install(TARGETS MyApp DESTINATION bin)

This ensures that the shared library is placed in the correct lib directory, and the
executable is placed in the bin directory.
162

4.3.6 Using target include directories()

As with static libraries, you can use target include directories() to specify
the include directories for the shared library:

target_include_directories(MyLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

The PUBLIC keyword indicates that this include directory is necessary not only for the
library itself but also for any executable or library that links to it.

4.3.7 Versioning Shared Libraries

Shared libraries often require versioning to ensure that applications can link to a specific
version of the library. To manage versioned shared libraries in CMake, you can use the
following syntax:

add_library(MyLib SHARED my_lib.cpp my_lib_utils.cpp)

set_target_properties(MyLib PROPERTIES
VERSION 1.0.0
SOVERSION 1
)

• VERSION: Specifies the full version of the shared library.

• SOVERSION: Specifies the API version of the shared library. This version is used
to ensure compatibility between the library and the applications that use it.
163

When building the shared library, CMake will automatically append the version and
SOVERSION to the library filename (e.g., libMyLib.so.1.0.0 or
libMyLib.so.1), which can help avoid version conflicts.

4.3.8 Advantages of Shared Libraries

• Smaller Executables: The executable remains small because the shared library code
is not embedded within the executable.

• Memory Efficiency: Multiple running applications can share the same instance of
the shared library in memory, reducing memory usage.

• Easier Updates: You can update the shared library independently of the applications
that use it, as long as the interface remains backward compatible.

• Modularity: Shared libraries promote modularity and code reuse, as different


applications can use the same shared library.

4.3.9 Disadvantages of Shared Libraries

• Dependency Management: The main disadvantage of shared libraries is that the


application depends on the shared library being available at runtime. If the shared
library is missing, the application will fail to run.

• Versioning Issues: When a shared library is updated, it can potentially break


applications that rely on an older version. Proper versioning and backward
compatibility are essential.

• Slightly Slower Startup: Shared libraries are loaded at runtime, which can result in
a slight delay in application startup.
164

4.3.10 Conclusion

Creating a shared library in CMake is a powerful way to modularize your C++ projects,
reduce the size of executables, and promote code reuse across multiple applications. By
using the add library(MyLib SHARED) command, you can easily create shared
libraries, link them to executables, and manage dependencies efficiently.
However, as with any dynamic linking, shared libraries introduce challenges such as
dependency management and versioning, which need to be carefully managed to ensure
the stability of your application. With proper setup and configuration, shared libraries can
significantly enhance the maintainability and scalability of your C++ projects.
165

4.4 Linking Libraries (target link libraries)

Linking libraries is a fundamental concept when building C++ projects using CMake. The
target link libraries() command in CMake is used to specify which libraries
(static or shared) an executable or another library should be linked with. Proper linking
ensures that all necessary code from external libraries (whether static or shared) is
incorporated into the final executable or library.

In this section, we will explore the target link libraries() command in detail,
how it works with static and shared libraries, and provide practical examples to
demonstrate how to manage and link libraries effectively in your CMake project.

4.4.1 Understanding the Purpose of target link libraries()

When you create a library or executable in CMake, it typically relies on external libraries
to provide functionality that isn't part of the C++ standard library. These external libraries
could be static libraries (which are compiled into the executable at compile time) or
shared libraries (which are linked at runtime). The target link libraries()
command is used to link an executable or library to one or more of these external libraries.

The basic syntax of the target link libraries() command is as follows:

target_link_libraries(<target> <libraries>)

Where:

• <target>: The name of the target (either an executable or another library) that
you want to link the libraries to.
166

• <libraries>: The libraries that the target should be linked with. This can be a
single library or a list of libraries.

For example:

add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib)

This command tells CMake to link the MyApp executable with the library MyLib.

4.4.2 Linking Static Libraries

When linking static libraries, CMake will ensure that the object files from the static library
are copied into the final executable during the linking phase. Static libraries are included
in the executable at compile-time, making the executable self-contained.
Here’s how to link a static library to an executable using
target link libraries():

# Create a static library


add_library(MyLib STATIC my_lib.cpp my_lib_utils.cpp)

# Create an executable
add_executable(MyApp main.cpp)

# Link the static library 'MyLib' to the executable 'MyApp'


target_link_libraries(MyApp PRIVATE MyLib)

In this case, MyApp depends on MyLib, and the MyLib library will be included directly
into the final executable. The PRIVATE keyword indicates that MyLib is needed only for
MyApp and does not need to be propagated to other targets that depend on MyApp.
167

4.4.3 Linking Shared Libraries

When linking shared libraries, the executable or library doesn’t include the shared library's
object files at compile time. Instead, a reference to the shared library is included, and the
actual code is loaded into memory when the program runs (at runtime). This means the
shared library must be available when the program starts.

Here's how to link a shared library to an executable:

# Create a shared library


add_library(MyLib SHARED my_lib.cpp my_lib_utils.cpp)

# Create an executable
add_executable(MyApp main.cpp)

# Link the shared library 'MyLib' to the executable 'MyApp'


target_link_libraries(MyApp PRIVATE MyLib)

In this case, MyApp will not contain the code from MyLib directly, but it will rely on the
shared library at runtime. The shared library (MyLib.so, .dll, or .dylib) must be
found in the system’s library search paths when the executable is run.

4.4.4 Specifying Link Dependencies

The target link libraries() command can also be used to specify a target’s
dependencies on other libraries. These dependencies can either be private, public, or
interface:

• PRIVATE: The library is only required for the target itself. Other targets that link to
this target do not need to know about this library.
168

• PUBLIC: The library is required for the target, and any other target that links to this
target will also need to link to this library.

• INTERFACE: The library is not required for the target itself, but any target that
links to this target will need to link to the library.

Here's an example with each type of dependency:

# Create a static library


add_library(MyLib STATIC my_lib.cpp my_lib_utils.cpp)

# Create a shared library


add_library(OtherLib SHARED other_lib.cpp)

# Create an executable
add_executable(MyApp main.cpp)

# Link libraries with different visibility


target_link_libraries(MyApp PRIVATE MyLib) # Only MyApp needs
,→ MyLib
target_link_libraries(MyApp PUBLIC OtherLib) # MyApp and any
,→ other target linking MyApp will need OtherLib

• MyLib is linked only for MyApp and won’t propagate to any target that depends on
MyApp (via the PRIVATE keyword).

• OtherLib is required both for MyApp and for any target that links against MyApp
(via the PUBLIC keyword).
169

4.4.5 Linking Multiple Libraries

You can link multiple libraries to a target at once. In this case, simply list the libraries
separated by spaces:

# Create an executable
add_executable(MyApp main.cpp)

# Link multiple libraries


target_link_libraries(MyApp PRIVATE MyLib OtherLib AnotherLib)

In this example, MyApp is linked with three libraries: MyLib, OtherLib, and
AnotherLib.

4.4.6 Linking System Libraries

In addition to linking libraries that are part of your project, you may need to link to system
libraries or third-party libraries installed on your system (such as pthread, zlib, or
boost). These libraries can be linked in the same way:

# Link to system libraries like pthread and zlib


target_link_libraries(MyApp PRIVATE pthread zlib)

You can also use find package() to find installed libraries, such as Boost, and then
link them to your target:

find_package(Boost REQUIRED)

add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE Boost::Boost)
170

In this case, CMake will search for the Boost library on your system and link it to MyApp.

4.4.7 Handling Transitive Dependencies

When a target is linked to another target that itself has dependencies, CMake will
automatically propagate those dependencies, provided the correct visibility is specified.
For instance, if a shared library OtherLib is linked to MyLib, and MyLib is linked to
MyApp, the dependencies of MyLib will be propagated to MyApp if the PUBLIC or
INTERFACE keyword is used:

add_library(MyLib SHARED my_lib.cpp)


add_library(OtherLib STATIC other_lib.cpp)

target_link_libraries(MyLib PUBLIC OtherLib) # MyLib depends on


,→ OtherLib

add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib) # MyApp will also depend
,→ on OtherLib

Here, since MyLib is linked to OtherLib using PUBLIC, MyApp will also implicitly
depend on OtherLib when it links to MyLib.

4.4.8 Linking Libraries with Custom Paths

In some cases, your libraries may not be located in standard directories (such as
/usr/lib or /lib). You can specify custom library paths using the
link directories() command:
171

link_directories(/path/to/custom/libs)

add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib)

This command tells CMake to look for libraries in the specified directory when linking.
However, it’s usually better practice to use find package() for third-party libraries or
to specify the full path in target link libraries() to avoid issues with finding
libraries across different systems.

4.4.9 Best Practices for Linking Libraries

Here are some best practices to keep in mind when linking libraries with
target link libraries():

• Be explicit: Always specify whether the library should be linked as PRIVATE,


PUBLIC, or INTERFACE. This makes the dependencies clear and avoids
unnecessary linking.

• Avoid link directories() when possible: Rather than relying on


link directories(), prefer to specify full library paths or use
find package() for external libraries. This makes your project more portable.

• Group related libraries: If you have a large number of dependencies, consider


grouping them logically (e.g., utility libraries, third-party libraries) and documenting
them for easier maintenance.

• Use find package() for system libraries: For commonly used external
libraries (such as Boost, OpenSSL, or zlib), use CMake’s find package()
functionality to automatically locate and link these libraries.
172

4.4.10 Conclusion

The target link libraries() command is a critical part of CMake’s functionality


for managing dependencies between libraries and executables. By properly linking your
targets, you ensure that all necessary code is included during the linking phase, whether
you are using static or shared libraries. Understanding the PRIVATE, PUBLIC, and
INTERFACE keywords allows you to manage dependencies efficiently, ensuring your
project is modular and maintainable.
With the knowledge gained in this section, you can confidently link multiple libraries,
manage dependencies across targets, and ensure your CMake projects are properly
structured and portable across different systems.
173

4.5 Controlling Symbol Visibility (PUBLIC, PRIVATE,


INTERFACE)

4.5.1 Introduction to Symbol Visibility in CMake

When working with libraries in CMake, controlling symbol visibility is crucial for
managing dependencies, improving build efficiency, and ensuring modularity. The
PUBLIC, PRIVATE, and INTERFACE keywords help define how include directories,
compile options, and linking dependencies are propagated between different targets.
Understanding these keywords allows developers to:

• Control which dependencies are exposed to consumers of a library.


• Optimize compilation by limiting unnecessary propagation of dependencies.
• Improve maintainability by keeping libraries self-contained.

This section explains the role of these keywords in CMake, how they affect compilation
and linking, and provides real-world examples.

4.5.2 Understanding PUBLIC, PRIVATE, and INTERFACE

The PUBLIC, PRIVATE, and INTERFACE keywords in CMake define how compile
options, include directories, and library dependencies are applied to a target and its
consumers.
These keywords are primarily used with:

• target link libraries() – Specifies library dependencies.


• target include directories() – Specifies include directories.
174

• target compile options() – Specifies compilation flags or options.

• PRIVATE
– The setting applies only to the target itself.
– It does not propagate to other targets that depend on this target.
• PUBLIC
– The setting applies to the target itself and also to any targets that link to it.
– Useful for dependencies that must be available both during compilation of the
library and when used by consumers.
• INTERFACE
– The setting applies only to targets that link to this library.
– The target itself does not use the setting.
– Useful for header-only libraries or when exposing dependencies to consumers
without affecting the library itself.
The following table summarizes how these keywords behave:

Keyword Affects the Library Itself? Affects Consumers of the


Library?
PRIVATE Yes No
PUBLIC Yes Yes
INTERFACE No Yes

4.5.3 Controlling Include Directories with


target include directories()

The target include directories() command specifies where the compiler


should look for header files. The PUBLIC, PRIVATE, and INTERFACE keywords define
175

how include directories are applied to the library and its consumers.

• Example: Include Directories with Visibility Control


Consider the following CMake project structure:

MyProject/
CMakeLists.txt
src/
CMakeLists.txt
include/
MyLib.h
my_lib.cpp
my_lib_utils.cpp
my_lib_utils.h
main.cpp

• Using PRIVATE Include Directories

add_library(MyLib STATIC my_lib.cpp my_lib_utils.cpp)

# This include directory is only used internally by MyLib


target_include_directories(MyLib PRIVATE
,→ ${CMAKE_CURRENT_SOURCE_DIR}/include)

– MyLib uses headers from include/, but these headers are not exposed to
consumers.
– If another target links against MyLib, it won’t see this include directory.

• Using PUBLIC Include Directories


176

target_include_directories(MyLib PUBLIC
,→ ${CMAKE_CURRENT_SOURCE_DIR}/include)

– MyLib and any target linking to MyLib will use the include/ directory.
– If MyLib.h is part of the public API, it must be accessible to consumers.
• Using INTERFACE Include Directories

target_include_directories(MyLib INTERFACE
,→ ${CMAKE_CURRENT_SOURCE_DIR}/include)

– MyLib itself does not use this include directory.


– However, any target that links against MyLib will see include/.
– Useful for header-only libraries where no compilation is needed.

4.5.4 Controlling Linking with target link libraries()

When linking a library to an executable or another library, PUBLIC, PRIVATE, and


INTERFACE determine how dependencies are propagated.

Example: Linking Libraries with Visibility Control

add_library(MyLib STATIC my_lib.cpp my_lib_utils.cpp)


add_library(OtherLib SHARED other_lib.cpp)

# Different visibility levels


target_link_libraries(MyLib PRIVATE OtherLib) # MyLib depends on
,→ OtherLib, but consumers don’t
target_link_libraries(MyLib PUBLIC OtherLib) # MyLib and its
,→ consumers require OtherLib
177

target_link_libraries(MyLib INTERFACE OtherLib) # Only consumers of


,→ MyLib need OtherLib

Visibility Effect
PRIVATE MyLib needs OtherLib, but consumers of MyLib don’t need it.
PUBLIC MyLib and all targets linking to MyLib also need OtherLib.
INTERFACE MyLib itself doesn’t use OtherLib, but consumers must link to it.

4.5.5 Controlling Compile Options with


target compile options()

The target compile options() command applies compiler flags to a target and
can use visibility keywords.

Example: Compiler Options with Visibility Control

add_library(MyLib STATIC my_lib.cpp my_lib_utils.cpp)

# Only MyLib is compiled with -Wall


target_compile_options(MyLib PRIVATE -Wall)

# MyLib and any target linking to MyLib will be compiled with -DDEBUG
target_compile_options(MyLib PUBLIC -DDEBUG)

# Consumers of MyLib get -O2, but MyLib itself does not


target_compile_options(MyLib INTERFACE -O2)
178

Visibility Effect
PRIVATE -Wall applies only to MyLib.
PUBLIC -DDEBUG applies to MyLib and its consumers.
INTERFACE -O2 applies only to consumers of MyLib.

4.5.6 Choosing the Right Visibility for Your Library

Here are guidelines to help choose the correct visibility:

• Use PRIVATE for internal dependencies that should not be exposed.

• Use PUBLIC when the dependency is required by both the target and its
consumers (e.g., a core utility library).

• Use INTERFACE when only consumers need the dependency, such as a


header-only library.

Example Use Cases

Scenario Visibility Why?


Library needs a private helper PRIVATE The helper library should not be exposed
library to consumers.
A framework library exposes PUBLIC Both the library and its users require the
a public API headers and linked dependencies.
A header-only library INTERFACE The library itself doesn’t compile, but
consumers need access to its headers.
179

4.5.7 Conclusion

Understanding PUBLIC, PRIVATE, and INTERFACE in CMake is essential for


controlling include directories, linked libraries, and compiler options. Proper use of
these keywords ensures modularity, optimizes dependency management, and keeps
libraries self-contained while exposing only necessary parts to consumers.
By applying these principles, you can build scalable, maintainable, and efficient C++
projects using CMake.
Chapter 5

Organizing Large-Scale Projects with


CMake

5.1 Understanding Multi-File Project Structure

5.1.1 Introduction to Multi-File Project Structure

As C++ projects grow in complexity, they often transition from a single-file structure to a
multi-file structure. While small projects may have a single main.cpp file containing
all logic, larger projects require modular organization to improve maintainability,
reusability, and scalability.
CMake provides robust mechanisms to structure large-scale projects effectively. A
well-organized project:

• Encourages code modularity by separating concerns into different files.


• Facilitates compilation efficiency by enabling incremental builds.

180
181

• Simplifies dependency management by clearly defining relationships between


components.
• Enhances collaboration by making it easier for multiple developers to work on
different parts of the codebase.

This section explores how to design and implement a multi-file project structure,
including best practices and an example project setup using CMake.

5.1.2 Evolution of Project Structure

• Single-File Projects (Simple Programs)


In the early stages of development, a C++ project may consist of just one or two
source files.
Example (single file project/):

single_file_project/
CMakeLists.txt
main.cpp

main.cpp:

#include <iostream>

int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}

This structure is suitable for small prototypes or simple scripts, but as the codebase
grows, this monolithic file becomes difficult to manage.
182

• Multi-File Projects (Modular Approach)


To manage complexity, larger projects are broken into multiple files, often structured
into header (.h) files, source (.cpp) files, and separate libraries.
A typical multi-file project includes:

1. A main executable (main.cpp) – Entry point of the application.


2. A src/ directory – Contains implementation files (.cpp).
3. An include/ directory – Contains header files (.h) for function/class
declarations.
4. A lib/ directory – Stores external or custom libraries.
5. A CMakeLists.txt for each directory – Defines how each component is
built.

Example (multi file project/):

multi_file_project/
CMakeLists.txt # Root CMake file
src/ # Source code directory
CMakeLists.txt # CMake file for source files
main.cpp # Main application entry
MyLibrary.cpp # Implementation of MyLibrary
MyLibrary.h # Header for MyLibrary
Utilities.cpp # Additional source file
include/ # Header files
MyLibrary.h
Utilities.h
lib/ # External or custom libraries
ThirdPartyLib/
CMakeLists.txt
third_party.cpp
third_party.h
183

build/ # Build directory (generated by CMake)

5.1.3 Setting Up a Multi-File Project with CMake

1. Root CMakeLists.txt
At the root of the project, CMakeLists.txt sets up the project name, minimum
required CMake version, and subdirectories for modular components.

cmake_minimum_required(VERSION 3.10)
project(MyProject)

# Add subdirectories
add_subdirectory(src)
add_subdirectory(lib/ThirdPartyLib)

# Specify the C++ standard


set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

2. src/CMakeLists.txt (Managing Source Files)


The src/ directory contains the main application and related source files. Here, we
define an executable target and specify dependencies.

# Add the executable


add_executable(MyApp main.cpp MyLibrary.cpp Utilities.cpp)

# Include the "include/" directory so headers can be found


target_include_directories(MyApp PRIVATE
,→ ${PROJECT_SOURCE_DIR}/include)
184

# Link with third-party libraries if needed


target_link_libraries(MyApp ThirdPartyLib)

Explanation:
• add executable(MyApp ...) – Creates an executable called MyApp
from main.cpp, MyLibrary.cpp, and Utilities.cpp.
• target include directories() – Ensures that headers from
include/ are available.
• target link libraries() – Links MyApp with an external library
(ThirdPartyLib).
3. lib/ThirdPartyLib/CMakeLists.txt (External Libraries)
The lib/ThirdPartyLib/ directory may contain third-party libraries or
custom-built libraries. To build it separately:

add_library(ThirdPartyLib STATIC third_party.cpp)

target_include_directories(ThirdPartyLib PUBLIC
,→ ${CMAKE_CURRENT_SOURCE_DIR})

Here, ThirdPartyLib is compiled as a static library, which can be linked to


other targets.

5.1.4 Benefits of a Multi-File Project Structure

1. Improved Readability and Maintainability


• Each file focuses on a single responsibility, making the project easier to
understand.
185

2. Faster Compilation

• CMake compiles only changed files, reducing build times.

3. Code Reusability

• Libraries (lib/) can be reused across multiple projects.

4. Better Dependency Management

• Using target link libraries(), dependencies are clearly defined.

5. Easier Collaboration

• Multiple developers can work on different parts of the codebase simultaneously.

5.1.5 Best Practices for Structuring Large-Scale Projects

1. Follow a Modular Approach

• Group related code into separate directories (src/, include/, lib/).


• Use CMake subdirectories (add subdirectory()) to organize
components.

2. Keep Headers and Source Files Separate

• Store .h files in include/ and .cpp files in src/.


• Example:

include/MyLibrary.h # Declarations
src/MyLibrary.cpp # Implementations

3. Use Libraries for Code Reuse

• Convert reusable components into static or shared libraries.


186

• Example:

add_library(MyLib STATIC my_lib.cpp)


target_include_directories(MyLib PUBLIC
,→ ${PROJECT_SOURCE_DIR}/include)

4. Clearly Define Dependencies

• Use target link libraries() to specify dependencies explicitly.

5. Use CMAKE SOURCE DIR and CMAKE BINARY DIR Properly

• Use CMAKE SOURCE DIR to reference source files.


• Use CMAKE BINARY DIR for output locations.

6. Encapsulate Configuration in CMakeLists.txt Files

• Each subdirectory should have its own CMakeLists.txt to keep


configurations modular.

5.1.6 Conclusion

As projects grow, adopting a multi-file project structure becomes essential for


maintainability and efficiency. CMake simplifies this process by allowing developers to
organize files into logical components, define dependencies clearly, and manage
builds effectively.
By following best practices such as modular organization, separate header/source files,
and proper use of CMake’s subdirectory and linking mechanisms, developers can
create scalable and maintainable C++ projects.
With this foundation, we can now explore creating and linking static/shared libraries in
the next sections of this chapter.
187

5.2 Creating Subprojects (add subdirectory)

5.2.1 Introduction to Subprojects in CMake

As C++ projects grow, breaking them into subprojects (or modules) improves
modularity, maintainability, and scalability. Instead of managing a large monolithic
codebase, CMake allows projects to be structured into smaller, independent subprojects,
each with its own CMake configuration.
CMake achieves this using the add subdirectory() command, which enables:

• Hierarchical project organization by treating different components as independent


subprojects.
• Incremental compilation by allowing selective rebuilding of modified subprojects.
• Code reuse by defining libraries in separate subprojects and linking them to the
main project.
• Team collaboration by allowing developers to work on different subprojects
independently.

This section explores how to structure and build multi-module C++ projects using
add subdirectory() in CMake.

5.2.2 Understanding add subdirectory()

The add subdirectory() command in CMake allows you to include another


directory as a subproject.

Syntax:
188

add_subdirectory(<source_dir> [<binary_dir>] [EXCLUDE_FROM_ALL])

• <source dir>: The path to the subproject's source directory.


• <binary dir> (optional): The path where the build files for this subdirectory
should be placed.
• EXCLUDE FROM ALL (optional): If specified, the subproject is not included in the
default build target (useful for optional components).

Example Usage

add_subdirectory(lib/MyLibrary)

• This includes lib/MyLibrary/CMakeLists.txt, which defines a library or


module.
• The main project can link to this subproject like any other CMake target.

5.2.3 Structuring a Project with Subprojects

Typical Multi-Module Project Structure


A large-scale project using subprojects may have the following structure:

MyProject/
CMakeLists.txt # Root CMake file
src/ # Main application source
CMakeLists.txt
189

main.cpp
App.cpp
App.h
lib/ # Libraries (subprojects)
MyLibrary/
CMakeLists.txt
MyLibrary.cpp
MyLibrary.h
Utils/
CMakeLists.txt
Utils.cpp
Utils.h
build/ # Build directory (created by CMake)

• The src/ directory contains the main application.


• The lib/ directory contains two subprojects (MyLibrary and Utils).
• Each subproject has its own CMakeLists.txt, allowing independent
compilation.

5.2.4 Defining Subprojects in CMake

1. Root CMakeLists.txt
The main CMakeLists.txt should include all subprojects using
add subdirectory().

cmake_minimum_required(VERSION 3.10)
project(MyProject)

# Set C++ standard


190

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# Add subprojects
add_subdirectory(lib/MyLibrary)
add_subdirectory(lib/Utils)
add_subdirectory(src)

# Define the main executable


add_executable(MyApp src/main.cpp)

# Link subproject libraries


target_link_libraries(MyApp PRIVATE MyLibrary Utils)

Key Points:
• add subdirectory(lib/MyLibrary) includes MyLibrary as a
subproject.
• add subdirectory(lib/Utils) includes Utils as a subproject.
• target link libraries(MyApp PRIVATE MyLibrary Utils)
links these subprojects to the main executable.
2. lib/MyLibrary/CMakeLists.txt (Defining a Subproject)
Each subproject should define its own library and specify include directories.

add_library(MyLibrary STATIC MyLibrary.cpp)

# Specify include directories


target_include_directories(MyLibrary PUBLIC
,→ ${CMAKE_CURRENT_SOURCE_DIR})

• MyLibrary is compiled as a static library.


191

• target include directories() makes its headers available to other


subprojects.

3. lib/Utils/CMakeLists.txt (Another Subproject)

add_library(Utils STATIC Utils.cpp)

# Make include directory available


target_include_directories(Utils PUBLIC
,→ ${CMAKE_CURRENT_SOURCE_DIR})

4. src/CMakeLists.txt (Main Application Subproject)

add_executable(MyApp main.cpp App.cpp)

# Include headers
target_include_directories(MyApp PRIVATE
,→ ${PROJECT_SOURCE_DIR}/lib/MyLibrary)
target_include_directories(MyApp PRIVATE
,→ ${PROJECT_SOURCE_DIR}/lib/Utils)

# Link libraries
target_link_libraries(MyApp PRIVATE MyLibrary Utils)

5.2.5 Benefits of Using add subdirectory() for Subprojects

1. Improved Code Organization

• Each component/library has its own directory and CMake configuration,


keeping the root project clean.

2. Modular Compilation
192

• Only modified subprojects are recompiled during incremental builds.

3. Easier Dependency Management

• Subprojects can be linked only where needed using


target link libraries().

4. Better Team Collaboration

• Teams can work on separate subprojects independently without affecting the


main build.

5. Reusability

• Libraries created in lib/ can be used across multiple projects.

5.2.6 Using EXCLUDE FROM ALL for Optional Subprojects

If a subproject is optional, you can exclude it from the default build using
EXCLUDE FROM ALL.

add_subdirectory(lib/ExperimentalFeature EXCLUDE_FROM_ALL)

• This prevents ExperimentalFeature from being built unless explicitly


requested.

To enable it, use:

cmake -DBUILD_EXPERIMENTAL=ON ..
193

5.2.7 Best Practices for Managing Subprojects in CMake

1. Use add subdirectory() for Modular Organization

• Place independent components in their own directories.

2. Keep Each Subproject Self-Contained

• Each subproject should have its own CMakeLists.txt and include


directories.

3. Use PUBLIC, PRIVATE, and INTERFACE for Proper Dependency Control

• Use target include directories() and


target link libraries() properly.

4. Minimize Cross-Dependencies Between Subprojects

• Avoid unnecessary dependencies between libraries to reduce coupling.

5. Use EXCLUDE FROM ALL for Optional Components

• Keep optional features out of the default build.

5.2.8 Conclusion

The add subdirectory() command is a powerful tool in CMake for organizing


large-scale C++ projects into modular subprojects. By structuring a project into
multiple smaller, independently compiled components, development becomes more
scalable, maintainable, and efficient.
By following best practices for subproject organization, dependency management, and
modular compilation, you can build robust, scalable, and reusable C++ applications
using CMake.
194

5.3 Using find package() to Locate External Libraries

5.3.1 Introduction to find package()

As C++ projects grow in size and complexity, they often rely on external libraries for
additional functionality. Instead of manually managing and compiling dependencies,
CMake provides the find package() command, which enables projects to
automatically locate and use prebuilt libraries.

Using find package(), CMake can:

• Search for installed libraries on the system.

• Retrieve library paths and configuration details automatically.

• Simplify dependency management by reducing the need for manual configuration.

• Improve cross-platform compatibility by adapting to different system


environments.

This section explores how to use find package() to locate external libraries,
configure dependencies, and integrate them into CMake projects effectively.

5.3.2 Understanding find package()

Basic Syntax

find_package(<PackageName> [version] [REQUIRED] [QUIET]


,→ [MODULE|CONFIG])
195

• <PackageName> – The name of the external library (e.g., Boost, OpenSSL,


Eigen).
• version (optional) – Specifies a required version (e.g., 3.2.1).
• REQUIRED (optional) – Fails the configuration if the library is not found.
• QUIET (optional) – Suppresses warnings if the package is missing.
• MODULE or CONFIG (optional) – Specifies whether to look for a module or a config
package.

Example Usage

find_package(OpenSSL REQUIRED)

• This searches for OpenSSL on the system.


• If OpenSSL is found, CMake sets up necessary variables (e.g.,
OPENSSL INCLUDE DIR, OPENSSL LIBRARIES).
• If not found, CMake produces an error due to REQUIRED.

5.3.3 Finding and Linking External Libraries

Example 1: Finding OpenSSL and Linking It

cmake_minimum_required(VERSION 3.10)
project(MyProject)

# Find OpenSSL
find_package(OpenSSL REQUIRED)
196

# Create an executable
add_executable(MyApp main.cpp)

# Link OpenSSL
target_link_libraries(MyApp PRIVATE OpenSSL::SSL OpenSSL::Crypto)

• find package(OpenSSL REQUIRED) locates OpenSSL.

• target link libraries(MyApp PRIVATE OpenSSL::SSL


OpenSSL::Crypto) links OpenSSL libraries.

• This ensures that OpenSSL functions are available for MyApp.

Example 2: Finding and Using Boost

Boost is a widely used C++ library with many modules. Using


find package(Boost), we can include Boost components in a project.

find_package(Boost REQUIRED COMPONENTS filesystem system)

add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE Boost::filesystem Boost::system)

• COMPONENTS filesystem system ensures only required parts of Boost


are found.

• Boost::filesystem and Boost::system are linked to MyApp.


197

5.3.4 Understanding find package() Search Mechanism

How CMake Searches for Packages

When calling find package(), CMake looks in:

1. CMake package registry (˜/.cmake/packages/) – Stores paths of installed


CMake packages.

2. System package managers (e.g., apt, brew, vcpkg, conan).

3. Environment variables (CMAKE PREFIX PATH, CMAKE MODULE PATH).

4. Standard system locations (/usr/lib/, /usr/local/lib/).

If a package isn’t found, install the missing library using:

• Linux: sudo apt install libboost-dev

• macOS: brew install boost

• Windows: vcpkg install boost

5.3.5 Using CONFIG and MODULE Modes

CMake uses two modes to locate packages:

1. Config Mode (CONFIG)


Modern libraries provide a CMake configuration file
(<Package>Config.cmake) that tells CMake where to find headers and
libraries.
198

find_package(OpenSSL CONFIG REQUIRED)

If OpenSSL is installed via vcpkg or conan, CMake finds it using


OpenSSLConfig.cmake.
2. Module Mode (MODULE)
If a package does not provide a Config.cmake file, CMake looks for a
FindModule file (Find<Package>.cmake).

find_package(OpenSSL MODULE REQUIRED)

If FindOpenSSL.cmake exists, CMake uses it to locate OpenSSL.

5.3.6 Handling Missing Dependencies Gracefully

If a dependency is optional, use QUIET to suppress errors:

find_package(OpenSSL QUIET)

To provide a custom error message:

if(NOT OpenSSL_FOUND)
message(FATAL_ERROR "OpenSSL not found! Please install it.")
endif()

5.3.7 Best Practices for Using find package()

1. Always use REQUIRED for critical dependencies


199

find_package(OpenSSL REQUIRED)

2. Specify only needed components

find_package(Boost REQUIRED COMPONENTS filesystem system)

3. Use QUIET for optional dependencies

find_package(OpenSSL QUIET)

4. Provide clear error messages if dependencies are missing

if(NOT OpenSSL_FOUND)
message(FATAL_ERROR "OpenSSL not found! Please install it.")
endif()

5. Set CMAKE PREFIX PATH for custom install locations

cmake -DCMAKE_PREFIX_PATH=/path/to/custom/install ..

5.3.8 Conclusion

The find package() command is an essential tool in CMake for managing external
dependencies efficiently. By using it, projects can automatically detect installed
libraries, link dependencies correctly, and ensure compatibility across platforms.
By following best practices, CMake projects can integrate external libraries seamlessly,
improve modularity, and reduce manual configuration overhead.
200

5.4 Using FetchContent to Download Dependencies at


Build Time

5.4.1 Introduction to FetchContent

In modern C++ projects, managing external dependencies is a crucial aspect of project


organization. Traditionally, dependencies were handled by manually installing libraries,
using package managers, or relying on find package(). However, these approaches
often require preinstalled dependencies and additional configuration.

To address this challenge, CMake provides FetchContent, a powerful module that


allows projects to download, configure, and build dependencies automatically at build
time. This eliminates the need for external package managers and simplifies dependency
management by ensuring that required libraries are fetched dynamically.

Advantages of FetchContent

• No need for preinstalled dependencies – Downloads and builds libraries as part of


the project.

• Improved portability – Ensures dependencies are available regardless of system


configuration.

• Simplifies build setup – Avoids the need for users to manually install libraries.

• Direct integration with CMake targets – Enables easy linking of fetched libraries.

This section explores how to use FetchContent to fetch, configure, and integrate
dependencies in a CMake project.
201

5.4.2 Understanding FetchContent

The FetchContent module provides a mechanism to download source code from


external repositories (e.g., GitHub) and build it within the current project.

Including the FetchContent Module

To use FetchContent, first include the module in CMakeLists.txt:

include(FetchContent)

This makes FetchContent available for downloading and managing dependencies.

Basic Syntax

FetchContent_Declare(
<DependencyName>
GIT_REPOSITORY <URL>
GIT_TAG <TagOrBranch>
)
FetchContent_MakeAvailable(<DependencyName>)

• FetchContent Declare defines an external dependency.

• GIT REPOSITORY specifies the repository URL.

• GIT TAG sets the commit, branch, or tag to use.

• FetchContent MakeAvailable downloads and integrates the dependency.


202

5.4.3 Example: Fetching and Using an External Library

Using FetchContent to Download and Build fmt

fmt is a popular C++ formatting library. To include fmt in a CMake project dynamically,
use FetchContent:

cmake_minimum_required(VERSION 3.14)
project(MyProject)

include(FetchContent)

# Fetch fmt library from GitHub


FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.0.0 # Specify version
)

# Make fmt available for use


FetchContent_MakeAvailable(fmt)

# Define an executable
add_executable(MyApp main.cpp)

# Link fmt to the executable


target_link_libraries(MyApp PRIVATE fmt)

Explanation

1. FetchContent Declare(fmt ...) specifies that fmt should be fetched


203

from GitHub.
2. FetchContent MakeAvailable(fmt) downloads and integrates fmt into
the build.
3. target link libraries(MyApp PRIVATE fmt) links the fmt library to
the executable.

This approach ensures that fmt is automatically downloaded and built if not already
available, eliminating the need for manual installation.

5.4.4 Using FetchContent with CMake Packages

If an external library provides a CMake package (i.e., a Config.cmake file),


FetchContent can integrate it seamlessly.

Example: Fetching and Using googletest (GoogleTest)


GoogleTest (GTest) is a popular testing framework for C++. The following example
shows how to integrate it using FetchContent:

include(FetchContent)

FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.13.0
)

# Disable GoogleTest installation to avoid conflicts


set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of GTest"
,→ FORCE)
204

FetchContent_MakeAvailable(googletest)

add_executable(MyTests test.cpp)

# Link GoogleTest to the test executable


target_link_libraries(MyTests PRIVATE gtest gtest_main)

Key Points

• FetchContent Declare(googletest ...) specifies GoogleTest as a


dependency.

• set(INSTALL GTEST OFF ...) prevents GoogleTest from installing globally.

• FetchContent MakeAvailable(googletest) fetches and builds the


library.

• target link libraries(MyTests PRIVATE gtest gtest main)


links the test executable with GoogleTest.

This ensures that GoogleTest is always available for unit testing, without requiring users
to install it manually.

5.4.5 Handling Already Installed Dependencies

A key advantage of FetchContent is that it avoids redundant downloads if a


dependency is already available.

To check whether a package is already found via find package() before


downloading it:
205

find_package(fmt QUIET)

if(NOT fmt_FOUND)
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.0.0
)
FetchContent_MakeAvailable(fmt)
endif()

How This Works

• First, attempt to find the package (find package(fmt QUIET)).

• If fmt is not found, download it using FetchContent.

This approach allows the project to use a system-installed version of the library if
available, reducing unnecessary downloads.

5.4.6 Using FetchContent with Non-Git Sources

While FetchContent is commonly used with Git repositories, it also supports:

Fetching Tarball Archives

FetchContent_Declare(
mylib
URL https://example.com/mylib.tar.gz
206

URL_HASH SHA256=abcdef1234567890
)
FetchContent_MakeAvailable(mylib)

This downloads a tarball, verifies its integrity with SHA256, and extracts it.

Fetching from Local Directories

FetchContent_Declare(
mylib
SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/mylib
)
FetchContent_MakeAvailable(mylib)

This allows bundling dependencies within the project instead of downloading them
from external sources.

5.4.7 Best Practices for Using FetchContent

1. Use find package() before FetchContent

• Avoid redundant downloads by checking for preinstalled versions.

2. Pin Specific Versions (GIT TAG)

• Always specify a commit/tag to ensure reproducibility.

3. Minimize External Dependencies

• Only use FetchContent for essential libraries.

4. Use FetchContent MakeAvailable() for Seamless Integration


207

• Automatically configures dependencies within the project.

5. Disable Installation for FetchContent Dependencies

• Prevent unnecessary installation of fetched libraries (set(INSTALL GTEST


OFF)).

5.4.8 Conclusion

FetchContent provides an efficient way to manage external dependencies at build


time. By dynamically downloading, configuring, and building libraries, it simplifies
project setup and ensures that dependencies are always available.
By following best practices, developers can integrate third-party libraries seamlessly,
improve build automation, and eliminate manual dependency installation, making
projects more maintainable and portable.
208

5.5 Using ExternalProject Add for External Project


Integration

5.5.1 Introduction to ExternalProject Add

When working on large-scale C++ projects, it is common to depend on external libraries


or third-party projects. These dependencies may need to be downloaded, configured,
built, and installed as part of the project's build process. While find package() and
FetchContent are useful for integrating prebuilt or source-based dependencies, they
do not provide full control over the build process of external projects.

CMake’s ExternalProject Add module addresses this by:

• Allowing external projects to be downloaded, configured, and built independently.

• Supporting various source types (Git, tarballs, local directories).

• Providing custom build commands for fine-grained control over dependencies.

• Ensuring that external dependencies are built before the main project.

This section explores how to use ExternalProject Add to integrate and manage
external dependencies effectively.

5.5.2 UnderstandingExternalProject Add

The ExternalProject Add command is part of CMake’s ExternalProject module,


which provides a way to build an external library separately from the main project.
Unlike FetchContent, which integrates dependencies into the main build tree,
ExternalProject Add treats dependencies as completely separate projects.
209

Including the ExternalProject Module

To use ExternalProject Add, include the module in CMakeLists.txt:

include(ExternalProject)

This makes ExternalProject Add available for managing external dependencies.

Basic Syntax

ExternalProject_Add(
<ProjectName>
GIT_REPOSITORY <URL>
GIT_TAG <TagOrBranch>
PREFIX <Directory>
CMAKE_ARGS <Arguments>
BUILD_COMMAND <BuildCommand>
INSTALL_COMMAND <InstallCommand>
)

• <ProjectName> – The name of the external project.

• GIT REPOSITORY – URL of the Git repository (or URL for tarballs).

• GIT TAG – Commit hash, tag, or branch to checkout.

• PREFIX – The directory where the project will be downloaded and built.

• CMAKE ARGS – Additional arguments to pass to CMake when configuring the


external project.

• BUILD COMMAND – Custom build command (default: make).

• INSTALL COMMAND – Custom install command (default: make install).


210

5.5.3 Example: Building an External Library with


ExternalProject Add

Using ExternalProject Add to Integrate fmt

cmake_minimum_required(VERSION 3.14)
project(MyProject)

include(ExternalProject)

# Define an external project for fmt


ExternalProject_Add(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.0.0
PREFIX ${CMAKE_BINARY_DIR}/external/fmt
CMAKE_ARGS
,→ -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/external/fmt/install
)

# Include fmt headers


include_directories(${CMAKE_BINARY_DIR}/external/fmt/install/include)

# Define an executable
add_executable(MyApp main.cpp)

# Link fmt library manually


target_link_libraries(MyApp PRIVATE
,→ ${CMAKE_BINARY_DIR}/external/fmt/install/lib/libfmt.a)

Explanation
211

1. ExternalProject Add(fmt . . . ) – Downloads fmt from GitHub and builds it.


2. CMAKE INSTALL PREFIX – Specifies where fmt should be installed.
3. include directories(...) – Ensures that the fmt headers are available.
4. target link libraries(...) – Manually links the built fmt library.

5.5.4 Building and Installing External Projects

ExternalProject Add provides full control over the build and installation process.
If a library needs specific configuration options, pass them using CMAKE ARGS.

Example: Custom Build and Install Commands

ExternalProject_Add(
mylib
GIT_REPOSITORY https://github.com/example/mylib.git
GIT_TAG master
PREFIX ${CMAKE_BINARY_DIR}/mylib
CONFIGURE_COMMAND ./configure
,→ --prefix=${CMAKE_BINARY_DIR}/mylib/install
BUILD_COMMAND make -j4
INSTALL_COMMAND make install
)

Key Differences

• CONFIGURE COMMAND – Uses ./configure instead of CMake.


• BUILD COMMAND – Uses make -j4 for parallel builds.
• INSTALL COMMAND – Runs make install to install the library.
212

This flexibility makes ExternalProject Add ideal for integrating projects that do
not use CMake.

5.5.5 Handling Dependencies Between External Projects

Ensuring One Project Builds Before Another

If multiple external projects depend on each other, use DEPENDS to ensure correct build
order.

ExternalProject_Add(
mylib
GIT_REPOSITORY https://github.com/example/mylib.git
PREFIX ${CMAKE_BINARY_DIR}/mylib
)

ExternalProject_Add(
myapp
GIT_REPOSITORY https://github.com/example/myapp.git
PREFIX ${CMAKE_BINARY_DIR}/myapp
DEPENDS mylib
)

• mylib is built before myapp to satisfy dependencies.

5.5.6 Using ExternalProject Add with CMake Targets

Unlike FetchContent, ExternalProject Add does not automatically create


CMake targets. To link an external project as a target, create an imported library:
213

Example: Creating an Imported Target

add_library(fmt STATIC IMPORTED)


set_target_properties(fmt PROPERTIES
IMPORTED_LOCATION
,→ ${CMAKE_BINARY_DIR}/external/fmt/install/lib/libfmt.a
INTERFACE_INCLUDE_DIRECTORIES
,→ ${CMAKE_BINARY_DIR}/external/fmt/install/include
)

target_link_libraries(MyApp PRIVATE fmt)

Why Use Imported Targets?

• Avoids manually specifying library paths.


• Makes target link libraries(MyApp PRIVATE fmt) more readable.
• Ensures correct dependency management.

5.5.7 Differences Between ExternalProject Add and


FetchContent

Feature FetchContent ExternalProject Add


When to Use When sources should be When the external project should be
part of the main build built separately
Dependency Integrated into the main Built as an independent project
Integration CMake project
214

Feature FetchContent ExternalProject Add


Supports Non- No Yes
CMake Projects
Requires No No
Preinstalled
Dependencies
Builds Dependencies No Yes
Separately
Use Case Header-only libraries or Large projects, external
small dependencies dependencies with complex build
steps

5.5.8 Best Practices for Using ExternalProject Add

1. Use DEPENDS to specify build order

• Ensures that dependencies are built before the main project.

2. Use CMAKE ARGS to pass options to external builds

• Example:

ExternalProject_Add(myproj CMAKE_ARGS
,→ -DCMAKE_BUILD_TYPE=Release)

3. Use INSTALL COMMAND to control how external projects are installed

• Avoid unnecessary global installations.

4. Create Imported Targets

• Allows easier linking to the external project.


215

5. Prefer FetchContent for header-only or small dependencies

• FetchContent is simpler when integration within the main project is needed.

5.5.9 Conclusion

The ExternalProject Add module is a powerful tool for integrating external


projects into a CMake build system. Unlike FetchContent, it treats dependencies as
fully independent builds, making it ideal for large, complex dependencies that require
separate configuration, compilation, and installation.
By understanding how to fetch, build, and integrate external projects, developers can
improve project organization, simplify dependency management, and ensure robust
cross-platform builds.
Chapter 6

Dependency Management in CMake

6.1 Using find package() to Locate Installed Libraries

6.1.1 Introduction

When developing a C++ project with CMake, it's common to rely on external libraries to
extend functionality and avoid reinventing the wheel. Managing these dependencies
efficiently is crucial for maintainability and portability. CMake provides the
find package() command as a robust mechanism to locate and integrate installed
libraries on a system. This section delves into how find package() works, its usage
patterns, and best practices.

6.1.2 Understanding find package()

The find package() command in CMake is used to locate external libraries or


packages installed on a system. It determines the necessary include directories, library

216
217

paths, and compile definitions required to use the library in a C++ project.

Basic Syntax

find_package(<PackageName> [version] [REQUIRED] [QUIET] [COMPONENTS


,→ <comp1> <comp2> ...] [CONFIG|MODULE])

Arguments Explanation

• <PackageName> – The name of the package to find (e.g., Boost, Eigen3,


OpenCV).

• version – (Optional) The minimum required version of the package.

• REQUIRED – If specified, CMake will terminate with an error if the package is not
found.

• QUIET – Suppresses messages when the package is not found.

• COMPONENTS – Specifies particular submodules of the package to locate.

• CONFIG or MODULE – Specifies whether to look for a package using a Config-file


or a Module-file approach (explained later).

6.1.3 Locating a Library Using find package()

Example 1: Finding a Library Without Version Specification

Suppose we want to use Eigen3, a popular C++ linear algebra library, in our CMake
project.
218

find_package(Eigen3 REQUIRED)

• This command searches for Eigen3 and ensures it is found before proceeding.

• If Eigen3 is not installed, CMake generates an error and stops the configuration
process.

Example 2: Finding a Library With a Version Requirement

If our project needs Eigen3 version 3.3 or later, we specify it like this:

find_package(Eigen3 3.3 REQUIRED)

• If Eigen3 version 3.3 or higher is found, it proceeds normally.

• If an older version or no installation is found, CMake stops with an error.

Example 3: Using COMPONENTS to Find Specific Parts of a Library

Some libraries provide multiple components. Boost is a common example. If we only


need the filesystem and system components:

find_package(Boost REQUIRED COMPONENTS filesystem system)

• This ensures that both Boost::filesystem and Boost::system are found.

• If any required component is missing, the configuration fails.


219

6.1.4 Config-Mode vs. Module-Mode in find package()

CMake supports two methods to find packages: Config-Mode and Module-Mode.

Config-Mode (CONFIG)

• Used when the library provides its own CMake configuration files
(<PackageName>Config.cmake or
<PackageName>-config.cmake).
• Typically found in installation directories like /usr/lib/cmake/ or custom
locations.
• More reliable than Module-Mode because it contains package-specific settings.

Example:

find_package(OpenCV CONFIG REQUIRED)

• Looks for OpenCVConfig.cmake in standard or user-specified locations.

If OpenCV is found, it provides imported targets such as OpenCV::Core and


OpenCV::ImgProc, which can be linked in a CMake project:

target_link_libraries(my_project PRIVATE OpenCV::Core


,→ OpenCV::ImgProc)

Module-Mode (MODULE)

• CMake ships with built-in Find Modules (Find<PackageName>.cmake) for


common libraries.
220

• These modules contain logic to locate library headers and binaries.

Example:

find_package(OpenGL REQUIRED)

• CMake will look for FindOpenGL.cmake in its module path (usually inside
CMake’s installation directory).

While this method works for many libraries, it is less preferred than Config-Mode
because it may not always align with the latest versions of the library.

6.1.5 Handling Missing Dependencies Gracefully

If a package is not found, the find package() command typically sets a


<PackageName> FOUND variable to FALSE. Developers can check for this and
provide alternative actions:

find_package(Eigen3 QUIET)

if (NOT Eigen3_FOUND)
message(FATAL_ERROR "Eigen3 was not found! Please install it.")
endif()

Alternatively, an optional dependency can be handled gracefully:

find_package(SDL2 QUIET)

if (SDL2_FOUND)
221

message(STATUS "SDL2 found: ${SDL2_INCLUDE_DIRS}")


else()
message(WARNING "SDL2 not found, disabling SDL2 support.")
endif()

6.1.6 Specifying Custom Paths for Dependencies

If a library is installed in a non-standard location, find package() may fail to locate


it. We can specify additional search paths using:

set(CMAKE_PREFIX_PATH "/custom/install/path" CACHE STRING "Custom


,→ search path for dependencies")
find_package(Eigen3 REQUIRED)

Alternatively, users can provide the path at the CMake command line:

cmake -DCMAKE_PREFIX_PATH=/custom/install/path ..

6.1.7 Using find package() in a Complete Project Example

Project Structure

/my_project
CMakeLists.txt
main.cpp
222

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyProject)

# Locate Eigen3
find_package(Eigen3 REQUIRED)

add_executable(my_project main.cpp)

# Link Eigen3 to the project


target_link_libraries(my_project PRIVATE Eigen3::Eigen)

main.cpp

#include <Eigen/Dense>
#include <iostream>

int main() {
Eigen::Matrix2d mat;
mat << 1, 2,
3, 4;
std::cout << "Matrix:\n" << mat << std::endl;
return 0;
}

Build Instructions

cmake -B build
cmake --build build
223

6.1.8 Best Practices for Using find package()

• Prefer Config-Mode (CONFIG) over Module-Mode whenever possible.


• Use REQUIRED only when the package is essential.
• Use QUIET for optional dependencies.
• Check <PackageName> FOUND before using the package.
• Allow users to specify custom paths using CMAKE PREFIX PATH.
• Provide clear error messages when dependencies are missing.

6.1.9 Conclusion

The find package() command is a fundamental tool in CMake for managing


dependencies. It simplifies integration with external libraries while maintaining portability.
By understanding its syntax, Config/Module modes, and best practices, developers can
create more maintainable and flexible build systems for C++ projects.
224

6.2 Creating Config.cmake Files for Custom Libraries

6.2.1 Introduction

When working with CMake, managing custom libraries or dependencies within your own
projects becomes more seamless if you create and use Config.cmake files. These files
provide a standardized way to define how your library should be located, linked, and
configured within other CMake-based projects. In this section, we’ll discuss how to create
Config.cmake files for custom libraries, which simplifies the integration of your
library into other projects.

6.2.2 What is a Config.cmake File?

A Config.cmake file is a CMake script used to configure and locate a library or


package in a CMake project. It contains necessary information, such as include
directories, library paths, and targets, that other CMake projects can use to link with your
library. It enables a clean, reusable, and easy-to-maintain interface for your library,
allowing other developers to seamlessly find and use it.

In contrast to the module-based approach (via Find<PackageName>.cmake), the


Config approach is more modern and preferable for custom libraries. It explicitly defines
the package, making it more flexible and robust. Typically, Config.cmake files are
used when distributing libraries that are CMake-aware.

6.2.3 Structure of a Config.cmake File

A typical Config.cmake file has the following structure:


225

1. Set Include Directories


Specify the headers or directories to include in other projects that use your library.

2. Set Library Directories


Provide paths to the library files (e.g., .so, .a, .dll).

3. Define Targets
Create imported targets to represent the library, so users can link to it easily.

4. Set Variables
Define variables that CMake users can refer to in their own CMakeLists.txt.

5. Package-Specific Settings
Define specific options that may be needed to configure the package (e.g., build
settings, compile options).

Basic Example of a Config.cmake File

Let’s consider a simple C++ library called MyLib. Below is an example of what the
MyLibConfig.cmake file might look like.

# MyLibConfig.cmake

# Ensure the package is being found correctly


set(MYLIB_VERSION "1.0.0" CACHE STRING "Version of MyLib")

# Specify the installation directory for the headers and libraries


set(MYLIB_INCLUDE_DIR "${CMAKE_INSTALL_PREFIX}/include")
set(MYLIB_LIBRARIES "${CMAKE_INSTALL_PREFIX}/lib/libmylib.a")

# Define an imported target for easy usage


add_library(MyLib::MyLib STATIC IMPORTED)
set_target_properties(MyLib::MyLib PROPERTIES
226

IMPORTED_LOCATION "${MYLIB_LIBRARIES}"
INTERFACE_INCLUDE_DIRECTORIES "${MYLIB_INCLUDE_DIR}"
)

# Optionally, define additional variables or configuration settings


set(MYLIB_FOUND TRUE CACHE BOOL "Indicates if MyLib is found")

Explanation of the File Components

• Set Variables: MYLIB INCLUDE DIR and MYLIB LIBRARIES define the paths
to the library’s headers and compiled libraries, respectively.
• Define Targets: The add library() command creates an imported target
MyLib::MyLib, which can be used by other projects to link with your library.
• Set Properties: set target properties() assigns properties like the
location of the library and the include directories to the target.
• Version and Metadata: The version and MYLIB FOUND variable provide metadata
that can be checked by users.

6.2.4 Where Should the Config.cmake File Be Installed?

For other CMake projects to find your library, the Config.cmake file should be
installed into a known CMake search directory. Common locations include:

• System-wide directories (e.g., /usr/local/lib/cmake/mylib/)


• User-defined directories (e.g., /home/user/mylib/cmake/)

The CMAKE PREFIX PATH variable is often used to specify the path where CMake
should search for the Config.cmake files.
227

cmake -DCMAKE_PREFIX_PATH=/path/to/install ..

If you are distributing the library, you might want to install the Config.cmake file into
the following location in your library’s installation process:

install(
FILES MyLibConfig.cmake
DESTINATION lib/cmake/MyLib
)

This installation ensures that when another project uses find package(MyLib
REQUIRED), CMake can locate and use the MyLibConfig.cmake file.

6.2.5 Using find package() to Find a Custom Library

Once the Config.cmake file is created and installed, other projects can use the
find package() command to locate and link your library. For example, in another
project, you can include the following in your CMakeLists.txt:

find_package(MyLib REQUIRED)

add_executable(my_project main.cpp)

# Link the imported target for MyLib


target_link_libraries(my_project PRIVATE MyLib::MyLib)

When find package() is called, CMake will search for the MyLibConfig.cmake
file. If the library is found, it will configure the project to use MyLib. This includes
228

setting include paths, linking to the correct library, and creating an imported target for the
library.

6.2.6 Handling Multiple Versions of a Custom Library

If your library is evolving and you want to support multiple versions, you can specify the
version in the Config.cmake file. Additionally, you can enforce version checks by
specifying a version requirement in the find package() call.

Example:

# In MyLibConfig.cmake
set(MYLIB_VERSION "1.0.0")

# If MyLib is part of a versioned library


set(MYLIB_VERSION "2.0.0" CACHE STRING "Version of MyLib")

Then, in the consuming project:

find_package(MyLib 2.0.0 REQUIRED)

This approach ensures that only the correct version of the library is linked with the
consuming project.

6.2.7 Supporting Optional Features and Components

Libraries often have optional components or features that can be enabled or disabled. You
can add configuration options in the Config.cmake file to manage these features.
229

For example, suppose MyLib has an optional feature for SSL support. You could add an
option in the Config.cmake file like this:

# In MyLibConfig.cmake
option(MYLIB_USE_SSL "Enable SSL support" ON)

if(MYLIB_USE_SSL)
target_compile_definitions(MyLib::MyLib INTERFACE USE_SSL)
endif()

When using the library, consumers can enable or disable SSL support by setting the
option:

find_package(MyLib REQUIRED)

# Optionally, configure SSL


option(MYLIB_USE_SSL "Enable SSL support" OFF)

This way, the Config.cmake file provides flexibility in how the library is configured.

6.2.8 Best Practices for Creating Config.cmake Files

Here are some best practices for creating and maintaining Config.cmake files for
custom libraries:

• Consistency in naming: Use consistent naming conventions for variables, targets,


and CMake properties. For example, always use the prefix MYLIB for
library-specific variables.
• Versioning: Make sure to include version information in the Config.cmake file,
and support version checks when appropriate.
230

• Target properties: Use imported targets (add library(... IMPORTED))


whenever possible to maintain a clean and modern CMake interface.
• Support multiple components: If your library provides optional components, use
find package()’s COMPONENTS option and allow consumers to specify which
components they need.
• Clear error handling: Always provide meaningful error messages if the package is
not found or if required components are missing.

6.2.9 Conclusion

Creating Config.cmake files is an essential skill for developers managing custom C++
libraries. These files provide a robust and flexible interface for users of your library,
helping them integrate it seamlessly into their own CMake-based projects. By following
the outlined best practices, you can ensure that your library is easy to use, versioned
correctly, and well-suited for complex dependency management.
231

6.3 Using FetchContent to Fetch Dependencies


Dynamically

6.3.1 Introduction

Managing external dependencies is a crucial part of any C++ project, especially when
building modular and scalable applications. While methods like find package() or
manually linking libraries have been widely used, CMake offers an elegant solution for
dynamically fetching and managing dependencies during the build process:
FetchContent. This feature allows you to download, configure, and use external
dependencies directly from the source at the time of the build, without the need for
pre-installed packages or system-wide configurations. In this section, we will explore how
to use FetchContent effectively for fetching dependencies dynamically.

6.3.2 What is FetchContent?

FetchContent is a CMake module introduced to simplify dependency management by


automatically downloading content (libraries, files, or other resources) from external
repositories and making them available within your project. Unlike traditional methods
where dependencies need to be pre-installed, FetchContent enables you to fetch
dependencies during the build process directly from a specified URL or version tag.

This is particularly useful when you:

• Don’t want users to manually install dependencies.

• Need to work with specific versions of a library or tool.

• Want to keep all dependencies under version control for reproducibility.


232

FetchContent ensures that the required dependencies are downloaded, configured,


and built as part of the project, making the integration process seamless.

6.3.3 Basic Syntax of FetchContent

The core command provided by CMake to use this feature is


FetchContent Declare(). This command declares a content to be fetched, while
other commands are used to ensure the content is actually downloaded and available for
use.

FetchContent_Declare(
<content_name>
GIT_REPOSITORY <repository_url>
GIT_TAG <commit_or_branch_or_tag> # Optional: specify a version
DOWNLOAD_NO_EXTRACT <ON|OFF> # Optional: whether to extract
,→ content
)

# Make content available and add it to the project


FetchContent_MakeAvailable(<content_name>)

Parameters of FetchContent Declare():

• <content name>: The name of the content being fetched.


• GIT REPOSITORY: The URL of the Git repository from which the content will be
fetched. Alternatively, URL can be used if fetching from a different source (like a zip
file or tarball).
• GIT TAG: The specific version (commit hash, tag, or branch) of the repository to
checkout. You can specify master, a version number, or a commit hash.
233

• DOWNLOAD NO EXTRACT: If set to ON, CMake will only download the content but
will not extract it. Useful when dealing with archives that don't need to be extracted.

6.3.4 Example: Using FetchContent to Fetch a Dependency

Let’s take an example where we want to fetch a library called fmt, a popular C++
formatting library. Here is how we can declare and fetch it dynamically using
FetchContent.

CMakeLists.txt Example:

cmake_minimum_required(VERSION 3.14)
project(MyProject)

# Declare the 'fmt' library as content to be fetched from GitHub


FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 8.0.1 # Fetch a specific version
)

# Make the 'fmt' content available for the project


FetchContent_MakeAvailable(fmt)

# Use fmt in your target


add_executable(my_project main.cpp)
target_link_libraries(my_project PRIVATE fmt::fmt)

Explanation:
234

• FetchContent Declare(fmt ...): We declare that we want to fetch the


fmt library from the GitHub repository. The GIT TAG specifies the exact version
(in this case, 8.0.1).
• FetchContent MakeAvailable(fmt): This command ensures that the
library is fetched and added to the build process. It makes the library available as if it
were part of the project.
• target link libraries(): After the dependency is available, you can link it
as you would any other library. The fmt::fmt target is automatically created by
the FetchContent module.

6.3.5 Fetching Dependencies from Different Sources

While Git repositories are commonly used with FetchContent, CMake also supports
fetching from other sources like tarballs or zip files. The general idea remains the same,
but the parameters change slightly.

Example 1: Fetching from a Tarball:

FetchContent_Declare(
my_library
URL https://example.com/my_library.tar.gz
)

FetchContent_MakeAvailable(my_library)

• URL: Specifies the location of the tarball or zip file to be downloaded and extracted.

Example 2: Fetching from a Subdirectory (Local Files):


235

If your project has a local subdirectory containing source files for an external dependency,
you can use FetchContent to manage it similarly.

FetchContent_Declare(
my_local_lib
URL_FILEPATH ${CMAKE_CURRENT_SOURCE_DIR}/libs/my_local_lib.tar.gz
)

FetchContent_MakeAvailable(my_local_lib)

Here, URL FILEPATH allows you to refer to a local path.

6.3.6 Managing Dependency Versions

When using FetchContent, it’s important to manage the versions of dependencies


being fetched. CMake provides the ability to specify exact versions through the GIT TAG,
GIT SHA1, or GIT HASH options for repositories. This ensures that your project builds
against a specific, known version of a dependency.

Example:

FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 8.0.1 # Fetch a specific release version
)

Alternatively, you can specify a commit hash directly:


236

FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 94e57d7a098ae3289e6a658801c5b5487ef0981a # Specific
,→ commit hash
)

This ensures that the content will always be retrieved at that exact state, guaranteeing
reproducibility.

6.3.7 Using FetchContent for Multiple Dependencies

You can fetch multiple dependencies in the same CMakeLists.txt file by calling
FetchContent Declare() multiple times and using
FetchContent MakeAvailable() for each one.

Example: Fetching Multiple Dependencies:

cmake_minimum_required(VERSION 3.14)
project(MyProject)

# Fetch fmt
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 8.0.1
)
FetchContent_MakeAvailable(fmt)

# Fetch spdlog
237

FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.9.2
)
FetchContent_MakeAvailable(spdlog)

# Create executable and link dependencies


add_executable(my_project main.cpp)
target_link_libraries(my_project PRIVATE fmt::fmt spdlog::spdlog)

In this example, both fmt and spdlog are fetched dynamically, and we link them to our
project.

6.3.8 Benefits and Limitations of FetchContent

Benefits:

• No Need for Pre-Installation: Dependencies are downloaded and built


automatically, eliminating the need for manual installation or pre-configured
systems.

• Version Control: You can specify exact versions of dependencies, ensuring


consistency across different builds.

• Simplified Dependency Management: All dependencies are handled within the


CMake build process, reducing configuration overhead for developers.

Limitations:
238

• Network Dependency: The build process relies on an active internet connection to


fetch the dependencies.

• Build Time: Fetching and building dependencies during the configuration process
can increase the overall build time, especially if the dependencies are large or
numerous.

• Complexity: While FetchContent simplifies dependency management, it can


introduce complexity in terms of version control and potential conflicts between
dependencies.

6.3.9 Best Practices for Using FetchContent

• Use for Dependencies with Less Frequent Updates: Since FetchContent


downloads the dependency every time the project is built, it’s better suited for stable,
well-maintained libraries that don’t update too frequently.

• Use Version Control: Always specify a version (tag or commit hash) to ensure
reproducibility. Avoid using branches like master unless necessary.

• Minimize Overuse: For large or numerous dependencies, consider other methods


such as find package() for common system-wide libraries to avoid
downloading and building everything.

• Check CMake Version: Ensure that you’re using a version of CMake that supports
FetchContent (CMake 3.14 or newer).

6.3.10 Conclusion

FetchContent is an incredibly powerful feature in CMake that simplifies dependency


management, making it easier to work with external libraries without worrying about
239

pre-installation. It offers flexibility, control over versions, and a seamless integration


process. By understanding how to use FetchContent, you can ensure that your project
remains portable and easy to build on any system, regardless of whether the dependencies
are pre-installed or not.
240

6.4 Integrating pkg-config and find library()

6.4.1 Introduction

When managing dependencies in a CMake-based project, you often encounter scenarios


where external libraries are installed via package management systems on Linux-based
systems. Many of these libraries provide a tool called pkg-config, which is commonly
used to retrieve metadata about installed libraries. CMake, being a cross-platform build
system, can easily integrate with pkg-config to fetch information about installed
libraries and their locations.

In this section, we’ll explore how to integrate pkg-config with CMake and use
find library() to locate libraries dynamically, enabling you to link external
dependencies seamlessly into your project. This integration is especially useful when
working with libraries that provide pkg-config files, such as GTK, OpenSSL, or
others commonly used in the open-source ecosystem.

6.4.2 What is pkg-config?

pkg-config is a tool used on Unix-like systems (Linux, macOS, etc.) to provide


information about installed libraries. It gives details like:

• Compiler flags (-I, -L, -D)

• Library locations

• Required dependencies of a library

• Version information
241

pkg-config works by reading .pc files, which are installed by the package manager
when a library is installed. These files contain the necessary information about the library
and are typically located in directories like /usr/lib/pkgconfig/ or
/usr/local/lib/pkgconfig/.

Example: Using pkg-config


For example, if you want to link with the libpng library, you can use pkg-config as
follows:

pkg-config --cflags --libs libpng

This command would output something like:

-I/usr/include/libpng -L/usr/lib -lpng

You can pass these flags to the compiler and linker manually, or automate the process with
CMake.

6.4.3 Integrating pkg-config with CMake

To integrate pkg-config with CMake, you use the find package() or


pkg check modules() commands in your CMakeLists.txt. This allows CMake to use
pkg-config to retrieve information about external libraries and automatically configure
the build.

Using find package() with pkg-config


If the library you are working with supports pkg-config and CMake's
find package() is designed to work with it, you can use the following approach:
242

1. Enable pkg-config: First, ensure that CMake knows to look for pkg-config
by enabling the PkgConfig module.

2. Use find package(): You can then call find package() to find the
required library, relying on pkg-config to handle the search.

Example:

Let’s say we want to use the libpng library in our project. CMake has support for
pkg-config through its FindPkgConfig module, and we can use it like this:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject)

# Enable pkg-config
find_package(PkgConfig REQUIRED)

# Use pkg-config to locate the libpng library


pkg_check_modules(LIBPNG REQUIRED libpng)

# Print the library's flags


message(STATUS "LIBPNG_CFLAGS: ${LIBPNG_CFLAGS}")
message(STATUS "LIBPNG_LIBRARIES: ${LIBPNG_LIBRARIES}")

# Add your executable and link the found library


add_executable(my_project main.cpp)
target_include_directories(my_project PRIVATE ${LIBPNG_INCLUDE_DIRS})
target_link_libraries(my_project PRIVATE ${LIBPNG_LIBRARIES})

Explanation:
243

• find package(PkgConfig REQUIRED): This ensures that pkg-config is


available.

• pkg check modules(LIBPNG REQUIRED libpng): This calls


pkg-config to find the libpng library and sets variables like
LIBPNG INCLUDE DIRS, LIBPNG LIBRARIES, and LIBPNG CFLAGS. These
variables store the necessary information to configure the build.

• target include directories() and target link libraries():


We link the required flags and libraries to our project using the variables set by
pkg check modules().

6.4.4 Using find library() to Find Libraries

While pkg-config is helpful for retrieving metadata, CMake also has the built-in
find library() function for locating libraries in specified directories. This command
searches for a library by name and returns the full path to the library file. It's particularly
useful when the library is installed but doesn't have a .pc file for pkg-config to find.

The syntax for find library() is as follows:

find_library(<VAR> name [path1 path2 ...])

Where:

• <VAR>: The variable that will store the path to the found library.

• name: The name of the library (without any prefixes or extensions, e.g., m for
libm.so).

• [path1 path2 ...]: Optional search directories to look for the library.
244

Example: Finding libm (Mathematics Library)

Here’s how you can use find library() to locate the mathematics library libm:

find_library(MATH_LIB m)
if(MATH_LIB)
message(STATUS "Found libm at ${MATH_LIB}")
else()
message(STATUS "libm not found")
endif()

# Add executable and link the found library


add_executable(my_project main.cpp)
target_link_libraries(my_project PRIVATE ${MATH_LIB})

In this example:

• find library(MATH LIB m): Searches for libm (the math library).

• target link libraries(my project PRIVATE ${MATH LIB}): If


libm is found, it is linked with the project.

6.4.5 Combining pkg-config and find library()

In some cases, you might want to use both pkg-config and find library()
together. This is common when a library is available through pkg-config, but its
dependencies are not declared or need to be manually located.

Here’s an example of using both pkg-config and find library() for a project
that depends on the libpng library (via pkg-config) and the libm math library
(using find library()):
245

# Enable pkg-config
find_package(PkgConfig REQUIRED)

# Find libpng using pkg-config


pkg_check_modules(LIBPNG REQUIRED libpng)

# Find libm using find_library()


find_library(MATH_LIB m)

# Create executable
add_executable(my_project main.cpp)

# Include directories for libpng


target_include_directories(my_project PRIVATE ${LIBPNG_INCLUDE_DIRS})

# Link libraries
target_link_libraries(my_project PRIVATE ${LIBPNG_LIBRARIES}
,→ ${MATH_LIB})

Explanation:

• pkg check modules(LIBPNG REQUIRED libpng): Locates libpng


using pkg-config.

• find library(MATH LIB m): Locates libm using the find library()
function.

• target link libraries(): Links both libpng and libm to the project.

This approach is useful for combining the strengths of both methods: pkg-config for
easily finding well-supported libraries and find library() for libraries that don’t
246

provide pkg-config support or when you need to locate additional dependencies


manually.

6.4.6 Best Practices for Using pkg-config and find library()

• Use find package() with PkgConfig when available: CMake has built-in
support for pkg-config via the PkgConfig module, so use
find package(PkgConfig REQUIRED) whenever possible to handle
external dependencies more elegantly.
• Set CMAKE PREFIX PATH for Custom Locations: If your libraries are installed in
non-standard locations, you can set the CMAKE PREFIX PATH to help
pkg-config or find library() locate the libraries.

cmake -DCMAKE_PREFIX_PATH=/path/to/custom/libs ..

• Check for Dependency Availability: Always verify that the required dependencies
are found before proceeding with the build. You can use find package() or
check the results of find library() to ensure everything is in place.

if(NOT LIBPNG_FOUND)
message(FATAL_ERROR "libpng not found!")
endif()

• Use find package() with explicit version requirements: When relying on


external dependencies, always specify the minimum version to ensure compatibility.

find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBPNG REQUIRED libpng>=1.6)
247

6.4.7 Conclusion

Integrating pkg-config with CMake using find package() and


pkg check modules() provides a convenient way to manage external dependencies
that are commonly available on Linux and other Unix-like systems. It allows CMake to
automatically retrieve the necessary information about external libraries, making it easier
to link them into your project. By combining pkg-config and find library(),
you gain flexibility in managing both well-known libraries with .pc files and more
generic libraries that don't provide such support.
248

6.5 Working with vcpkg and Conan for Package


Management

6.5.1 Introduction

Managing external dependencies efficiently is an essential part of any C++ project,


particularly when working with larger, more complex applications. While
find package(), pkg-config, and FetchContent provide robust solutions for
handling dependencies, tools like vcpkg and Conan bring a new level of convenience
and integration when dealing with third-party libraries. These modern package managers
simplify the installation, management, and integration of C++ libraries into your CMake
projects.

In this section, we’ll dive deep into how to integrate vcpkg and Conan with CMake,
providing you with powerful tools for dependency management, versioning, and
cross-platform builds. We will cover each tool’s installation, configuration, and usage
within CMake.

6.5.2 What are vcpkg and Conan?

vcpkg

vcpkg is a cross-platform C++ package manager that simplifies library management by


providing an easy-to-use interface for installing and integrating third-party libraries into
C++ projects. It works on Windows, Linux, and macOS, making it a versatile tool for
managing dependencies in CMake-based projects.

Key features of vcpkg include:


249

• Cross-platform: It supports Windows, macOS, and Linux, ensuring that your


dependencies can be handled uniformly across platforms.
• Precompiled Binaries: Many libraries in vcpkg come with precompiled binaries,
reducing the need for building dependencies from source.
• CMake Integration: vcpkg integrates seamlessly with CMake, allowing it to
automatically manage dependencies for your project.

Conan
Conan is another popular C++ package manager that focuses on providing a
decentralized, user-centric way of managing dependencies. Unlike vcpkg, which uses a
central repository, Conan supports multiple repositories and gives developers the
flexibility to host and manage their own packages.
Key features of Conan include:

• Decentralized: Conan allows developers to share and distribute packages across


various repositories, providing flexibility.
• Package Versioning: Conan helps manage dependencies with version control,
ensuring consistency and reproducibility in your builds.
• CMake Integration: Conan also integrates well with CMake, automating the
process of finding and linking packages.

6.5.3 Integrating vcpkg with CMake

vcpkg simplifies the process of installing libraries and managing them for CMake-based
projects. Below is a detailed guide on how to use vcpkg in your CMake workflow.

Installing vcpkg
250

1. Clone the vcpkg repository: First, clone the vcpkg repository from GitHub:

git clone https://github.com/microsoft/vcpkg.git

2. Bootstrap the vcpkg build system: Inside the vcpkg directory, run the bootstrap
script to build the package manager:
On Windows:

.\bootstrap-vcpkg.bat

On Linux/macOS:

./bootstrap-vcpkg.sh

3. Install packages with vcpkg: Use vcpkg to install C++ libraries. For example, to
install the fmt library:

./vcpkg install fmt

Integrating vcpkg with CMake

To make vcpkg work with your CMake project, you need to set the appropriate
environment variable and tell CMake where vcpkg is located.

1. Set the CMake Toolchain File: When invoking CMake, specify the vcpkg
toolchain file to let CMake know that you want it to use vcpkg for package
management:
251

cmake
,→ -DCMAKE_TOOLCHAIN_FILE=<path_to_vcpkg>/scripts/buildsystems/vcpkg.cma
,→ ..

Replace <path to vcpkg> with the actual path to your vcpkg installation.

2. Linking Installed Libraries: After installing a package using vcpkg, you can link
it to your project using find package() or target link libraries() as
you would with any other CMake-managed library.
For example, after installing fmt with vcpkg, you can use:

find_package(fmt REQUIRED)
target_link_libraries(my_project PRIVATE fmt::fmt)

vcpkg automatically configures find package() for most libraries, so you


don’t have to manually specify paths or flags.

6.5.4 Integrating Conan with CMake

Conan is another excellent option for C++ package management, providing an alternative
approach to managing dependencies with a focus on flexibility, versioning, and
decentralized package repositories. Let’s go over how to integrate Conan with your
CMake project.

Installing Conan

1. Install Conan: You can install Conan via pip (Python's package manager):
252

pip install conan

2. Initialize a conanfile.txt or conanfile.py: In your project’s root


directory, create a conanfile.txt or conanfile.py file. This file specifies
which dependencies your project requires. A basic conanfile.txt for a project
that depends on fmt might look like:

[requires]
fmt/8.0.1

[generators]
cmake

Here, fmt/8.0.1 specifies the version of the fmt library required.

Using Conan with CMake


To use Conan with CMake, you need to configure the build system to generate the
appropriate CMake files.

1. Install Dependencies: Use conan install to install the dependencies listed in


your conanfile.txt or conanfile.py:

conan install . --build=missing

The --build=missing flag ensures that any missing packages are built from
source.
2. Link with CMake: After installing dependencies, you need to tell CMake to use the
Conan-generated configuration files. Conan generates CMake files that set up
environment variables and paths for the dependencies.
253

In your CMakeLists.txt, add the following line to include the Conan-generated


files:

include_directories(${CMAKE_BINARY_DIR}/conan_includes)

Alternatively, if you are using the cmake generator, include the Conan CMake
module:

include(${CMAKE_BINARY_DIR}/conan.cmake)

3. Link the Libraries: With the dependencies installed and configured by Conan, you
can link the libraries to your project in the usual way:

target_link_libraries(my_project PRIVATE fmt::fmt)

6.5.5 Comparing vcpkg and Conan

Both vcpkg and Conan are popular and powerful tools for package management in C++.
However, they have different strengths and use cases:

Feature vcpkg Conan


Package Management Centralized (single Decentralized (supports
repository) multiple repositories)
Cross-Platform Yes (Windows, macOS, Yes (Windows, macOS,
Linux) Linux)
CMake Integration Seamless integration via Seamless integration with
toolchain file conan.cmake
254

Feature vcpkg Conan


Precompiled Binaries Many packages have Many packages, but some
precompiled binaries require building from source
Version Control Manages versions via vcpkg Full version control with
tool multiple repositories
Package Hosting Managed by Microsoft Community-driven and user-
(official repository) defined repositories

When to Use vcpkg:

• If you are working on a cross-platform C++ project and want a simple, streamlined
way to manage dependencies.

• If you need precompiled binaries for faster builds.

• If you prefer to work with a single, centralized repository.

When to Use Conan:

• If you need decentralized package management and control over your package
repository.

• If you need detailed version control and fine-grained control over the dependencies
of your project.

• If your project has complex or custom dependencies that need to be versioned


carefully.
255

6.5.6 Conclusion

Both vcpkg and Conan provide excellent solutions for managing external dependencies
in CMake-based C++ projects. While vcpkg offers a simpler, centralized approach with
easy integration into CMake, Conan offers more flexibility, version control, and
decentralized package management. Depending on your project’s needs, either tool can
significantly simplify dependency management and ensure a more consistent, reproducible
build process.
By leveraging the power of these package managers, you can focus more on developing
your application and less on handling dependency configurations.
Chapter 7

Working with CMake GUI & CLI

7.1 Using the CMake GUI on Windows and Linux

CMake is a cross-platform tool that provides users with the ability to manage the build
process of software projects in a compiler-independent manner. One of the most
convenient ways to interact with CMake is through its graphical user interface (GUI),
which offers a user-friendly way to configure projects, set up build paths, and generate
platform-specific build files. This section focuses on how to use the CMake GUI on both
Windows and Linux platforms, giving you insights into its interface and functionality.

7.1.1 Overview of the CMake GUI

The CMake GUI is an interactive application that provides a straightforward way to


configure and generate CMake project files. It eliminates the need to remember specific
commands or command-line options by offering an intuitive interface. With CMake GUI,
you can:

256
257

• Configure Projects: Set CMake variables, choose generators, and set specific paths
for building your project.
• Generate Build Files: Create platform-specific build files (e.g., Makefiles, Visual
Studio project files).
• Troubleshoot Configuration Errors: View error messages and warnings related to
the project configuration.

7.1.2 Installing the CMake GUI

Before you can use the CMake GUI, you'll need to install it. Here are the installation steps
for both Windows and Linux.

1. Installing CMake GUI on Windows


1. Download the CMake Installer:
• Go to the CMake official download page and download the latest version of
the CMake installer for Windows (e.g.,
cmake-x.y.z-win64-x64.msi).
2. Run the Installer:
• Launch the downloaded .msi installer.
• Follow the on-screen instructions. Ensure that the option to add CMake to
the system PATH is checked. This will allow you to use CMake from both
the GUI and the command line.
3. Verify Installation:
• Once the installation completes, open the CMake GUI application from the
Start menu or by searching for “CMake” in the Windows search bar.
• If you installed it correctly, the CMake GUI should open, and you'll be
ready to begin using it.
258

2. Installing CMake GUI on Linux

1. Install via Package Manager:


• On most Linux distributions, CMake is available through the system's
package manager. Use the appropriate command for your distribution.
For Ubuntu or Debian:

sudo apt-get update


sudo apt-get install cmake-qt-gui

For Fedora:

sudo dnf install cmake-qt-gui

2. Verify Installation:
• After installation, you can open the CMake GUI by running the following
command in a terminal:

cmake-gui

This should launch the CMake GUI application, ready to configure and generate
build files.

7.1.3 The CMake GUI Interface

Upon launching the CMake GUI, the interface will look slightly different on Windows and
Linux but functions similarly across both platforms. Below is a breakdown of the interface
elements:

1. Initial Configuration
259

When you first open the CMake GUI, you will be prompted to provide paths for both
your source directory (where the CMakeLists.txt file is located) and the binary
directory (where the build files will be generated).
• Source Directory: This is the location of the source code of your project,
typically the folder containing your CMakeLists.txt.
• Binary Directory: This is the folder where the generated build files will be
stored. It's common to create a separate build folder within your project’s
root directory for this purpose.
2. Setting CMake Variables
In the CMake GUI, the main window consists of several buttons and areas where you
can input information:
• CMake Cache Entries
: On the left side of the GUI, there is a list of variables that CMake uses for the
configuration. These variables can be set manually, or CMake can automatically
populate them based on the system configuration. For instance:
– CMAKE BUILD TYPE: Specifies the build type (e.g., Debug, Release).
– CMAKE INSTALL PREFIX: Defines the installation directory after
building the project.
– CMAKE CXX COMPILER: Specifies the C++ compiler to use.
• Edit Variables: You can double-click on any variable in the list to modify its
value. If you're unsure about a variable, you can click the Help button for a brief
description.
3. Generating Build Files
Once you have configured your project in the GUI, you can generate the appropriate
build files for your platform. The “Configure” and “Generate” buttons in the CMake
GUI allow you to:
260

1. Configure: CMake will analyze the source and binary directories, attempt to
detect your system’s properties, and populate the GUI with relevant values (e.g.,
compilers, libraries). If there are issues or warnings, they will be displayed in
the output window.
2. Generate: After configuration, you can press the Generate button to create the
build files. This step will generate files such as:
• Makefiles (on Linux/macOS)
• Visual Studio project files (on Windows)
• Ninja build files (if Ninja is selected as the generator)
4. Advanced Options
The CMake GUI also allows you to specify advanced settings:
• Choose a Generator
: CMake supports different generators for different platforms, such as:
– On Windows, you may choose Visual Studio or MinGW.
– On Linux, you may select Unix Makefiles or Ninja.
• CMake Log: The log section provides output messages that help debug or
verify configurations.
5. Building the Project
While the CMake GUI does not directly build the project, it helps set up the build
environment. After generating the build files, you will typically use your chosen
build system (e.g., make, Visual Studio) to actually compile and link your project.
For instance:
• On Linux, you would navigate to the build directory and run make or ninja
(depending on the generator you chose).
• On Windows, you would open the generated Visual Studio solution and build
from there.
261

7.1.4 Using the CMake GUI on Linux

Though the CMake GUI is very similar on both platforms, some Linux-specific behavior
should be noted:

• On Linux, the CMake GUI relies on the Qt framework for its interface, so having the
appropriate Qt libraries installed is essential.

• The Generate button will allow you to choose between different build systems, such
as Unix Makefiles or Ninja.

7.1.5 Using the CMake GUI on Windows

On Windows, the process is mostly the same as on Linux, with a few key differences:

• The most notable difference is the choice of Visual Studio generators, which allow
you to create Visual Studio project files directly from the GUI. Once you generate
these files, you can open and build them inside the Visual Studio IDE.

• If you're using MinGW or another alternative to Visual Studio, the CMake GUI will
configure for those environments as well.

7.1.6 Troubleshooting

The CMake GUI offers helpful error messages if something goes wrong during
configuration or generation:

• Missing Dependencies: If CMake can’t find required libraries or tools, it will show
warnings or errors. You can often resolve these by specifying the correct paths in the
GUI or ensuring that necessary software is installed.
262

• Configuration Errors: Errors during configuration (e.g., wrong compiler or


mismatched versions) will be displayed in the log output. You can fix these by
adjusting the configuration or modifying the CMakeLists.txt file.

7.1.7 Conclusion

Using the CMake GUI is an effective way to simplify the process of configuring and
building C++ projects. Whether you're working on Windows or Linux, the GUI provides
an intuitive interface that guides you through configuring your project, setting CMake
variables, and generating platform-specific build files. With its built-in error messages and
advanced options, it is an excellent tool for both beginners and advanced developers alike.
263

7.2 Command-Line Interface (CLI) with CMake

While the CMake GUI is a powerful tool for configuring and generating build files with a
graphical interface, many advanced users prefer the Command-Line Interface (CLI)
because of its flexibility, automation capabilities, and faster workflows. The CMake CLI
allows you to execute the same tasks that you would with the GUI, but through terminal
commands, making it easier to integrate CMake into scripts, continuous integration (CI)
pipelines, and large-scale automated builds.
In this section, we will walk through how to use CMake’s Command-Line Interface to
configure, generate, and build CMake projects. We’ll also cover important commands and
options that you can use to streamline your build process.

7.2.1 Overview of the CMake CLI

The CMake CLI is a text-based tool that uses commands to control how CMake configures
and generates build files. The basic syntax of the cmake command is as follows:

cmake [options] <path-to-source>

Where:

• options are various flags and parameters to control the configuration.


• <path-to-source> is the directory containing the project's
CMakeLists.txt file, which CMake uses to configure the project.

You can use the CMake CLI on both Windows and Linux (and other Unix-based systems),
and its commands are consistent across platforms, though the underlying build system
may differ.
264

7.2.2 Basic CMake Command-Line Workflow

Let’s go through a typical CMake CLI workflow, from configuring the project to
generating build files and building the project.

• Step 1: Creating a Build Directory


In most CMake workflows, it’s a best practice to create a separate build directory
outside of the source directory. This keeps the source directory clean and avoids
mixing source files with build artifacts. From the root of your project directory,
create a new build directory:

mkdir build
cd build

This step ensures that all generated files (such as Makefiles or Visual Studio project
files) are placed in the build directory rather than in the source directory.

• Step 2: Running CMake to Configure the Project


After navigating to the build directory, you can run the cmake command to
configure your project and generate the appropriate build files. The general syntax is:

cmake <path-to-source>

For example, if your project’s source code is in the parent directory, you would run:

cmake ..

When you run this command, CMake will:

– Find the CMakeLists.txt file in the specified source directory.


265

– Detect the environment and configure variables (such as compilers, libraries,


and system settings).
– Display a summary of the configuration process in the terminal, listing paths,
flags, and other settings.

• Step 3: Specifying Build Types and Generators


You can also pass additional options to the cmake command to control various
aspects of the configuration. Two important options are:

– CMAKE BUILD TYPE: Specifies the build type, which typically defines
whether you want a Debug or Release build.
For example:

cmake -DCMAKE_BUILD_TYPE=Release ..

This sets the build type to Release, optimizing the code for performance.
– CMAKE GENERATOR: Defines the build system generator, such as Makefiles,
Ninja, or Visual Studio.
Example for Unix Makefiles:

cmake -G "Unix Makefiles" ..

Example for Ninja:

cmake -G "Ninja" ..

On Windows, you could specify a Visual Studio version generator:

cmake -G "Visual Studio 16 2019" ..

• Step 4: Generating Build Files


266

Once you run the cmake command with the necessary options, CMake will generate
the build files in the build directory. The generated files are specific to the platform
and generator you’ve selected (e.g., Makefiles, Visual Studio solution files, etc.).

7.2.3 Important CMake CLI Commands and Options

CMake provides a range of commands and options for advanced configurations. Here are
some key ones that you will likely encounter when using the CLI:

1. cmake --build
After configuring the project with CMake, you can invoke the build process directly
from the CLI using the cmake --build command. This command allows you to
build your project using the same configuration you generated earlier, without
needing to switch to a separate build tool or IDE.
For example, to build the project:

cmake --build .

This will use the default generator (e.g., Makefiles or Ninja) to build the project in
the current directory.
You can also specify the number of parallel jobs to run during the build process
(useful for speeding up builds, especially in large projects):

cmake --build . --parallel 4

2. cmake -D (Setting Cache Variables)


You can set specific cache variables directly from the CLI using the -D flag. This
allows you to override default values defined in CMakeLists.txt or other
configuration files.
267

For example, to set a custom installation path:

cmake -DCMAKE_INSTALL_PREFIX=/custom/install/path ..

Another example is setting the compiler:

cmake -DCMAKE_CXX_COMPILER=g++-9 ..

3. cmake --install
Once your project is built, you can use the cmake --install command to
install the project into the specified location. This is especially useful for projects
that require installation steps, such as libraries or applications that need to be copied
to a system-wide location.
For example:

cmake --install .

This will install the project using the installation paths defined during the
configuration process.
4. cmake --version
To check the installed version of CMake, you can use the following command:

cmake --version

This is useful when you want to confirm that you are using the correct version of
CMake, particularly in environments with multiple versions.
5. cmake --help
To view a list of available options and commands for CMake, you can use the help
command:
268

cmake --help

This provides a comprehensive list of CMake commands, options, and usage


instructions.

7.2.4 Advanced CMake CLI Usage

For more advanced workflows, the CMake CLI provides several additional features to
support complex build processes.

1. Using CMake Presets


CMake allows you to define build configurations using preset files, making it easier
to standardize configurations across different systems or team members. You can use
CMakePresets.json and CMakeUserPresets.json to define options like
compilers, build types, and toolchains.
For example, you can run CMake with a preset configuration using:

cmake --preset mypreset

This reduces the need to manually set multiple flags and ensures consistency in how
your project is configured.

2. Running CMake from Scripts


One of the biggest advantages of using the CMake CLI is the ability to automate the
configuration and build process by including CMake commands in scripts. For
example, you can write a shell script to configure and build your project
automatically:
269

#!/bin/bash

# Create a build directory


mkdir -p build
cd build

# Configure the project


cmake -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" ..

# Build the project


cmake --build .

# Optionally, install the project


cmake --install .

Such automation is especially useful in CI/CD pipelines and for teams working on
large projects with many dependencies.

7.2.5 Troubleshooting Common Issues

While the CMake CLI is powerful and flexible, there are a few common issues you might
encounter:

• Missing Dependencies: If CMake cannot find a required library or tool, it will


display an error message. In such cases, ensure the necessary software is installed
and available in your PATH, or specify custom paths using the -D option.

• Build Type Issues: If you’re not specifying the CMAKE BUILD TYPE, CMake
might default to an empty configuration, leading to unexpected results. Always
explicitly set the build type if needed.
270

• Generator Errors: If you specify an invalid generator (e.g., Visual Studio version
mismatch), CMake will return an error. Ensure that the chosen generator matches
your system’s available build tools.

7.2.6 Conclusion

The Command-Line Interface (CLI) provides CMake users with the flexibility to
configure, generate, and build projects directly from the terminal. This is especially useful
for automating builds, integrating with continuous integration systems, and streamlining
workflows. Mastery of the CMake CLI is a valuable skill for C++ developers, as it gives
them complete control over the build process while maintaining portability across
platforms. Through the combination of simple commands and powerful flags, the CLI
allows for highly customizable and efficient project management.
271

7.3 Configuring Different Generators (-G "Ninja", -G


"Unix Makefiles", ...)

One of the most powerful features of CMake is its ability to generate build files for
different platforms, compilers, and build systems. This flexibility is enabled by CMake’s
use of generators, which control how the build process is handled once CMake has
configured the project. A generator specifies the type of build system (e.g., Makefiles,
Visual Studio project files, Ninja build files) that CMake will use to create the final output.

In this section, we will explore how to configure different generators in CMake, such as
-G "Ninja", -G "Unix Makefiles", and others. We’ll discuss when and why
you might choose one generator over another, and how to properly configure them using
the command-line interface (CLI).

7.3.1 What is a Generator in CMake?

A generator in CMake is a template that defines how build files will be created based on
the current platform and build tools. When you run the cmake command with a specific
generator, CMake uses that generator to produce the build system files that your chosen
platform requires.

For instance:

• On Linux, CMake may generate Makefile files that can be processed by the
make tool.

• On Windows, CMake might generate Visual Studio project files.

• On macOS, it might generate Xcode project files or Makefiles.


272

The -G option in the cmake command is used to specify which generator to use. You can
also choose from a wide variety of generators depending on your project’s needs.

7.3.2 Common Generators in CMake

Here are some of the most common CMake generators, how they work, and when you
might want to use them.

1. -G "Unix Makefiles"
The Unix Makefiles generator creates traditional Makefiles that can be
processed by the make build tool. This is one of the most widely used generators on
Linux and other Unix-like systems, including macOS.
Use Case:

• When working on Linux, BSD, or macOS systems with make as the build tool.
• For projects that do not require a specific IDE and prefer the command-line
interface.

Example:

cmake -G "Unix Makefiles" ..

This command will generate a Makefile that can later be used to build the project
with make:

make

Advantages:

• Works well on Unix-like systems.


273

• Supported by nearly all distributions and environments.


• Well-suited for small-to-medium-sized projects.

Disadvantages:

• Slower compared to some other build tools like Ninja, especially for large
projects.
• Lacks advanced parallelization features found in other build systems.

2. -G "Ninja"
The Ninja generator is designed for fast builds and provides better parallelization
than traditional make-based builds. Ninja is often preferred for large projects where
speed is a priority. It also has a simpler, more efficient design than Make, which
leads to faster incremental builds.
Use Case:

• When building large projects that require fast incremental builds.


• Projects that need to leverage parallel builds for better performance.
• When working with CI/CD pipelines that prioritize build speed.

Example:

cmake -G "Ninja" ..

Once the project is configured with Ninja, you can build it using:

ninja

Advantages:

• Faster builds than Unix Makefiles, especially on large projects.


• Efficient parallel builds by default.
274

• Simple and easy to use with no extra dependencies.


Disadvantages:
• Requires Ninja to be installed separately. While it is lightweight, it is another
tool to install and maintain.
• May not be as familiar to developers who are used to make or other traditional
build systems.
3. -G "Visual Studio <version>"
On Windows, CMake supports generating project files for Microsoft’s Visual Studio
IDE. By specifying a version number (e.g., Visual Studio 2019), CMake will
generate .sln (solution) and .vcxproj (project) files that can be opened directly
in Visual Studio. This generator is particularly useful for developers who want to
take advantage of Visual Studio’s graphical interface, debugging tools, and other
features.
Use Case:
• When working on Windows and targeting Visual Studio as the development
environment.
• Developers who prefer using Visual Studio for debugging, profiling, and
GUI-based project management.
Example: For Visual Studio 2019:

cmake -G "Visual Studio 16 2019" ..

Advantages:
• Provides seamless integration with Visual Studio.
• Supports advanced IDE features like debugging, code navigation, and profiling.
• Can be used for both C++ and C# projects in Visual Studio.
275

Disadvantages:

• Only works on Windows.


• Requires Visual Studio to be installed.
• The build process is slower compared to other generators like Ninja or
Makefiles.

4. -G "Xcode"
The Xcode generator creates an Xcode project on macOS, enabling developers to
build their projects using the Xcode IDE. This is an ideal choice for macOS
developers who want to integrate with Xcode’s graphical interface, testing tools, and
simulator support for iOS/macOS applications.
Use Case:

• When working on macOS and targeting the Xcode IDE for development and
testing.
• Ideal for iOS and macOS applications, especially if you need to use the Xcode
interface for design, profiling, or app store submissions.

Example:

cmake -G "Xcode" ..

Advantages:

• Direct integration with Xcode, which provides a rich development environment.


• Full support for iOS/macOS-specific tools such as simulators, testing, and app
submissions.
• Useful for cross-platform C++ projects that are targeting Apple’s ecosystem.

Disadvantages:
276

• Only available on macOS.


• Not suitable for non-Apple platforms.
5. -G "MinGW Makefiles"
The MinGW Makefiles generator is used when you are building on Windows
with the MinGW (Minimalist GNU for Windows) toolchain. MinGW provides a set
of GNU tools (including GCC) to enable a Unix-like development environment on
Windows.
Use Case:
• When building on Windows using the MinGW toolchain.
• If you prefer to work with make rather than Visual Studio or other IDEs.
Example:

cmake -G "MinGW Makefiles" ..

Advantages:
• Allows using make on Windows with the MinGW toolchain.
• Suitable for cross-platform C++ development targeting both Windows and
Unix-like systems.
Disadvantages:
• Requires MinGW to be installed and properly configured.
• Not as widely used as Visual Studio on Windows, so you may encounter
compatibility issues in certain environments.

7.3.3 Choosing the Right Generator

Selecting the right generator depends on several factors, including:


277

• Platform: Different generators are available for different platforms (Windows,


Linux, macOS).

• Toolchain: If you are using a specific build tool (e.g., Ninja, Make, or Visual
Studio), select the corresponding generator.

• Project Size: For larger projects, generators like Ninja are preferred due to their
faster incremental builds and parallelization features.

• IDE Preferences: If you prefer working in a specific IDE, such as Visual Studio or
Xcode, select the generator that integrates with that IDE.

Example Scenarios for Choosing a Generator

• Linux/Unix Project: If you are on a Linux or Unix system and working with a
command-line environment, you would typically use -G "Unix Makefiles"
or -G "Ninja".

• Windows Developer: On Windows, if you are using Visual Studio, you would use
-G "Visual Studio 16 2019". If you are using a different build system like
MinGW, you would use -G "MinGW Makefiles".

• macOS Development: On macOS, if you're building a project for Xcode or


iOS/macOS, you would use -G "Xcode". For non-Xcode workflows, -G "Unix
Makefiles" or -G "Ninja" might also work.

7.3.4 Specifying Multiple Generators

In some cases, you may need to test or build your project with different generators. You
can simply run CMake with different -G flags, specifying a different generator for each
configuration. Keep in mind that some build systems (such as Visual Studio) may create
278

multiple configuration files (e.g., solution files, project files) for different build types
(Debug, Release).

7.3.5 Troubleshooting Generator Issues

Sometimes, you may run into issues while configuring CMake with a specific generator.
Some common issues include:

• Incorrect Generator Syntax: Make sure the generator string is entered correctly.
CMake is very specific about the exact names of the generators.
• Missing Tools: Some generators, like Ninja or MinGW, require the corresponding
tools to be installed. If CMake cannot find the necessary toolchain, it will generate
an error message.
• Generator Compatibility: Some generators might not work well on certain
platforms or with specific versions of CMake. Always refer to the CMake
documentation to verify compatibility.

7.3.6 Conclusion

CMake’s ability to support multiple generators makes it a highly flexible tool for
managing cross-platform and cross-toolchain projects. Whether you are targeting
traditional Makefiles, Ninja for speed, Visual Studio for IDE integration, or Xcode for
macOS/iOS development, CMake makes it easy to configure and generate the right build
files for your platform and toolchain. By understanding the various generator options and
when to use them, you can streamline your build process and improve productivity in your
C++ projects.
279

7.4 Managing Options and Settings with ccmake

While CMake offers powerful configuration capabilities through the command-line


interface (CLI) and graphical user interface (GUI), there’s a middle ground: ccmake.
This tool is a terminal-based user interface that combines the simplicity of a GUI with the
power and flexibility of the command line. It is particularly useful when working in
environments where a GUI is not available or where you prefer a text-based interface.

In this section, we’ll explore ccmake, how it works, how to manage project settings and
options through it, and how it compares to both the full CMake GUI and the
command-line interface.

7.4.1 What is ccmake?

ccmake stands for CMake curses interface, and it’s a command-line tool that provides
an interactive interface to configure CMake options for a project. The main purpose of
ccmake is to provide a text-based configuration interface that can be run inside a
terminal. This tool allows users to:

• View and set CMake cache variables.

• Modify project settings such as build types, installation paths, and optional features.

• Save and apply changes interactively without needing to edit configuration files
manually.

Unlike the full graphical CMake GUI, ccmake works entirely in the terminal, making it
useful for remote development or environments where a GUI is impractical. It’s also often
preferred by advanced users who are comfortable with terminal-based workflows but still
want to take advantage of the interactive nature of CMake's configuration process.
280

7.4.2 Installing and Using ccmake

ccmake is included as part of the CMake distribution, so it should be available as long as


you have CMake installed on your system. To check if ccmake is installed, simply type
the following in the terminal:

ccmake --version

If the tool is not installed, you may need to install CMake via your system’s package
manager. For example:

• On Ubuntu (or other Debian-based systems):

sudo apt-get install cmake

• On macOS (using Homebrew):

brew install cmake

• On Windows, ccmake is included with the standard CMake installer.

7.4.3 Basic Workflow with ccmake

Using ccmake is straightforward and involves running it in the build directory where
CMake has been previously configured. Here's the basic workflow:

1. Navigate to the Build Directory: It's a best practice to use an out-of-source build
directory (to keep source files clean). If you haven’t already created a build directory,
do so:
281

mkdir build
cd build

2. Run ccmake: Once you’re inside the build directory, run the ccmake command,
pointing to the project’s source directory (typically the parent directory containing
the CMakeLists.txt file).

ccmake ..

This command will start the interactive configuration interface, displaying the
current configuration options and settings for your project.

3. Navigate the ccmake Interface: The ccmake interface uses keyboard navigation.
Once the interface appears, you'll see a list of cache variables with their current
values. You can use the following keys to interact with ccmake:

• Arrow keys: Navigate through the list of options.


• Enter: Select an option to edit its value.
• Type the value: Modify the value for the selected option (for variables, paths,
etc.).
• c: Configure the project (this updates the cache and displays any errors or
warnings).
• g: Generate the build files after configuration is complete.
• q: Quit the ccmake interface.

This interface is highly interactive, making it easy to change settings without


needing to manually edit configuration files.
282

7.4.4 Managing Cache Variables in ccmake

A key feature of ccmake is the ability to view and modify the CMake cache variables.
These variables control the configuration of the project, such as the paths to dependencies,
compiler flags, or build types. The cache variables are saved in the CMakeCache.txt
file in your build directory.

In ccmake, the cache variables are displayed in a table-like structure, with each row
representing a variable, its current value, and its description. Some common types of
cache variables include:

• Boolean variables: These represent on/off or true/false options. For example,


enabling or disabling specific features or modules in the project.
• String variables: These represent paths, filenames, or any other string value, such as
installation directories or compiler paths.
• Path variables: These are used for specifying the location of libraries or tools that
your project depends on.

You can change the values of these variables using the arrow keys to navigate to the
desired row, pressing Enter to edit the value, and typing the new value.

Example: Let’s say your project has an option to enable or disable a specific feature, and
it is defined as a Boolean cache variable, ENABLE FEATURE X. If the default value is
OFF, you can change it to ON using ccmake:

1. Scroll down to the ENABLE FEATURE X variable.


2. Press Enter to select it.
3. Change the value from OFF to ON.
4. Press Enter again to save the change.
283

3. Important ccmake Features

1. Configuration with ccmake


Once you’ve modified the desired options, you can trigger a reconfiguration by
pressing c. This will run CMake to re-evaluate the configuration and update the
CMakeCache.txt file accordingly. During this process, CMake will check for
any errors or missing dependencies and will notify you if any problems arise.
For example:
• If CMake detects missing dependencies or invalid paths, it will alert you with an
error message.
• If everything is valid, CMake will proceed and update the cache.
2. Generating Build Files with ccmake
After configuration is complete, you can use the g key to generate the appropriate
build files for your project. These files will be created in the build directory, and they
will be tailored to the generator and settings you've selected.
For example, if you are using the Unix Makefiles generator, running g will
create a Makefile that you can later use with the make tool to build the project.
3. Viewing CMake Warnings and Errors
ccmake displays useful warnings and errors during the configuration process. If
there are any issues with your settings or missing dependencies, CMake will provide
a message in the terminal window, helping you quickly identify and resolve
configuration problems. This feature makes ccmake a great option for
troubleshooting build problems interactively.
4. Advanced Options in ccmake
For more advanced workflows, ccmake also allows you to:
• Set cache entries directly: If you know the specific cache variable you want to
change, you can type its name and value directly in the interface.
284

• Clear cached variables: If you want to reset the configuration to its initial state,
you can delete or clear the cache variables.
• Use advanced mode: By pressing the t key, you can switch to advanced mode,
which reveals additional configuration options that are typically hidden. This is
useful for more complex projects or when you need to fine-tune the build
configuration.

7.4.5 Differences Between ccmake, CMake GUI, and CLI

While both ccmake and the graphical CMake GUI serve similar purposes, they have
distinct use cases and benefits:

• ccmake is a text-based tool that works within the terminal, ideal for environments
without graphical interfaces. It’s best suited for users who prefer terminal-based
workflows but still want interactive configuration.

• CMake GUI provides a more visually intuitive interface, ideal for those who prefer
using a mouse and require a more user-friendly experience.

• CMake CLI is fully command-line based, offering the highest level of automation
and flexibility, particularly for integrating CMake into scripts or CI/CD systems.

7.4.6 Example ccmake Workflow

Let’s go through an example of configuring a simple project using ccmake:

1. Create a build directory:


285

mkdir build
cd build

2. Run ccmake to configure the project:

ccmake ..

3. Modify some options:

• Use the arrow keys to select CMAKE BUILD TYPE and change it from Debug
to Release.
• Enable or disable certain features by modifying Boolean cache variables (e.g.,
ENABLE TESTING).

4. Configure the project:

• Press c to configure and update the cache.

5. Generate the build files:

• Press g to generate the build files (e.g., Makefiles or Ninja files).

6. Build the project: After generating the files, you can use the make or ninja
command (depending on your generator) to build the project.

7.4.7 Conclusion

ccmake provides a convenient, interactive text-based interface for managing and


modifying CMake configuration settings. It strikes a balance between the full GUI and the
more automated CLI workflows, offering developers a simple yet powerful tool for
configuring build options without needing to edit configuration files manually. Whether
you're working on a remote server, prefer terminal-based tools, or need a lightweight
286

configuration interface, ccmake is a valuable addition to your CMake toolkit. By


mastering ccmake, you can easily fine-tune your project's settings and quickly
troubleshoot build configuration issues.
Chapter 8

CMake and Different Development


Environments

8.1 Integration with Visual Studio

One of the major benefits of using CMake is its ability to generate project files for various
integrated development environments (IDEs), including Visual Studio. Visual Studio is
one of the most popular IDEs for C++ development, especially on Windows, offering
powerful debugging, profiling, and code editing tools. With CMake's flexibility,
developers can easily integrate CMake into the Visual Studio workflow and take
advantage of the IDE’s features while maintaining a cross-platform build system.

In this section, we will explore how to use CMake with Visual Studio, covering topics like
generating Visual Studio project files, managing configurations, and leveraging the IDE's
full potential for development and debugging.

287
288

8.1.1 Why Integrate CMake with Visual Studio?

Before diving into the details, let's briefly discuss why integrating CMake with Visual
Studio is beneficial:

1. Cross-Platform Compatibility: CMake allows you to write cross-platform C++


code and generate project files not only for Visual Studio but also for other build
systems like Make or Ninja. This enables you to easily switch between platforms
and IDEs.

2. IDE Features: Visual Studio offers a rich set of features like IntelliSense,
auto-completion, refactoring tools, debugging, and performance profiling. Using
CMake with Visual Studio gives you access to all of these features while maintaining
a consistent build configuration.

3. Multiple Build Configurations: Visual Studio supports multiple configurations


such as Debug, Release, and Custom configurations. With CMake, you can define
these configurations in your CMakeLists.txt file, and Visual Studio will
automatically pick them up when you generate project files.

4. Easy Debugging: Visual Studio’s debugger is one of the most powerful on


Windows, and using CMake with Visual Studio allows you to integrate the debugger
with your project seamlessly, making development and debugging much easier.

5. Customizable Build Options: With CMake, you can specify detailed build options
and flags for your Visual Studio project, such as compiler options, preprocessor
definitions, and target-specific settings. This provides you with full control over how
your project is built.
289

8.1.2 Generating Visual Studio Project Files with CMake

CMake makes it easy to generate Visual Studio project files using the -G flag with the
cmake command. You need to specify the appropriate generator for the version of Visual
Studio you’re using. Each version of Visual Studio has a corresponding generator string.

For example:

• Visual Studio 2019: -G "Visual Studio 16 2019"

• Visual Studio 2022: -G "Visual Studio 17 2022"

These generator strings tell CMake to create the correct project files for the specified
version of Visual Studio. Let’s walk through the steps to generate Visual Studio project
files using CMake.

1. Set up the project structure:

• Suppose you have a project with a CMakeLists.txt file in the root


directory. This file contains the necessary configuration instructions for CMake.
• It's recommended to create an out-of-source build directory to keep source files
clean.

cd build

2. Run CMake to generate Visual Studio project files: In the build directory, run
the following CMake command to generate Visual Studio project files for a specific
version:
290

cmake -G "Visual Studio 16 2019" ..

This will create a .sln (solution) file and other Visual Studio-specific project files
in the build directory. These files can now be opened directly in Visual Studio.
3. Open the Solution in Visual Studio: After CMake finishes generating the project
files, you can open the generated .sln file directly in Visual Studio by either
double-clicking the file or opening it from Visual Studio's File > Open >
Project/Solution menu.
Visual Studio will automatically recognize the CMake project and load it. If you
have multiple configurations (e.g., Debug, Release), Visual Studio will allow you to
choose between them before building.

8.1.3 Understanding CMake and Visual Studio Configurations

Visual Studio supports multiple build configurations (e.g., Debug, Release, MinSizeRel,
RelWithDebInfo), and CMake allows you to define these configurations as part of the
build process. When you generate Visual Studio project files using CMake, it
automatically incorporates these configurations into the .sln file, giving you the
flexibility to build your project in different modes.
For example, here’s how you might define configurations in your CMakeLists.txt:

# Define build types for Visual Studio


set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "Build
,→ configurations" FORCE)

When you generate the Visual Studio project, you can choose the configuration from the
drop-down menu in the IDE. CMake ensures that the necessary flags and settings for each
configuration are set.
291

Additionally, CMake supports multi-config generators, which allow you to build multiple
configurations in one go (for instance, both Debug and Release configurations). When you
use Visual Studio with CMake, the build configurations are automatically handled for you,
making it easy to switch between different build modes.

8.1.4 Using Visual Studio to Build CMake Projects

Once you have generated the Visual Studio project files with CMake, building the project
becomes straightforward. Visual Studio handles the build process through the Build menu,
or you can use the toolbar to trigger builds.

1. Select the Build Configuration: In Visual Studio, you can select the build
configuration you want to use from the toolbar. Common configurations include:

• Debug: Includes debugging symbols and disables optimizations.


• Release: Optimizes the code for performance and excludes debugging symbols.

2. Build the Project: After selecting your configuration, you can build the project by
clicking Build > Build Solution (or pressing Ctrl + Shift + B), which will
trigger the compilation process for your CMake-based project.

3. Managing Multiple Configurations: Visual Studio allows you to manage multiple


configurations for your project. For example, you can define custom configurations
in your CMakeLists.txt file (e.g., for testing or deployment) and easily switch
between them within the IDE.
CMake will automatically map these configurations to Visual Studio's build system,
and you can further fine-tune them within Visual Studio’s build settings if needed.
292

8.1.5 Debugging CMake Projects in Visual Studio

One of the main reasons for using Visual Studio is its advanced debugging tools. CMake
integrates seamlessly with Visual Studio's debugger, enabling you to set breakpoints,
inspect variables, and step through your code with ease.

1. Set Breakpoints: After opening the project in Visual Studio, you can navigate to
your code and set breakpoints by clicking in the margin next to the line numbers.
This is similar to debugging any other Visual Studio project.

2. Start Debugging: Once the breakpoints are set, press F5 (or select Debug > Start
Debugging) to run the program in debug mode. Visual Studio will automatically
stop at the breakpoints, allowing you to inspect the program state.

3. Advanced Debugging Features: Visual Studio also supports more advanced


debugging features, such as:

• Live code analysis with Visual Studio's IntelliSense and code suggestions.
• Memory debugging to analyze memory leaks or corrupted data.
• Performance profiling to help optimize your code by identifying bottlenecks.

Since you generated the Visual Studio project files using CMake, all these features
are available, making debugging and profiling your project much easier.

8.1.6 Modifying CMake Configuration for Visual Studio Projects

While CMake generates project files based on the CMakeLists.txt file, you might
need to tweak certain build settings for Visual Studio. You can do this directly within the
CMakeLists.txt file or by using CMake variables that influence the Visual Studio
build process.
293

1. Set Compiler Flags: CMake allows you to specify compiler flags that will be used
by Visual Studio. For instance, you can set flags to enable warnings or optimizations
specifically for Visual Studio:

if(MSVC)
add_compile_options(/W4) # Enable level 4 warnings for MSVC
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O2") # Enable
,→ optimization for MSVC
endif()

2. Setting Up CMake Toolchain Files: For more advanced configurations, you may
use CMake's toolchain files to control various aspects of the build, such as the
compiler, linker, or additional build options. Toolchain files are particularly useful
when working with custom Visual Studio setups or when cross-compiling.

8.1.7 Benefits of Using CMake with Visual Studio

• Cross-Platform Development: CMake allows developers to write


platform-independent build configurations while generating platform-specific files
for different IDEs (like Visual Studio). This is especially useful for teams working
across different operating systems.

• Consistency Across Environments: By using CMake to generate Visual Studio


project files, you ensure that the build process remains consistent across different
machines and IDEs, even if you are working in a cross-platform project.

• Improved Productivity: With Visual Studio’s powerful features like IntelliSense,


automated testing, and visual debugging tools, CMake integration significantly
boosts productivity. Developers can focus more on coding while relying on the IDE
to handle compilation, debugging, and profiling.
294

8.1.8 Conclusion

Integrating CMake with Visual Studio offers a powerful combination for C++
development on Windows. By generating Visual Studio project files through CMake,
developers can leverage the full range of Visual Studio’s IDE features, including advanced
debugging, profiling, and code navigation, while still maintaining a flexible,
cross-platform build system. Whether you are working on a small project or a large,
multi-platform application, CMake’s integration with Visual Studio provides a streamlined
workflow that enhances both productivity and code quality.
By understanding how to generate Visual Studio projects, configure build settings, and
debug using Visual Studio's tools, you can significantly enhance your CMake-powered
development process, making it easier to manage and build C++ projects within Visual
Studio.
295

8.2 Integration with CLion

CLion is a powerful, cross-platform Integrated Development Environment (IDE) designed


specifically for C and C++ development. Developed by JetBrains, CLion comes with a
range of advanced features, such as intelligent code completion, integrated debugging,
refactoring tools, and static analysis. One of the key features of CLion is its deep
integration with CMake, which allows you to seamlessly manage, configure, and build
C++ projects.

This section will walk you through how to integrate CMake with CLion, explaining how
CLion handles CMake projects, the configuration options, and how you can leverage the
IDE’s features to streamline your development process. You’ll also see how CMake
simplifies managing cross-platform C++ projects in CLion.

8.2.1 Why Integrate CMake with CLion?

Before diving into the steps, it’s important to understand why integrating CMake with
CLion is beneficial:

1. Cross-Platform Development: Like CMake, CLion is designed with cross-platform


development in mind. By using CLion with CMake, you can write code on one
platform and easily build it on another. CLion will automatically detect the CMake
configuration and generate the appropriate build files for the platform you’re
working on.

2. Intelligent CMake Support: CLion provides excellent support for CMake through
its built-in integration. It can automatically detect CMakeLists.txt files, handle
build configurations, and offer detailed CMake project navigation, making it easier
to work with large and complex projects.
296

3. Streamlined Workflow: With CLion’s built-in CMake integration, you no longer


need to manually configure build files or invoke CMake from the command line. You
can easily modify CMake options and manage build configurations through the
IDE’s graphical interface.

4. Simplified Debugging and Testing: CLion integrates with CMake to provide a


seamless experience for debugging and running tests. You can build, run, and debug
your CMake-based C++ projects directly within the IDE.

5. Advanced IDE Features: CLion offers powerful features such as code completion,
code analysis, refactoring, and integrated unit testing support. When combined with
CMake, CLion allows you to fully utilize these features in your C++ projects,
enabling you to improve both productivity and code quality.

8.2.2 Setting Up CLion for CMake Projects

To begin using CMake with CLion, you'll first need to set up your project. Here are the
steps:

1. Install CLion: If you haven't installed CLion yet, go to the JetBrains CLion
download page and install the appropriate version for your operating system
(Windows, macOS, or Linux). You can download a free trial or purchase a full
license.

2. Create or Open a CMake Project:

• Creating a New CMake Project: When starting a new project, you can create a
CMake-based project directly from within CLion. To do this:
– Open CLion.
– Go to File > New Project.
297

– Select C++ Executable under the project types and specify the language
standard and build system as CMake.
– CLion will generate a basic CMakeLists.txt file for you, where you
can add source files and other configurations.
• Opening an Existing CMake Project: If you already have a project with a
CMakeLists.txt file, you can open it directly in CLion:
– Open CLion and choose Open from the welcome screen.
– Browse to the directory containing your CMake project and select the
CMakeLists.txt file.
– CLion will automatically detect the project as a CMake-based project and
open it accordingly.

3. Verify CMake Integration: After opening or creating a CMake project, you should
see the CMake tool window on the right side of CLion. This window shows the
current CMake configurations and build targets for your project. You can access
additional options by clicking on the CMake tab or CMake GUI from this window.

8.2.3 CLion’s CMake Configuration and Options

Once your CMake project is open in CLion, it’s time to configure CMake settings. CLion
allows you to modify CMake variables and manage multiple build configurations, all from
within the IDE. Here’s how you can manage your project’s configuration in CLion:

1. Viewing and Modifying CMake Variables:

• CLion detects and displays CMake variables in the CMake Tool Window. You
can modify these variables directly by clicking the gear icon in the CMake
window and selecting Edit Configurations. This opens a dialog where you can
298

change CMake options like CMAKE BUILD TYPE,


CMAKE INSTALL PREFIX, and custom variables specific to your project.
• For example, if you want to change the build type from Release to Debug,
you can do so by adding or modifying the CMAKE BUILD TYPE variable in
this configuration dialog.

2. Setting Up Build Configurations:

• In CLion, you can define multiple build configurations (such as Debug, Release,
or custom configurations). You can create new configurations by going to File
> Settings > Build, Execution, Deployment > CMake.
• For each configuration, CLion will show options for specifying:
– CMake executable: The path to the CMake binary. Typically, CLion
auto-detects this, but you can modify it if necessary.
– CMake options: Extra flags you want to pass to CMake, such as
-DCMAKE INSTALL PREFIX=/custom/path.
– Build directory: Where the build files will be generated. By default, CLion
generates them in a folder called cmake-build-debug or
cmake-build-release, depending on the selected configuration.
• Once the configuration is set, you can select the build configuration from a
drop-down list in the toolbar and click the Build button to trigger the CMake
build.

3. Target Management:

• CMake projects often involve multiple targets (e.g., executables, libraries, etc.).
CLion lets you manage and select the target you want to build or run through its
Run/Debug Configurations. You can create new configurations for different
build targets in the Run/Debug Configurations dialog.
299

• You can also specify command-line arguments for the selected target and easily
debug specific targets by setting breakpoints and running the target in debug
mode.

8.2.4 Building and Running CMake Projects in CLion

CLion simplifies building and running CMake projects, allowing you to stay within the
IDE for the entire development lifecycle. Here's how the build process works in CLion:

1. Build the Project:

• Once the project is configured, you can build it by clicking the Build button (a
hammer icon) in the toolbar. CLion will call CMake to generate the necessary
build files and invoke the build system (e.g., make, ninja, or MSBuild) to
compile the project.
• The Build Output tab in CLion will display the results of the build process,
showing any errors, warnings, or successful builds.

2. Run the Project:

• You can run your application directly from within CLion by clicking the Run
button (a green arrow icon). CLion will use the appropriate build configuration
and launch the application.
• CLion will also allow you to specify runtime arguments, which can be useful for
testing different scenarios or running the program with specific configurations.

3. Automatic Rebuilds:

• If you modify your source files, CLion will automatically detect the changes
and offer to rebuild the project for you. You can also configure it to rebuild the
project on each launch or when necessary.
300

8.2.5 Debugging CMake Projects in CLion

CLion’s debugging tools work seamlessly with CMake projects. When you set up your
project correctly, debugging becomes an integrated experience that takes full advantage of
CLion’s powerful debugger.

1. Set Breakpoints:

• To set a breakpoint, simply click in the left margin next to the line numbers in
your source code. The line will be highlighted, and a red dot will appear,
indicating that a breakpoint has been set.

2. Start Debugging:

• To start debugging, click the Debug button (a bug icon) in the toolbar or press
Shift + F9. CLion will compile your project (if necessary) and start the
application in debug mode.
• The Debugger tab will show the call stack, variables, and breakpoints, allowing
you to step through your code and inspect values.

3. Watch Variables and Evaluate Expressions:

• CLion offers the ability to watch variables and evaluate expressions in real time.
This is particularly useful for tracking the value of specific variables during
debugging or checking the result of complex expressions.

4. Remote Debugging:

• CLion also supports remote debugging. If you're working on a project hosted on


a remote machine, you can configure CLion to debug applications running on a
different system. This is ideal for cross-platform or embedded development
scenarios.
301

8.2.6 Running Tests in CLion

CLion integrates well with CMake-based test frameworks, such as Google Test, Catch2,
and Boost.Test. You can run your tests directly within CLion, making the process of unit
testing seamless.

1. Configure Test Framework:

• In your CMakeLists.txt, add the necessary configurations to enable testing.


For example, to add Google Test, you would add the following lines:

enable_testing()
add_subdirectory(tests)
add_executable(my_tests test_main.cpp)
target_link_libraries(my_tests gtest gtest_main)
add_test(NAME MyTests COMMAND my_tests)

2. Run Tests:

• Once your tests are configured, you can run them directly from CLion. CLion
will detect the tests in your project and display them in the Run tool window.
You can run all tests or select specific ones to execute.

3. View Test Results:

• After running the tests, CLion provides a detailed view of the test results,
including pass/fail statuses, output, and logs.

8.2.7 Conclusion

Integrating CMake with CLion offers an efficient, user-friendly environment for C++
development. CLion’s built-in support for CMake allows you to easily manage, configure,
302

build, and debug CMake-based projects, while its intelligent features like code completion,
refactoring, and testing provide a significant productivity boost. Whether you're working
on a small C++ application or a large, cross-platform project, CLion offers the tools
necessary to streamline your development workflow.
By leveraging CMake and CLion together, you can focus more on coding and less on
configuring your build system, while also taking full advantage of the IDE's powerful
features to optimize your code and workflow.
303

8.3 Integration with Qt Creator

Qt Creator is a powerful, cross-platform Integrated Development Environment (IDE)


designed specifically for development with the Qt application framework. While Qt
Creator is primarily known for its excellent support for GUI-based C++ development
using the Qt libraries, it also provides excellent support for general C++ development,
making it an ideal environment for managing and building C++ projects with CMake.
In this section, we’ll cover how to effectively use CMake with Qt Creator for managing
your C++ projects, explaining the integration process, configuration settings, and the
workflow of building and debugging CMake-based projects within Qt Creator.

8.3.1 Why Use CMake with Qt Creator?

Integrating CMake with Qt Creator offers several key advantages:

1. Cross-Platform Development: Like CMake, Qt Creator is cross-platform and


supports development for Windows, macOS, and Linux. CMake allows you to
configure builds for multiple platforms, while Qt Creator enables you to develop,
test, and debug applications on those platforms with ease.
2. Unified IDE for Qt and Non-Qt Projects: While Qt Creator is optimized for
developing Qt-based applications, it also provides robust support for non-Qt projects,
including those that use plain C++ and external libraries. By using CMake in
combination with Qt Creator, you can manage a wide variety of projects without
needing to switch IDEs or build systems.
3. Seamless Project Configuration: Qt Creator supports automatic detection of
CMake-based projects. This allows developers to quickly set up and configure
projects in the IDE without needing to manually generate or edit build files.
304

4. Advanced C++ Features: Qt Creator provides several advanced features such as


code completion, refactoring, version control integration, debugging, and unit testing.
These features, when combined with CMake, provide an efficient workflow for
developing, testing, and maintaining C++ projects.

8.3.2 Setting Up a CMake Project in Qt Creator

To begin using CMake with Qt Creator, follow these steps to configure your project
within the IDE:

1. Install Qt Creator:
• First, ensure that Qt Creator is installed on your system. You can download the
installer from the Qt website and follow the installation instructions for your
platform (Windows, macOS, or Linux).
• Make sure to install the necessary Qt libraries if you plan to develop Qt-based
applications.
2. Create a New CMake Project:
• Step 1: Open Qt Creator and select New Project from the welcome screen or
File > New File or Project from the main menu.
• Step 2: In the New Project wizard, select Application under Projects and then
choose C++ Application or Qt Widgets Application, depending on your needs.
• Step 3: When asked to choose a build system, select CMake. Qt Creator will
automatically generate a basic CMakeLists.txt file for your project.
• Step 4: Choose the project location and name, and click Finish to create your
project.
This will create a new CMake-based project with an initial CMakeLists.txt that
includes the necessary setup for the application.
305

3. Open an Existing CMake Project: If you already have a CMake project (with an
existing CMakeLists.txt file), you can easily open it in Qt Creator:

• Step 1: Launch Qt Creator.


• Step 2: From the File menu, select Open File or Project.
• Step 3: Browse to the folder containing your CMake project and select the
CMakeLists.txt file.
• Step 4: Click Open to load the project. Qt Creator will automatically detect the
CMake configuration and load the project settings.

Once the project is loaded, you can start configuring the CMake build system directly
within Qt Creator.

8.3.3 Managing CMake Configuration in Qt Creator

Qt Creator provides a variety of options for configuring your CMake-based project. Let’s
explore the key settings available in the IDE.

1. CMake Configuration Dialog : To access and modify CMake settings:

• Open your project in Qt Creator.


• From the Projects tab, select the Build & Run section.
• You’ll see an option to select the CMake configuration for your project. Qt
Creator automatically detects the CMake configuration, including the generator
(e.g., Ninja, Makefiles) and the CMake executable.

Here are some key options available:

• CMake executable: This is the path to the CMake binary used to configure the
project. Qt Creator will auto-detect this, but you can change it if necessary.
306

• CMake options: This section allows you to specify additional command-line


arguments for CMake, such as custom flags or variables. For instance, you can
add -DCMAKE BUILD TYPE=Release or
-DCMAKE INSTALL PREFIX=/path/to/install.
• Build directory: Qt Creator automatically generates a separate build directory
(typically called build-<project-name>), but you can change this if you
prefer a different structure.

2. Build Configurations
: Qt Creator supports multiple build configurations for different use cases (e.g.,
Debug, Release). For each build configuration, you can define CMake variables or
options to customize the build process.

• To switch between different build configurations, select the Build & Run tab,
then choose the desired configuration from the Build and Run configuration
lists.
• Qt Creator also allows you to add custom build configurations, which can be
especially useful for creating specialized configurations like unit tests or
cross-platform builds.

8.3.4 Building and Running CMake Projects in Qt Creator

Building and running CMake projects in Qt Creator is straightforward and highly


integrated. Here’s how to get started:

1. Build the Project:

• Once the project is set up, you can build it by clicking the Build button (hammer
icon) in the toolbar or by selecting Build > Build Project from the main menu.
307

• Qt Creator will run CMake to generate the necessary build files and then call
the appropriate build system (e.g., make, ninja) to compile the project.

2. Running the Project:

• After the project has been built, you can run it directly from Qt Creator by
clicking the Run button (green arrow icon).
• Qt Creator will launch the application within the IDE. If you’re working with a
Qt GUI application, the window will appear as expected.

3. Managing Run Configurations:

• You can create multiple run configurations within


Qt Creator
. For example, you can specify different parameters, environment variables, or
additional arguments for your program. To create or modify run configurations:
– Go to Projects > Build & Run.
– Click on Run and add or edit configurations for different environments.

8.3.5 Debugging CMake Projects in Qt Creator

Debugging in Qt Creator is fully integrated with CMake-based projects. It provides a


powerful debugger with a variety of useful features for C++ development.

1. Setting Breakpoints:

• To set a breakpoint, click in the left margin next to the line numbers in your
source files. A red dot will appear to indicate the breakpoint.

2. Starting Debugging:

• To start a debugging session, click the Debug button (bug icon) in the toolbar or
press Shift + F9.
308

• Qt Creator will build the project (if necessary) and run the application in debug
mode.
3. Debugger Interface:
• The Debugger tab in Qt Creator shows the call stack, local variables, and
allows you to inspect objects in the program.
• You can step through your code line-by-line, step into functions, step over lines,
and continue execution. You can also examine memory, variables, and the
program’s output.
4. Remote Debugging:
• Qt Creator supports remote debugging. This is useful if you’re working on an
embedded system or need to debug an application running on a different
machine. You can configure Qt Creator to connect to the remote system, set up
breakpoints, and debug as if you were working locally.

8.3.6 Running Tests in Qt Creator

If your CMake project includes unit tests (e.g., using Google Test, Catch2, or
Boost.Test), you can run and manage these tests directly from Qt Creator.

1. Configure Unit Tests:


• In your CMakeLists.txt, add the necessary lines to enable unit testing. For
example, if you're using Google Test, your CMake configuration might look like
this:

enable_testing()
add_subdirectory(tests)
add_executable(test_example test_example.cpp)
309

target_link_libraries(test_example gtest gtest_main)


add_test(NAME TestExample COMMAND test_example)

2. Running Tests:

• Qt Creator detects the unit tests in the project and provides options to run them.
You can open the Test pane from the Projects view and see all the available
tests.
• Running tests within Qt Creator is as simple as selecting a test and clicking
Run. The test results are displayed directly in the IDE, with details on pass/fail
statuses.

3. Debugging Unit Tests:

• You can also debug individual tests by setting breakpoints in your test code and
running the tests in debug mode.

8.3.7 Conclusion

Integrating CMake with Qt Creator provides an efficient workflow for managing and
building C++ projects, whether you are developing Qt-based GUI applications or working
on other C++ projects. Qt Creator’s deep integration with CMake streamlines the project
setup, configuration, building, debugging, and testing processes, allowing developers to
focus on writing high-quality code while Qt Creator handles the rest.
By following the steps outlined in this section, you will be able to leverage the full power
of CMake and Qt Creator to simplify your development process, whether you’re
working on a small project or a large-scale C++ application.
310

8.4 Integration with Xcode on macOS

Xcode is Apple's integrated development environment (IDE) that provides a suite of


software development tools for building macOS, iOS, watchOS, and tvOS applications. It
is a powerful platform for software development, featuring a wide range of tools for
coding, debugging, performance analysis, and more. When it comes to managing and
building C++ projects, Xcode also supports integration with CMake, a popular build
system that simplifies project configuration and management.

This section will explore how to integrate CMake with Xcode on macOS to build and
manage C++ projects efficiently. We will cover how to generate Xcode project files using
CMake, how to configure and build the project within Xcode, and how to take advantage
of Xcode's features, such as debugging and performance analysis, while using CMake.

8.4.1 Why Use CMake with Xcode?

There are several reasons why using CMake with Xcode can benefit macOS developers:

1. Cross-Platform Development:

• CMake is a cross-platform build system, which means that once you configure
your project using CMake, you can generate build files for multiple platforms,
including macOS, Linux, and Windows. By using CMake with Xcode, you can
manage your macOS-specific build configurations while keeping your project
setup portable across different platforms.

2. Seamless Integration with Xcode Features:

• Xcode provides a host of features for software development, including a


graphical interface for editing, debugging, and analyzing applications. Using
311

CMake to generate Xcode projects means you can continue to leverage these
powerful tools while maintaining a flexible, portable build system.

3. Consistency with Other Platforms:

• If you are working in a team or on a project that targets multiple platforms,


CMake ensures that the same build configuration can be used across all
development environments. By using CMake with Xcode, you ensure that your
macOS builds are consistent with other platforms such as Linux or Windows.

4. Support for Large Projects:

• CMake is excellent for managing complex, multi-module C++ projects. When


you work with large projects or cross-platform dependencies, CMake helps
automate the configuration process, making it easier to manage and build.

8.4.2 Setting Up CMake with Xcode

To use CMake with Xcode, the first step is to ensure you have the necessary tools
installed on your macOS system.

1. Install Xcode:

• Download and install


Xcode
from the Mac App Store. Make sure that you also install
Xcode Command Line Tools
by running the following command in the terminal:

xcode-select --install

2. Install CMake:
312

• You will also need to install


CMake
. You can do this via
Homebrew
, a package manager for macOS:

brew install cmake

• Alternatively, you can download the official CMake installer from the CMake
website and install it manually.

3. Verify CMake Installation:

• After installing
CMake
, you can verify the installation by running the following command in the
terminal:

cmake --version

• This should display the installed version of CMake.

8.4.3 Generating Xcode Project Files with CMake

Once CMake is installed, you can generate Xcode project files for your C++ project. This
involves configuring your CMakeLists.txt file to define how the project should be built
and then using CMake to generate the corresponding Xcode project.

1. Creating a Simple CMake Project:

• In your C++ project, create a


313

CMakeLists.txt

file to define your project's build configuration. Here's an example of a simple

CMakeLists.txt

for a basic C++ project:

cmake_minimum_required(VERSION 3.16)
project(MyProject)

# Specify the C++ standard


set(CMAKE_CXX_STANDARD 14)

# Add the source files


add_executable(MyProject main.cpp)

2. Generate the Xcode Project:

• To generate the
Xcode
project files, navigate to your project directory in the terminal and run the
following
CMake
command:

cmake -G "Xcode"

• This command instructs CMake to generate an Xcode project for your current
project directory. After running this command, you will see an Xcode project
file (MyProject.xcodeproj) in the directory.
314

3. Opening the Xcode Project:


• You can now open the generated
Xcode
project by running the following command:

open MyProject.xcodeproj

• Alternatively, you can double-click the MyProject.xcodeproj file in


Finder to open the project in Xcode.
4. Customizing CMake Project:
• As your project grows, you can modify the CMakeLists.txt file to add
additional settings, such as linking external libraries, specifying include
directories, and adding preprocessor definitions.
• For example, to link a library, you would use

target_link_libraries

target_link_libraries(MyProject MyLibrary)

8.4.4 Building and Running the CMake Project in Xcode

Once you’ve generated the Xcode project files using CMake, you can build and run your
project directly from Xcode.

1. Build the Project


:
315

• In Xcode, click the Build button (the hammer icon) in the toolbar, or choose
Product > Build from the top menu to start the build process. Xcode will use
the build configurations generated by CMake to compile your project.
2. Run the Project
:
• After building your project, you can run it by clicking the Run button (the play
icon) in the toolbar or choosing Product > Run from the top menu.
• If you are working with a GUI-based application, the application window will
open. If it’s a console application, the terminal output will be displayed in the
Xcode debug console.

8.4.5 Debugging CMake Projects in Xcode

Xcode offers a rich set of debugging tools that can help you troubleshoot issues in your
C++ projects. When working with CMake, you can take full advantage of these
debugging features.

1. Setting Breakpoints:
• To set a breakpoint in your C++ code, click on the line number where you want
to stop the execution. A blue arrow will appear, indicating that a breakpoint is
set.
2. Starting a Debugging Session:
• To start debugging, click the Debug button (the bug icon) or select Product >
Debug from the menu. Xcode will build your project (if necessary) and start it
in debug mode.
• Execution will pause at your breakpoints, allowing you to inspect variables, step
through code, and evaluate expressions.
316

3. Inspecting Variables:

• While debugging, you can use the Variables View in Xcode to inspect the
values of your variables. This is especially helpful when tracking down memory
or logic issues.
• You can also use the Debug Area at the bottom of the screen to view detailed
information about the program’s execution and variables.

4. Stack Tracing:

• If your application crashes or encounters an error, Xcode provides a stack trace


that shows the call hierarchy at the point of failure. This allows you to trace
back the error to its source.

5. Remote Debugging:

• Xcode also supports remote debugging. This can be useful if you're developing
for iOS or macOS devices, or working in a cross-platform environment. You
can set up your device as the target for debugging and follow the same
debugging steps.

8.4.6 Managing Dependencies with CMake in Xcode

Managing external libraries or dependencies is a common requirement in C++ projects.


CMake makes it easy to add external libraries and link them to your Xcode project.

1. Using find package:

• If your project depends on an external library (e.g., Boost , OpenCV ), you can
use
317

find_package

in your

CMakeLists.txt

to locate the library and link it to your project:

find_package(OpenCV REQUIRED)
target_link_libraries(MyProject ${OpenCV_LIBS})

2. Using ExternalProject:

• For more complex cases where a library needs to be built from source, you can
use the ExternalProject module in CMake to download, build, and link
external projects directly within your Xcode project.

8.4.7 Conclusion

Integrating CMake with Xcode on macOS offers a seamless workflow for managing and
building C++ projects. By generating Xcode project files using CMake, you can leverage
the powerful features of Xcode, such as debugging, profiling, and unit testing, while still
using the flexible, cross-platform build system that CMake provides.
Whether you're working on a macOS-specific application or managing a multi-platform
project, CMake combined with Xcode allows you to streamline your development
process, manage dependencies efficiently, and take full advantage of Xcode’s debugging
and performance analysis tools.
By following the steps outlined in this section, you will be able to build, configure, and
debug your C++ projects in Xcode with the flexibility and power of CMake. This
318

combination is ideal for developers looking to maintain clean, portable, and efficient build
configurations on macOS.
319

8.5 Working with VS Code and CMake Tools

Visual Studio Code (VS Code) is a lightweight yet powerful open-source code editor
developed by Microsoft. It is one of the most popular IDEs for a variety of programming
languages, including C++. With its rich set of features, extensions, and vast ecosystem,
VS Code provides a flexible and customizable development environment. When
combined with CMake, VS Code offers a robust solution for C++ project management,
especially for cross-platform development.

The CMake Tools extension for VS Code is a plugin that allows seamless integration of
CMake functionality directly within the VS Code environment. It simplifies tasks such as
configuring, building, and debugging CMake-based projects without leaving the editor. In
this section, we will explore how to set up VS Code for working with CMake and how to
use the CMake Tools extension effectively.

8.5.1 Installing and Setting Up VS Code for C++ Development

Before you can integrate CMake with VS Code, you need to install the necessary
components and configure the editor for C++ development. Here’s a step-by-step guide to
setting up VS Code.

1. Install VS Code:

• Download and install Visual Studio Code from the official website: VS Code
Download.

2. Install C++ Extension:

• Open VS Code, go to the Extensions view (using the left sidebar or


Ctrl+Shift+X), and search for the C++ extension by Microsoft. This
320

extension provides features like IntelliSense, debugging, and C++ code


navigation.
• Click Install on the C++ extension by Microsoft.

3. Install CMake Tools Extension:

• In the Extensions view, search for the CMake Tools extension by Microsoft.
• Click Install on the extension to add CMake support to VS Code. This
extension provides various CMake-related features such as configuration, build,
and debugging, all accessible directly from the VS Code interface.

4. Install CMake and Build Tools:

• Make sure that CMake is installed on your system. You can install it via
Homebrew (on macOS) or download the installer from the CMake website.
• If you are working on Linux, you can install CMake using your system’s
package manager (e.g., sudo apt install cmake on Ubuntu).
• On Windows, make sure that CMake is added to the system’s PATH during
installation so that it can be accessed from the terminal.

5. Install a Compiler:

• For Windows users, you need a compatible C++ compiler such as MSVC
(Microsoft Visual C++). The Build Tools for Visual Studio package includes
MSVC and related tools.
• For Linux/macOS users, ensure that a C++ compiler (like GCC or Clang) is
installed on the system.

8.5.2 Creating and Configuring a CMake Project in VS Code

Now that VS Code is set up, let’s create and configure a simple C++ project using CMake.
321

1. Creating the Project:

• Create a new folder for your project and add a new C++ source file, for example,

main.cpp

. Below is an example of a simple C++ code snippet for

main.cpp

#include <iostream>

int main() {
std::cout << "Hello, CMake with VS Code!" << std::endl;
return 0;
}

2. Creating the CMakeLists.txt File:

• In the same directory, create a

CMakeLists.txt

file. This file will contain the build instructions for your project. Below is a
simple

CMakeLists.txt

file for a basic project:


322

cmake_minimum_required(VERSION 3.16)
project(HelloWorld)

set(CMAKE_CXX_STANDARD 14)

add_executable(HelloWorld main.cpp)

3. Opening the Project in VS Code:

• Open VS Code and navigate to your project directory. You can either use the
File > Open Folder menu option or the command line (code . in the project
folder) to open the project.

4. Configuring the Project:

• The CMake Tools extension will automatically detect the CMakeLists.txt


file in your project directory and provide configuration options. You’ll see a
blue status bar at the bottom with a message saying ”CMake: Not Configured.”
• Click on the Configure button that appears in the CMake Tools extension (or
press Ctrl+Shift+P and type CMake: Configure). This will trigger
CMake to generate the build system for your project.
• The extension will prompt you to select a build kit, which is essentially a
toolchain for your compiler (e.g., GCC, Clang, MSVC). Select the appropriate
one based on your environment.

5. Building the Project:

• After configuration, you can build the project directly within VS Code. In the
blue status bar, click on the Build button (or press Ctrl+Shift+P and type
CMake: Build).
• CMake Tools will use the selected build system (e.g., Unix Makefiles, Ninja,
323

or MSBuild) to compile the project. Once the build process completes, you’ll
see the output in the Terminal pane at the bottom.
6. Running the Executable:
• To run the executable, click on the green Run button in the status bar or use the
CMake: Run command from the Command Palette (Ctrl+Shift+P). This
will execute your program, and the output will appear in the Terminal pane.

8.5.3 Debugging with CMake Tools in VS Code

VS Code provides integrated debugging capabilities, and with the CMake Tools
extension, you can debug your C++ projects seamlessly.

1. Setting Breakpoints:
• Open your main.cpp file and click on the left gutter next to the line numbers
to set breakpoints. A red dot will appear, indicating where execution will pause
during debugging.
2. Starting a Debugging Session:
• To start debugging, click the Run and Debug icon from the Activity Bar (or use
F5 to start debugging). VS Code will build the project first, then launch the
debugger.
• You will have full access to debugging features, such as stepping through the
code, inspecting variables, viewing call stacks, and more.
3. Debug Configuration:
• If you need to customize your debug configuration, you can modify the
launch.json file in the .vscode folder. The CMake Tools extension can
auto-generate a basic configuration for you. You can modify the configuration
to add specific options for debugging.
324

• Below is an example of a launch.json configuration that works with


CMake projects:

{
"version": "0.2.0",
"configurations": [
{
"name": "Debug (GDB)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/HelloWorl
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"miDebuggerPath": "/usr/bin/gdb",
"preLaunchTask": "CMake: build"
}
]
325

8.5.4 Additional Features of CMake Tools in VS Code

The CMake Tools extension in VS Code provides several advanced features for efficient
CMake project management.

1. CMake Presets:

• CMake Presets allow you to configure different environments for building your
project. You can define multiple configurations and switch between them using
the extension. Presets can specify compiler options, build system types, and
more.

2. Multiple Build Configurations:

• With CMake Tools, you can easily switch between different build
configurations, such as Debug, Release, or custom configurations. You can
configure the active build configuration directly from the VS Code status bar.

3. Testing Integration:

• The extension supports integrating with CTest, the testing tool used by CMake.
If your project contains test cases, you can use the CMake Tools extension to
run and manage tests from within VS Code.

4. CMake Cache Management:

• The CMake Tools extension provides an interface for interacting with the
CMake Cache, allowing you to configure and modify cache entries easily. This
is particularly useful for managing project options and settings.

5. CMake Command Palette:


326

• CMake Tools integrates with the Command Palette (Ctrl+Shift+P),


enabling you to run various CMake commands, such as configuring, building,
testing, and cleaning, without leaving VS Code.

8.5.5 Conclusion

By integrating VS Code with CMake and using the CMake Tools extension, developers
can create a streamlined and efficient workflow for managing C++ projects. This
combination allows you to configure, build, and debug projects all within a lightweight yet
powerful editor, making it easier to develop, test, and maintain complex C++ projects.
The CMake Tools extension simplifies the process of interacting with CMake and
provides a native VS Code interface for common tasks like building, configuring, and
debugging. Whether you are working on a single-platform project or managing a
cross-platform codebase, VS Code and CMake provide a seamless experience for C++
development.
Chapter 9

Using CTest for Project Testing

9.1 Introduction to CTest and Its Importance

In the process of developing C++ projects, ensuring the correctness of your codebase is
critical. Over time, as your project grows, keeping track of potential bugs and regressions
becomes increasingly complex. This is where testing frameworks come into play. While
testing is an essential practice throughout the software development lifecycle, integrating
it effectively with the build system is equally important for maintaining a smooth and
efficient development pipeline.

9.1.1 What is CTest?

CTest is a testing tool provided by CMake, designed to automate the process of running
tests and generating reports. It allows developers to define and execute unit tests,
integration tests, and other quality assurance checks directly within the CMake build
process. By doing so, CTest integrates testing seamlessly with the project’s build,

327
328

providing a unified environment where both building and testing can be managed
efficiently.

CTest works in tandem with CTest-driven testing frameworks, such as Google Test or
Catch2, which provide the actual testing infrastructure. CTest itself doesn’t perform the
tests directly but coordinates their execution, manages results, and generates detailed
reports for the developers.

9.1.2 Why is CTest Important?

Integrating testing into the build system, especially in a larger C++ project, is essential for
several reasons:

1. Automation and Efficiency


One of the core benefits of using CTest is its ability to automate the testing process.
Manual testing is tedious, error-prone, and often neglected during development,
leading to costly bugs that are discovered too late in the project lifecycle. CTest
eliminates this manual step, enabling tests to run automatically after every build or
as part of a Continuous Integration (CI) pipeline. This automation helps developers
detect issues early, saving time and resources.

2. Consistency in Test Execution


With CTest, you can define a standard way to run tests across different environments.
Whether you’re working on your local machine or on a remote server, CTest ensures
that tests are executed consistently with the same configuration. This reduces
discrepancies between different test runs and helps identify real issues.

3. Integration with Continuous Integration (CI) Tools


CTest is commonly integrated with CI/CD tools like Jenkins, GitLab CI, or Travis
CI. This integration is vital for teams that want to ensure that every change pushed to
329

the repository is verified against a set of automated tests. By triggering CTest as part
of the CI pipeline, developers can receive immediate feedback about the stability of
their codebase. This results in faster development cycles and more reliable software.

4. Centralized Test Management


As your project expands, managing and tracking tests can become increasingly
complex. CTest helps organize tests, group them into logical categories, and ensure
that tests are executed in a particular order. Additionally, CTest allows you to
generate reports that provide detailed insights into which tests passed, failed, or were
skipped, making it easier to track your project's health and identify areas that need
attention.

5. Support for Different Test Types


Whether you’re writing simple unit tests, complex integration tests, or performance
tests, CTest supports a wide variety of test types and configurations. You can run
tests for specific parts of your project or execute a suite of tests that cover the entire
codebase. Moreover, CTest supports multiple platforms and configurations, allowing
you to test your project across different compilers, operating systems, and hardware
architectures.

6. Scalability
CTest scales effortlessly with the size and complexity of your project. From small
C++ libraries to large multi-module applications, CTest can handle a diverse range of
test suites and workflows. Its ability to distribute tests and execute them in parallel
on multiple cores or machines further improves performance as the project grows.

9.1.3 Key Features of CTest

• Test Discovery: CTest can automatically discover tests within the project, even if the
test cases are added or removed over time.
330

• Test Reporting: CTest generates human-readable and machine-readable test reports,


which can be integrated with CI systems or stored for later review.
• Test Scheduling: You can control when and how tests are executed—whether you
want to run them after each build or only on specific occasions.
• Test Grouping: CTest allows you to categorize and group tests, enabling you to
execute only a specific set of tests depending on the scope of the changes.
• Advanced Test Configuration: It offers fine-grained control over how tests are run,
such as adjusting the number of retries for failing tests, setting timeouts, or
specifying extra parameters.

9.1.4 CTest vs Other Testing Tools

While other testing tools such as Google Test, Catch2, or Boost Test provide great testing
frameworks, they focus mainly on running and reporting tests. CTest, on the other hand, is
not a testing framework but a test driver that integrates these testing tools with the CMake
build system. It allows you to use different testing frameworks while leveraging CMake’s
configuration and build capabilities.
By using CTest, you achieve a streamlined development process where tests are
seamlessly integrated into your build cycle. This is a step above manually running tests
and makes it far easier to maintain a consistent and automated testing environment.

9.1.5 Conclusion

Incorporating CTest into your development process provides significant advantages,


particularly in the realms of automation, efficiency, and reliability. Whether you are
working in a small team or on a large-scale C++ project, CTest offers an integrated
solution for testing your code, tracking results, and automating the verification of your
331

project’s stability. As part of your CMake-driven workflow, CTest becomes a powerful


ally, ensuring that your project remains robust, maintainable, and continuously improving.
In the next sections, we will dive deeper into how to set up and use CTest effectively
within your CMake-based projects, exploring configurations, test case execution, and
integration with various CI tools.
332

9.2 Writing Tests with add test()

In CMake, testing is a fundamental part of the development workflow, and add test()
is the key command used to define and register tests with CTest. This command allows
you to associate specific tests with your project and makes it easy to automate test
execution through CTest. In this section, we will explore the syntax and usage of
add test(), provide practical examples, and cover best practices to effectively
incorporate tests into your CMake project.

9.2.1 What is add test()?

The add test() command in CMake is used to register individual tests with CTest.
When you invoke add test(), you essentially tell CMake to treat a particular
executable or script as a test that should be run during the testing phase of your build
process.

The basic syntax of the command is:

add_test(NAME test_name COMMAND test_command [ARGUMENTS...])

• NAME: The name of the test. This is an identifier used by CTest to refer to the test.

• COMMAND: The executable or script to be executed for the test. This could be a
compiled test binary, a shell script, or any other executable that can be run from the
command line.

• ARGUMENTS: Optional arguments that are passed to the test executable when it is
run. These can be command-line arguments or test-specific parameters.
333

Once registered using add test(), the test can be run using the ctest command, or
as part of a larger suite of tests in a CI pipeline.

9.2.2 Basic Example

To begin, let’s look at a basic example of defining a unit test in a CMake project.

1. Write a simple test program: Create a small C++ file that outputs a success
message or fails deliberately.

// test_example.cpp
#include <iostream>

int main() {
std::cout << "Test Passed!" << std::endl;
return 0; // Return 0 indicates success
}

1. CMakeLists.txt for the test: In your CMakeLists.txt file, you will need to
compile the test executable and then register it using add test().

cmake_minimum_required(VERSION 3.10)
project(MyProject)

# Add executable for the test


add_executable(test_example test_example.cpp)

# Register the test


add_test(NAME test_example COMMAND test_example)
334

In this example, the test example executable is compiled from


test example.cpp, and then it is registered as a test with add test(). When you
run ctest, this test will execute and print ”Test Passed!” to the output.

9.2.3 Running Tests with CTest

Once you have registered your test using add test(), you can run the tests using the
ctest command:

ctest

This command will search for all tests that were registered via add test(), execute
them, and report their results (whether they passed or failed).
You can also specify individual tests to run by using the -R flag with a regular expression:

ctest -R test_example

This will run only the tests whose names match test example.

9.2.4 Advanced Use of add test()

While the basic usage of add test() is straightforward, there are several advanced
features and options you can use to control how tests are executed:

1. Passing Arguments to the Test


Sometimes, your test executable may require command-line arguments to control its
behavior or provide input. You can pass these arguments directly in the
add test() command:
335

add_test(NAME test_example COMMAND test_example --input data.txt


,→ --verbose)

In this case, the test will be executed with the specified arguments.

2. Grouping Tests Together


For larger projects, it’s helpful to group related tests together to run them as part of a
larger suite. You can do this using add test() in combination with test
directories or categories.

add_test(NAME unit_tests COMMAND test_example)


add_test(NAME integration_tests COMMAND test_integration)

Alternatively, you can organize tests into directories and use regular expressions to
run all tests from a specific group:

ctest -R unit_tests

3. Defining Test Timeouts


In some cases, you may want to define a timeout for a particular test to avoid hanging
indefinitely if the test fails or hangs. This can be accomplished using TIMEOUT:

add_test(NAME test_example COMMAND test_example)


set_tests_properties(test_example PROPERTIES TIMEOUT 10)

Here, the test will automatically be terminated after 10 seconds if it hasn’t


completed.

4. Defining Expected Test Results


336

By default, add test() assumes that an exit code of 0 indicates a successful test,
while a non-zero exit code indicates failure. You can customize this behavior for
specific tests, for example, by specifying that a particular test should pass with a
non-zero exit code:

add_test(NAME test_expected_failure COMMAND


,→ test_expected_failure)
set_tests_properties(test_expected_failure PROPERTIES WILL_FAIL
,→ TRUE)

This will mark the test as expected to fail, and ctest will not report it as a failure.
5. Testing with External Scripts
In addition to testing compiled C++ executables, you can use add test() to run
shell scripts, Python scripts, or any other executable. For example, you could use a
Python script to test specific functionality:

add_test(NAME python_test COMMAND python3 test_script.py)

This is useful when integrating with scripting languages for tasks that are not easily
tested using compiled binaries.

9.2.5 Best Practices for Writing Tests with add test()

To make the most of add test() and maintain a high-quality testing environment,
consider the following best practices:

1. Naming Conventions: Use descriptive and consistent names for your tests. Group
tests logically by functionality, and include the test's purpose in the name for easier
tracking.
337

Example: add test(NAME test addition COMMAND test addition)


and add test(NAME test subtraction COMMAND
test subtraction).
2. Keep Tests Isolated: Ensure that each test is isolated and independent. One test
should not depend on the results of another. This way, failures in one test will not
affect others.
3. Use Assertions in Tests: Within your test code, use appropriate assertions (e.g.,
assert() or framework-specific assertions like those in Google Test) to validate
the results of your tests.
4. Automate Testing: Incorporate tests into your CI/CD pipeline to run them
automatically after each commit, ensuring that bugs and regressions are caught early.
5. Limit Resource Usage: Avoid resource-intensive tests that could slow down your
build or CI pipeline. If necessary, run resource-heavy tests in parallel or on separate
machines.

9.2.6 Conclusion

The add test() command is the foundation for integrating automated testing into your
CMake projects. By using this command effectively, you can define and manage tests,
automate their execution, and integrate them into a continuous testing pipeline.
Understanding how to utilize its advanced features, such as passing arguments, setting
timeouts, and grouping tests, will help you build a robust and maintainable testing
infrastructure for your C++ project.
In the next sections, we’ll look at how to configure and run tests in more advanced
scenarios, including integrating tests with Continuous Integration systems, handling test
dependencies, and reporting detailed results.
338

9.3 Running Tests (ctest)

After you have written and registered your tests using the add test() command in
CMake, the next step is to actually run those tests. The ctest command is the tool
provided by CMake to execute the tests that have been defined in your project. It
automates the process of running your tests, checking their results, and generating reports
that allow you to monitor the stability and correctness of your codebase.
In this section, we will cover how to use ctest, explore its various options for running
tests, discuss how to interpret test results, and look at ways to integrate ctest into your
build and Continuous Integration (CI) workflows.

9.3.1 What is ctest?

ctest is a command-line tool that comes with CMake and is used to run tests that have
been registered with the add test() command. It works with CMake-generated
projects and allows you to run the tests in a controlled manner, check their status, and
generate reports. ctest can be run from the command line interface after the project has
been built, and it provides a range of options to customize how tests are run.
When you run ctest, it will search for all tests that were added via add test() and
execute them. It will then display a summary of the test results, indicating whether each
test passed, failed, or was skipped. Additionally, ctest can output detailed logs and
provide a number of command-line flags to refine your testing process.

9.3.2 Basic Usage of ctest

The simplest way to run tests with ctest is by running the command without any
additional options:
339

ctest

This command will:

1. Search for all registered tests.

2. Execute those tests.

3. Display the test results in a summary format in the terminal.

By default, ctest will run all tests in the current directory and its subdirectories that
were defined via add test().

9.3.3 Running Specific Tests

In a large project with many tests, you may not want to run all tests every time. ctest
allows you to execute a specific test or a subset of tests using several filtering options. The
most common method is by using the -R (regular expression) flag, which allows you to
specify a pattern that matches the names of tests.

For example, if you want to run only tests whose names contain the word ”unit”, you can
use:

ctest -R unit

This command will run only those tests whose names match the regular expression unit.
This is useful for running specific categories of tests, such as unit tests, integration tests, or
performance tests, without running the entire suite.

You can also specify individual tests by name:


340

ctest -R test_addition

This would only run the test named test addition (assuming it’s been defined with
add test()).

9.3.4 Test Output and Reporting

By default, ctest will provide a simple summary of the test results:

Test project /path/to/your/project


Start 1: test_example
1/1 Test #1: test_example ................... Passed 0.01 sec

In this output, Passed indicates that the test passed successfully. If the test fails, it will
be marked as Failed along with additional details.

However, ctest provides several flags to control the level of detail in the output. Some
useful flags for controlling output include:

• -V (Verbose): Displays detailed information about each test as it runs. This includes
the full output from each test, including any std::cout or std::cerr
statements.

ctest -V

• --output-on-failure: This flag ensures that the output of a test will be


shown only if the test fails. This can be helpful to keep the output concise when tests
pass but still provides useful information when something goes wrong.
341

ctest --output-on-failure

• -N (Dry Run): Performs a dry run without actually executing the tests. This will
only show which tests would be executed, without running them.

ctest -N

9.3.5 Running Tests with Timeouts

Sometimes, tests may hang or take longer to run than expected. ctest allows you to set
timeouts for the tests. You can define timeouts for individual tests using
set tests properties() with the TIMEOUT property in your
CMakeLists.txt. However, you can also limit the overall runtime for all tests through
ctest by setting the -T flag to control the maximum amount of time allocated for the
entire test run.

ctest -T 10

This will allow ctest to run for up to 10 seconds before terminating the test process.

9.3.6 Running Tests in Parallel

Running tests sequentially can be time-consuming, especially in larger projects. ctest


supports running tests in parallel, which can significantly reduce the testing time,
especially when tests are independent of each other.
You can enable parallel test execution by using the -j flag, which specifies the number of
tests to run in parallel. For example, to run tests using 4 parallel jobs:
342

ctest -j 4

You can also set -j to 4 or another number depending on the number of CPU cores
available on your machine.

9.3.7 Test Results and Exit Codes

The results of the test execution are returned through the exit code of ctest. The exit
code allows you to integrate ctest into CI/CD pipelines and other automated systems.

• Exit code 0: All tests passed.


• Exit code 1: At least one test failed.
• Exit code 2: ctest was unable to run the tests due to a configuration error (e.g.,
missing executable, misconfigured tests).

This makes it possible to use ctest within build systems or CI tools to monitor the
success or failure of tests programmatically.

9.3.8 Integration with Continuous Integration (CI) Tools

Integrating ctest into Continuous Integration (CI) pipelines is an essential practice for
modern software development. By running tests automatically each time code is
committed or merged, teams can quickly identify regressions or other issues that may
affect the stability of the software.
Most CI systems (e.g., Jenkins, GitLab CI, Travis CI) provide built-in support for running
ctest commands as part of their build scripts. Here is an example of how you might use
ctest in a Jenkins pipeline:
343

1. Run the build to compile your CMake project.


2. Run the tests using ctest:

ctest -j 4

This will execute the tests using 4 parallel jobs. If any tests fail, ctest will return a
non-zero exit code, which the CI system can use to mark the build as failed.

9.3.9 Conclusion

The ctest command is an indispensable tool for managing and running tests in
CMake-based projects. It provides a straightforward way to execute tests, customize their
execution, and report their results. Whether you are running a simple project or a complex
multi-module C++ application, ctest allows you to integrate automated testing into
your workflow, automate quality assurance tasks, and ensure that your project remains
stable and robust. By mastering ctest and utilizing its powerful features, you can
achieve faster feedback loops, reduce manual testing effort, and catch issues early in the
development process.
In the next section, we will explore advanced topics such as generating custom test reports,
integrating ctest with various CI/CD systems, and handling test dependencies to further
enhance your testing pipeline.
344

9.4 Unit Testing with Google Test

Unit testing is an essential practice in software development, ensuring that individual units
or components of the code work as expected. While ctest allows for running tests in
general, integrating a robust testing framework can provide greater flexibility, clarity, and
functionality. Google Test (often abbreviated as gtest) is one of the most popular C++
testing frameworks, known for its rich feature set, ease of use, and compatibility with
CMake and ctest.

In this section, we will explore how to integrate Google Test with your CMake project,
write unit tests, run them using ctest, and leverage the full potential of the Google Test
framework. By the end of this section, you’ll be able to write unit tests for your C++
project, use assertions to check conditions, and incorporate Google Test into your
CMake-based build system.

9.4.1 What is Google Test?

Google Test is an open-source, cross-platform C++ testing framework developed by


Google. It provides a wide variety of assertions, test fixtures, and advanced features such
as test mocking, death tests, and test parameterization. It integrates seamlessly with
CMake and ctest, allowing you to write, manage, and run unit tests for your C++ code.

Key features of Google Test include:

• Assertions: A rich set of assertions such as EXPECT EQ, ASSERT TRUE,


ASSERT NE, and more, which help verify that the code behaves as expected.

• Test Fixtures: Setup and teardown code that runs before and after each test,
allowing you to share code between tests.
345

• Test Organization: Tests can be grouped into test suites (test cases) and tests, and
they are executed automatically with meaningful test names.

• Mocking: Integration with Google Mock for creating mock objects in your tests.

• Rich Output: Detailed test results are printed, including failures, which makes
debugging easier.

9.4.2 Integrating Google Test with CMake

To begin using Google Test in your CMake-based project, you need to integrate it into
your project’s build system. Fortunately, CMake provides excellent support for Google
Test, either by downloading it as a submodule or by installing it on your system.

1. Download Google Test


You can add Google Test to your project in several ways, but the simplest approach
is to download it as a Git submodule. This ensures that the correct version of Google
Test is always available for your project.
To include Google Test as a submodule, follow these steps:

1. Navigate to your project’s root directory in the terminal.


2. Add Google Test as a Git submodule:

git submodule add https://github.com/google/googletest.git


,→ extern/googletest
git submodule update --init --recursive

3. Modify your CMakeLists.txt file to include Google Test:


346

# Add Google Test as a subdirectory


add_subdirectory(extern/googletest)

4. Link Google Test to your test executable:

# Add executable for the test


add_executable(test_example test_example.cpp)

# Link Google Test with the test executable


target_link_libraries(test_example gtest gtest_main)

Now, you’ve successfully included Google Test in your CMake project, and you can
start writing unit tests.
2. Alternatively, Install Google Test System-Wide
If you prefer, you can install Google Test on your system instead of including it as a
submodule. You can do this by following the instructions on the Google Test GitHub
page for your platform. After installation, you can link your tests to the installed
version of Google Test by modifying your CMakeLists.txt file to find and link
the library:

find_package(GTest REQUIRED)
add_executable(test_example test_example.cpp)
target_link_libraries(test_example GTest::GTest GTest::Main)

9.4.3 Writing Unit Tests with Google Test

Once Google Test is integrated into your project, you can begin writing unit tests for your
code. Unit tests typically involve testing individual functions or classes to ensure that they
perform as expected under different conditions.
347

1. Include the Google Test Header: Each test file requires the Google Test header,
which provides the necessary functionality for writing and running tests.

#include <gtest/gtest.h>

1. Define Test Cases and Test Fixtures: In Google Test, tests are organized into test
suites (test cases) and individual tests. A test suite is a collection of related tests, and
each test case typically tests one specific unit of functionality. Test cases are defined
using the TEST macro.

// Sample test case for addition


TEST(AdditionTest, PositiveNumbers) {
EXPECT_EQ(1 + 1, 2); // Assertion to check that 1 + 1 equals 2
EXPECT_EQ(2 + 3, 5); // Another assertion
}

TEST(AdditionTest, NegativeNumbers) {
EXPECT_EQ(-1 + -1, -2); // Testing negative numbers
}

1. Assertions: Google Test provides various assertion macros to check expected


outcomes:

• EXPECT EQ(val1, val2) – Tests if val1 == val2.


• EXPECT NE(val1, val2) – Tests if val1 != val2.
• ASSERT TRUE(condition) – Asserts that a condition is true.
• ASSERT FALSE(condition) – Asserts that a condition is false.
348

• ASSERT THROW(statement, exception type) – Asserts that an


exception of type exception type is thrown by statement.

You should use ASSERT * for conditions that should never fail, causing the test to
terminate early, while EXPECT * allows for continued execution even if the
assertion fails.
2. Test Fixtures: For tests that need setup and teardown operations (e.g., creating
objects, opening files), Google Test provides test fixtures. Test fixtures define
common setup and teardown code that is shared by multiple tests in the same test
case.

class MathTest : public ::testing::Test {


protected:
int a;
int b;

void SetUp() override {


a = 5;
b = 10;
}
};

TEST_F(MathTest, Addition) {
EXPECT_EQ(a + b, 15);
}

TEST_F(MathTest, Subtraction) {
EXPECT_EQ(b - a, 5);
}

In the above example, MathTest is a test fixture with SetUp() code that initializes a
349

and b before each test.

9.4.4 Running Unit Tests with Google Test

Once you have written your unit tests using Google Test, you can run them in the same
way as any other CMake-based tests.

1. Build the Test Executable: First, ensure that your tests are compiled by running the
following command:

cmake --build .

2. Run the Tests with CTest: After the tests are built, you can run them using ctest
or directly by running the executable. To run the tests using ctest, simply execute:

ctest

Or run the tests directly from the command line by executing the test executable
(e.g., ./test example).
If you wish to run only a specific test case or test, you can specify the name using the
-R flag:

ctest -R AdditionTest

This will only run tests in the AdditionTest suite.

9.4.5 Using Google Mock with Google Test

Google Test also integrates seamlessly with Google Mock, another open-source library by
Google used for creating mock objects in unit tests. Google Mock allows you to mock
350

dependencies in your unit tests, making it easier to isolate the unit under test and verify
interactions with dependencies.
If you need to mock objects or functions, you can include Google Mock in the same way
as Google Test and use it for more advanced unit testing scenarios.

9.4.6 Conclusion

Unit testing with Google Test is an essential part of ensuring the correctness of your C++
code. By integrating Google Test with CMake and ctest, you can automate the process
of writing, running, and reporting unit tests. Google Test provides a wealth of features,
including assertions, test fixtures, and powerful mocking capabilities, making it an
excellent choice for testing complex C++ projects.
With Google Test and CMake, you can establish a strong testing foundation for your C++
projects, leading to higher code quality, easier debugging, and more reliable software. In
the next sections, we will explore how to enhance your testing workflow with Continuous
Integration and more advanced testing strategies.
351

9.5 Generating Test Reports and Analyzing Results

One of the primary goals of running tests in a project is to gather valuable feedback on the
code's correctness, stability, and performance. Simply running tests isn't enough; you need
to be able to analyze the results effectively and generate comprehensive reports that
provide insights into the health of your project. In this section, we will explore how to
generate test reports using ctest and how to analyze the results to improve the quality of
your project.

By the end of this section, you will understand how to generate various types of test
reports, customize the reporting output, and interpret the results to take action on any
issues in your project.

9.5.1 Why Test Reports Matter

Test reports are crucial for understanding the success or failure of individual tests, test
suites, or the entire test suite. Detailed reports help developers:

• Identify failing tests quickly.

• Investigate the causes of failures by providing detailed logs and error messages.

• Track trends over time to see if code changes introduce regressions or new issues.

• Integrate testing results into Continuous Integration (CI) and build systems to
automate monitoring.

Effective test reports go beyond just indicating whether tests passed or failed—they
provide necessary context, detailed logs, and other diagnostic information that allow
teams to respond promptly to any issues.
352

9.5.2 Generating Simple Test Output with ctest

By default, ctest provides a summary output with the results of all tests executed. The
simplest output looks like this:

Test project /path/to/your/project


Start 1: test_example
1/1 Test #1: test_example ................... Passed 0.01 sec

While this provides basic information about whether the test passed or failed, more
detailed reports are often required to understand why tests fail, which tests failed, and how
to fix them.

To get more detailed output in the terminal, use the -V (verbose) flag:

ctest -V

This will show the complete output of each test, including any std::cout,
std::cerr, or other diagnostic information from the test run. This is useful for seeing
detailed logs, especially when tests fail and you need to troubleshoot the issue.

9.5.3 Generating XML Reports with ctest

For more advanced reporting and integration with other tools, ctest can generate XML
reports. These reports can be processed and parsed by Continuous Integration (CI)
systems, test coverage tools, or other external applications that track the health of your
project.

To generate an XML report with ctest, use the following command:


353

ctest --output-on-failure -T test -D Experimental

Here’s a breakdown of the options:

• -T test: This specifies that we are running the tests and generating reports.
• --output-on-failure: This ensures that the output is shown only when a test
fails, keeping the report focused and concise.
• -D Experimental: This flag helps categorize the results as ”Experimental” for
easier tracking in a CI system.

After running the tests, ctest will generate a file called CTestTestfile.cmake that
contains the XML-formatted test results. The XML file contains detailed information
about each test, such as:

• The name of the test.


• The status of the test (passed, failed, or skipped).
• The duration of each test.
• Any output generated by the test (e.g., std::cout or error messages).

Here is an example of a simple XML report:

<?xml version="1.0"?>
<testsuite name="MyTestSuite" tests="1" failures="0" errors="0"
,→ skipped="0" timestamp="2023-02-04T12:00:00">
<testcase name="test_example" time="0.01">
<failure message="Test failed because of XYZ reason">Stack trace
,→ here</failure>
</testcase>
</testsuite>
354

This XML file can be integrated into build systems or CI tools like Jenkins, GitLab CI, or
Travis CI, which can automatically parse the results, track trends, and report on failures
and successes.

9.5.4 Integrating with CI Tools

Most modern Continuous Integration (CI) tools (such as Jenkins, GitLab CI, Travis CI,
or CircleCI) can consume the XML reports generated by ctest. Integrating Google
Test and ctest into your CI pipeline allows you to automate the testing and reporting
process, ensuring that your codebase remains stable as you make changes.
Here's how to set up ctest reporting with popular CI tools:

• Jenkins Integration
To integrate ctest with Jenkins, you need to:
1. Set up a Jenkins job to build your CMake project.
2. In the
”Post-build Actions”
section, configure Jenkins to parse the test results:
– Choose ”Publish JUnit test result report”.
– Point Jenkins to the XML file generated by ctest (e.g.,
CTestTestfile.xml).
Jenkins will automatically parse the XML file, display the results on the Jenkins
dashboard, and notify you of any failing tests.
• GitLab CI Integration
For GitLab CI, you can use the JUnit format to parse ctest results. In your
.gitlab-ci.yml file, you can configure the ctest output to be saved as a
JUnit XML file:
355

test:
script:
- cmake --build .
- ctest -T test --output-on-failure -D Experimental
artifacts:
paths:
- CTestTestfile.xml
allow_failure: false

GitLab will then display the results in a well-formatted test report in the CI
dashboard.

• Travis CI Integration
For Travis CI, you can configure the .travis.yml file to run ctest and store
the results:

script:
- cmake --build .
- ctest --output-on-failure -T test -D Experimental

Travis will automatically display the test results in the build log, and you can use the
generated XML for further processing.

9.5.5 Analyzing Test Results

Once your test results are generated, it's time to analyze them and understand the health of
your project.

• Understanding Test Statuses


356

– Passed Tests: Indicate that the functionality works as expected. These tests are
a sign that your project is functioning correctly for the covered scenarios.
– Failed Tests: Indicate that the expected behavior was not met. You need to
investigate the failure reason, often by reviewing logs and error messages.
Common causes of test failures include logic errors, regression bugs, or
misconfigurations in your build or test environment.
– Skipped Tests: Tests may be skipped due to missing dependencies, incorrect
configurations, or platform-specific issues. If too many tests are skipped, it’s
essential to investigate why and fix the issues to ensure full test coverage.

• Tracking Trends and Identifying Issues


One of the most valuable aspects of generating test reports is tracking test results
over time. With CI tools, you can monitor trends such as:

– New failures: Are failing tests occurring more frequently after recent changes?
This may signal regressions.
– Test coverage: Are all areas of your codebase adequately tested? If certain
modules or functions lack tests, it’s time to write new tests.
– Test stability: Are tests passing consistently, or are there intermittent failures?
Intermittent failures may indicate issues with the environment or test setup.

By tracking these trends, you can ensure that your project remains in a stable state
and catch issues early before they escalate.
• Advanced Report Analysis with External Tools
While ctest provides a simple XML format, for more complex analysis, you can
integrate external tools like SonarQube for code quality and test coverage reports,
or Coveralls for tracking test coverage trends.
Additionally, advanced visualization tools like Allure can help you generate more
human-readable reports from the raw ctest XML files. These tools can enhance
357

your reporting system and make it easier for developers and stakeholders to analyze
the results.

9.5.6 Conclusion

Generating test reports and analyzing results is a crucial part of a successful testing
process. By using ctest, you can generate detailed XML reports that integrate with CI
systems and external tools, providing a comprehensive view of your project's health. By
effectively analyzing the test results, you can catch regressions early, improve test
coverage, and ensure your project remains stable as it evolves.
In the next section, we will explore best practices for managing and structuring tests in
large projects and dive deeper into automated testing strategies that help maintain code
quality over time.
Chapter 10

Packaging Projects with CPack

10.1 Introduction to CPack and Its Role

When developing C++ projects, it’s not enough to just build them—at some point, you’ll
want to package them for distribution, deployment, or sharing with others. This is where
CPack comes in. CPack is a powerful, flexible tool integrated into CMake that facilitates
the creation of installation packages for various platforms and formats. Whether you're
distributing your software as binaries, source code, or installers, CPack simplifies the
packaging process and ensures that your project can be shared with others with minimal
friction.

10.1.0.1 What is CPack?

CPack is a packaging system that is tightly integrated with CMake, designed to create
distribution packages of your project. Once you’ve used CMake to configure and build
your project, CPack takes over and handles the creation of packages. These packages can

358
359

then be shared and installed on different systems, making it a crucial tool for developers
who need to provide easy access to their software.
CPack supports various packaging formats such as .zip, .tar.gz, .rpm, .deb,
.dmg, and .msi, among others. It can generate platform-specific installers that help
automate the process of installing software, ensuring that all necessary files are placed in
appropriate directories and that any necessary environment setup is done.

10.1.0.2 Why Should You Use CPack?

For many developers, packaging a project can be a tedious and error-prone process.
Manual creation of installers, configuration of paths, and ensuring that all dependencies
are bundled correctly can be time-consuming. This is especially true when your software
needs to be distributed across different platforms (Windows, Linux, macOS) and
packaging formats.
CPack streamlines this process by providing a standardized way of creating and managing
packages. Instead of having to manually configure packaging for each target platform,
CPack automates this, saving you significant time and effort. Furthermore, because CPack
is part of CMake, you don’t need to learn a new tool or set up a complex build system. It
integrates seamlessly with your existing CMake configuration files, which reduces
overhead and simplifies maintenance.

10.1.0.3 How Does CPack Fit into the CMake Workflow?

The role of CPack is to handle the final step in the CMake-based project build process:
creating distributable packages. After you’ve used CMake to configure your project and
run the build process (compiling your source code, linking libraries, etc.), CPack helps
create the final deliverables. CPack can package everything from binaries and
documentation to configuration files, ensuring your project is ready for deployment.
360

Here’s a high-level view of the typical CMake workflow, including CPack:

1. Configuration: First, CMake is run to configure the project. This step defines how
the project will be built, what source files to include, which dependencies to link,
and so on.
2. Build: Once the configuration is complete, you can use CMake to build the project.
This involves compiling source files, running tests, and creating any necessary
artifacts, such as libraries or executables.
3. Packaging: After the build process, CPack takes over. CPack will use the
instructions provided in your CMake configuration to package the project into a
distribution format of your choice (e.g., .deb, .rpm, .dmg, .zip, or .msi).
4. Installation: If you choose to distribute an installer package, CPack can also
generate an installer that makes the installation process easier for users. This
installer can automatically detect and configure the required paths and dependencies.

10.1.0.4 Key Benefits of Using CPack

1. Cross-platform Support: CPack supports multiple platforms and packaging


formats out of the box. It allows you to create installer packages for Windows,
macOS, and Linux with minimal effort.
2. Integration with CMake: Since CPack is a CMake module, there’s no need to learn
a new tool or modify your existing CMake configuration files. You simply extend
your CMakeLists.txt with additional packaging instructions, making the integration
seamless.
3. Customizability: While CPack provides many built-in packaging options, it also
offers the flexibility to customize packaging to suit your specific needs. You can
specify which files are included, configure installation locations, add
post-installation steps, and much more.
361

4. Ease of Use: Creating a package with CPack is simple. After configuring your
project with CMake, you can generate the package with a single command: cpack.
This simplicity makes it accessible to both novice and advanced developers.
5. Extensibility: CPack can be extended with custom scripts and commands to
accommodate more complex packaging scenarios. Whether you need to create
specialized installation routines or include custom build steps, CPack gives you the
freedom to extend its functionality.
6. Consistency: Because CPack is part of CMake’s ecosystem, it ensures consistency
across different projects. Once you’re familiar with CMake, you don’t have to learn
a new packaging system for each of your projects.

10.1.0.5 How to Get Started with CPack

Getting started with CPack is straightforward if you already have a CMake-based project.
In fact, most of the setup involves adding a few additional lines to your existing
CMakeLists.txt file. In this chapter, we’ll walk through a detailed example of how to
configure and use CPack to package your project, including best practices for creating
installer packages and distribution archives.
362

10.2 Creating Installation Packages (.deb, .rpm, .msi,


.tar.gz)

Once you have configured your project with CMake and are ready to distribute it, CPack
provides a variety of options for packaging your software. These packages can be
generated in formats suitable for different platforms, making it easy for users to install
your software on various operating systems. In this section, we’ll cover how to use CPack
to create common installation package formats: .deb for Debian-based Linux
distributions, .rpm for Red Hat-based Linux distributions, .msi for Windows, and
.tar.gz for source code distribution and Linux environments.

1. Packaging for Linux: .deb and .rpm


For Linux-based systems, two of the most common package formats are .deb
(Debian package) and .rpm (Red Hat Package Manager). These formats allow for
easy installation via package managers such as dpkg for Debian-based systems (like
Ubuntu) and rpm for Red Hat-based systems (like Fedora, CentOS, or RHEL).
CPack can automatically create these packages with minimal configuration.

Creating a .deb Package


The .deb package format is widely used by Debian-based distributions. CPack
provides an easy way to create these packages, which can then be installed via dpkg
or other package managers.
Steps to create a .deb package:

1. Add the CPack module to your CMakeLists.txt: Make sure that the
CPACK GENERATOR is set to include .deb.
363

set(CPACK_GENERATOR "DEB")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Your Name
,→ <[email protected]>")

2. Specify package information: CPack allows you to specify important metadata


about your package, such as the package name, version, maintainer, description,
and dependencies.

set(CPACK_PACKAGE_NAME "your-project-name")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION "A brief description of your
,→ project")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libstdc++6")

3. Generate the package: After configuring your project with CMake and
building it, run the following command to create the .deb package:

cpack -G DEB

This command will produce a .deb file that can be installed on any
Debian-based system using dpkg:

sudo dpkg -i your-project-name-1.0.0.deb

Creating an .rpm Package


The .rpm package format is used by Red Hat-based distributions such as Fedora
and CentOS. Similar to .deb packages, .rpm packages allow users to install
software with the rpm command or via tools like yum or dnf.
Steps to create a .rpm package:
364

1. Set the generator in CMakeLists.txt: Similar to the .deb package,


configure the CPACK GENERATOR to use .rpm.

set(CPACK_GENERATOR "RPM")

2. Specify package metadata: Just like with .deb packages, you can specify
metadata for the .rpm package.

set(CPACK_PACKAGE_NAME "your-project-name")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION "A brief description of your
,→ project")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")

3. Generate the package: Once you have configured your project, use the
following command to create the .rpm package:

cpack -G RPM

This will produce an .rpm package that can be installed with the rpm tool:

sudo rpm -i your-project-name-1.0.0.rpm

2. Packaging for Windows: .msi


For Windows, one of the most popular formats for software distribution is the .msi
installer package. CPack can generate .msi packages, which provide an easy-to-use
installation process with a GUI, and allow users to install your software by simply
following an installation wizard.

Creating an .msi Package


365

1. Set the generator in CMakeLists.txt: To create an .msi package, set


CPACK GENERATOR to "MSI".

set(CPACK_GENERATOR "MSI")

2. Specify package metadata: For .msi packages, you can define various
installer properties, such as the installer’s title, the company’s name, and the
default installation directory.

set(CPACK_PACKAGE_NAME "your-project-name")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION "A brief description of your
,→ project")
set(CPACK_MSI_PACKAGE_INSTALL_DIRECTORY "C:\\Program
,→ Files\\your-project-name")

3. Generate the package: After running CMake and building the project, create
the .msi package by running the following:

cpack -G MSI

The output will be an installer .msi file that users can execute to install your
software. The installer will guide users through the installation process,
ensuring that files are placed in the correct directories and dependencies are
handled properly.
3. Packaging for Source Code: .tar.gz
In addition to binary packages, you might want to distribute the source code of your
project. The .tar.gz format is a common choice for source code distribution on
Linux and macOS systems. This format is widely recognized and allows users to
easily unpack the source code and build it manually.
366

Creating a .tar.gz Package

1. Set the generator in CMakeLists.txt: For a source distribution, you can


specify .tar.gz by setting CPACK GENERATOR to "TGZ", which will
create a .tar.gz file.

set(CPACK_GENERATOR "TGZ")

2. Configure the package information: Similarly to the binary packages, you can
specify the project name, version, and description.

set(CPACK_PACKAGE_NAME "your-project-name")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION "A brief description of your
,→ project")

3. Generate the package: After running CMake and building the project, you can
create the .tar.gz source package by running:

cpack -G TGZ

This command will generate a your-project-name-1.0.0.tar.gz


file that contains the source code of your project. This file can be extracted
using the tar command:

tar -xvzf your-project-name-1.0.0.tar.gz

Users can then navigate into the extracted directory and build the project
manually.

4. Final Thoughts on CPack Package Formats


367

CPack simplifies the process of creating installation packages for different platforms,
allowing you to focus on your project rather than worrying about packaging
complexities. By supporting multiple formats (.deb, .rpm, .msi, .tar.gz),
CPack ensures that your software can be distributed across various environments and
is easy to install for end users.
Each packaging format has its own use cases and advantages, so the format you
choose will depend on your target platform and the needs of your users. CPack’s
flexibility and ease of use make it an invaluable tool in the CMake ecosystem for
managing software distribution.
368

10.3 Configuring CPackConfig.cmake

While CPack makes it easy to package your project, fine-tuning the packaging process
requires further customization. This is where CPackConfig.cmake comes into play.
This file provides an additional level of control over the packaging configuration, allowing
you to customize various aspects of the package creation process beyond the basic CMake
and CPack configuration. By configuring CPackConfig.cmake, you can define how
your package will behave during the packaging process, set custom values for the package
metadata, and handle complex packaging scenarios with ease.

10.3.1 What is CPackConfig.cmake?

CPackConfig.cmake is a configuration file that CPack uses to store additional


packaging details and customizations. While CMakeLists.txt handles the basic
configuration and build process, CPackConfig.cmake allows you to set advanced
packaging options and override default behavior. This file is generally included in the
build directory and is processed when CPack is invoked to create the package.

When you invoke CPack via the cpack command, it first looks for
CPackConfig.cmake to determine any specific customizations for packaging. If this
file is not found, CPack will proceed with default settings defined in your
CMakeLists.txt.

10.3.2 Creating and Using CPackConfig.cmake

The CPackConfig.cmake file can be placed in your project’s root directory or


generated in your build directory during the build process. To use it, you typically need to
tell CMake to include it in the configuration process. You can do this by modifying the
369

CMakeLists.txt to include the configuration file or specifying it directly when


running the CPack command.

Basic Example of Creating CPackConfig.cmake

1. Create CPackConfig.cmake:
In your project’s root directory (or build directory), create a file named
CPackConfig.cmake. Here’s an example of a basic CPackConfig.cmake
file:

# CPackConfig.cmake

# Set the package version


set(CPACK_PACKAGE_VERSION "1.0.0")

# Set the package name


set(CPACK_PACKAGE_NAME "your-project-name")

# Set the package description


set(CPACK_PACKAGE_DESCRIPTION "A brief description of your
,→ project")

# Specify the installer package for Windows


set(CPACK_GENERATOR "MSI;ZIP")

# Specify the installation directory for Windows


set(CPACK_MSI_PACKAGE_INSTALL_DIRECTORY "C:\\Program
,→ Files\\YourProject")

# Set Debian package dependencies


set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libstdc++6")
370

# Set the directory to which the package will be installed


set(CPACK_INSTALL_PREFIX "/usr/local")

# Configure additional metadata (optional)


set(CPACK_PACKAGE_VENDOR "Your Company")
set(CPACK_PACKAGE_CONTACT "[email protected]")

include(CPack)

2. Include CPackConfig.cmake in CMakeLists.txt:


In your CMakeLists.txt, include CPackConfig.cmake at the end of your
configuration.

# CMakeLists.txt

project(YourProject)

# Other CMake configurations for your project


...

# Include CPack configuration


include(CPackConfig.cmake)

3. Run CPack:
Once you've configured the CPackConfig.cmake file, you can run CPack as
usual:

cpack

This will generate the package according to the settings you specified in the
CPackConfig.cmake file.
371

10.3.3 Key Configuration Options in CPackConfig.cmake

There are many customizable options you can set in CPackConfig.cmake. Some of
the most commonly used options include:

1. Package Metadata
You can define several key attributes for your package, such as name, version,
description, vendor, and contact information. This metadata will be included in the
final package and can be useful for users or for distributing your software.
• CPACK PACKAGE NAME: The name of your package.
• CPACK PACKAGE VERSION: The version of your package.
• CPACK PACKAGE DESCRIPTION: A short description of your project.
• CPACK PACKAGE VENDOR: The vendor name or company.
• CPACK PACKAGE CONTACT: A contact email address.
Example:

set(CPACK_PACKAGE_NAME "MySoftware")
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_DESCRIPTION "An awesome software package")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_CONTACT "[email protected]")

2. Package Generators
You can specify the format(s) in which you want your package to be generated.
CPack supports several formats, such as .deb, .rpm, .tar.gz, .zip, .msi,
and more. By defining the CPACK GENERATOR, you can generate one or multiple
package formats.
Example:
372

set(CPACK_GENERATOR "TGZ;DEB;RPM")

This will generate .tar.gz, .deb, and .rpm packages when CPack is invoked.
3. Installation Prefix
The installation prefix defines the root directory where your package will be installed
on the user’s system. This can be set to different values depending on the packaging
format. For instance, for Unix-based systems, it’s common to use /usr/local.
Example:

set(CPACK_INSTALL_PREFIX "/usr/local")

4. Windows MSI Specific Options


For Windows .msi packages, CPack allows you to set specific properties, such as
the default installation directory and additional Windows installer options.

• CPACK MSI PACKAGE INSTALL DIRECTORY: Sets the default installation


directory for Windows.

Example:

set(CPACK_MSI_PACKAGE_INSTALL_DIRECTORY "C:\\Program
,→ Files\\MySoftware")

5. Package Dependencies
For .deb and .rpm packages, you can specify package dependencies to ensure that
the package installer automatically installs the necessary libraries or packages before
the software can be installed.
Example:
373

set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libstdc++6")


set(CPACK_RPM_PACKAGE_DEPENDS "glibc, libstdc++")

6. Customizing Package Content


You can also specify which files are included or excluded from the package and how
the package should behave during installation. For instance, you can set custom
post-installation steps, include additional files like configuration files, or define
special installation scripts.

• CPACK INSTALL CMAKE PROJECTS: Defines CMake projects to be


included in the package.
• CPACK COMPONENTS ALL: Specifies which components should be included.

Example:

set(CPACK_INSTALL_CMAKE_PROJECTS "path/to/your/project;ALL")

10.3.4 Advanced Usage: Customizing the Packaging Process

The CPackConfig.cmake file can be used for advanced customizations, such as:

• Custom post-installation scripts: You can write scripts that run after installation,
such as updating configuration files or setting environment variables.

• Versioning control: Dynamically adjust the version of the package by pulling it


from a Git repository or a version control system.

• Adding additional files: Include non-standard files or directories in the package,


such as license files, documentation, or configuration templates.
374

10.3.5 Final Thoughts on CPackConfig.cmake

The CPackConfig.cmake file provides you with a great deal of flexibility to


customize the packaging process for your CMake-based project. By fine-tuning the
configuration file, you can control the package format, set metadata, manage
dependencies, and ensure that the installation process aligns with your project’s needs.
Using CPackConfig.cmake in combination with the settings in CMakeLists.txt
allows you to fully automate the creation and customization of your project’s installer
packages, making distribution a smooth and hassle-free process. Whether you are
packaging for a Linux distribution, Windows, or for source code distribution,
CPackConfig.cmake gives you the tools to create tailored installation packages that
meet your requirements.
375

10.4 Supporting Multiple Operating Systems

When packaging software, one of the most significant challenges is ensuring compatibility
across different operating systems. This is especially true when working with C++
projects, which are often designed to run on multiple platforms. CPack, being part of the
CMake ecosystem, offers powerful features that enable you to create installation packages
that work seamlessly across Windows, Linux, macOS, and other Unix-like systems. In
this section, we will explore the best practices and techniques for supporting multiple
operating systems with CPack, ensuring that your project can be distributed and installed
with ease on different platforms.

10.4.1 Challenges of Supporting Multiple OSes

Each operating system has its own package management system, installation directories,
system paths, and conventions. For instance:

• Windows uses .msi or .zip files, where installation often requires creating
registry entries or handling system-specific directories like Program Files.
• Linux uses package formats like .deb or .rpm, which rely on package managers
like dpkg or rpm. Installation paths and dependencies are managed differently
compared to Windows.
• macOS often uses .dmg or .pkg files for installation, with different conventions
for system paths and directory structures.

As a result, packaging for multiple operating systems requires careful consideration of


these differences. Fortunately, CPack helps mitigate these challenges by allowing you to
specify OS-specific configurations and generating platform-specific installation packages
automatically.
376

10.4.2 Cross-Platform Packaging with CPack

CPack simplifies the process of creating platform-specific packages through its


configuration system. It allows you to define settings in a way that CPack will
automatically adjust for each operating system. The key to cross-platform support lies in
configuring CMake and CPack correctly, along with using conditional logic to set specific
options for different platforms.
Let’s look at some essential techniques for supporting multiple operating systems.

1. Setting Up Conditional Logic in CMakeLists.txt


One of the first steps in supporting multiple operating systems is to use CMake’s
built-in variables to detect the operating system and adjust settings accordingly.
CMake provides the CMAKE SYSTEM NAME variable, which indicates the target
platform (e.g., Windows, Linux, macOS). This variable allows you to use
conditional logic to apply OS-specific settings when configuring your project.

Example: Defining Platform-Specific Settings


In your CMakeLists.txt, you can define different settings based on the
operating system:

# Detect the operating system


if(WIN32)
set(CPACK_GENERATOR "MSI;ZIP")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "C:\\Program
,→ Files\\MyProject")
elseif(APPLE)
set(CPACK_GENERATOR "TGZ;DMG")
set(CPACK_PACKAGE_INSTALL_DIRECTORY
,→ "/Applications/MyProject")
elseif(UNIX)
377

set(CPACK_GENERATOR "DEB;RPM;TGZ")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "/usr/local/MyProject")
else()
message(FATAL_ERROR "Unsupported platform")
endif()

In this example, we use if, elseif, and else to specify different package
formats and installation directories depending on the detected platform. CPack will
automatically adjust and generate the appropriate package for each platform during
the packaging process.
2. Platform-Specific Configuration Files
For complex projects, you may need to create additional configuration files that are
tailored to the specifics of each operating system. These files can help you set
platform-specific settings, include or exclude files, and define behavior that differs
across platforms.

Example: Using CPackConfig.cmake for OS-Specific Configuration


You can also conditionally include OS-specific CPack configurations within the
CPackConfig.cmake file. For instance, you can create different configuration
files for each operating system and include the relevant one based on the system
detected by CMake.

# In CMakeLists.txt
if(WIN32)
set(CPACK_CONFIG_FILE "CPackConfigWindows.cmake")
elseif(APPLE)
set(CPACK_CONFIG_FILE "CPackConfigMac.cmake")
elseif(UNIX)
set(CPACK_CONFIG_FILE "CPackConfigLinux.cmake")
378

endif()

# Include the platform-specific config file


include(${CPACK_CONFIG_FILE})

In each of the configuration files (CPackConfigWindows.cmake,


CPackConfigMac.cmake, etc.), you can specify platform-specific settings, such
as package names, version numbers, dependencies, and installation directories.

3. Handling Dependencies for Different OSes


Dependencies often vary across platforms. For example, a library that is available on
Linux may not be present on Windows, or the installation process may differ. CPack
allows you to define dependencies conditionally for each operating system.

Example: Defining Dependencies for Different OSes


In your CMakeLists.txt, you can conditionally set dependencies based on the
platform:

if(WIN32)
set(CPACK_PACKAGE_DEPENDS "msvcrt")
elseif(APPLE)
set(CPACK_PACKAGE_DEPENDS "libc++")
elseif(UNIX)
set(CPACK_PACKAGE_DEPENDS "libc6, libstdc++6")
endif()

This allows CPack to package the correct dependencies for each platform, ensuring
that users have everything they need when installing your software.

4. Using Different Package Formats for Different OSes


379

Each operating system has preferred package formats, so it’s essential to configure
CPack to generate appropriate packages for each platform. CPack supports several
popular formats for each operating system, and you can specify multiple formats in
the CPACK GENERATOR variable.
• Windows: .msi, .zip
• Linux: .deb, .rpm, .tar.gz
• macOS: .dmg, .pkg, .tar.gz

Example: Specifying Different Formats for Each Platform


You can specify different package formats for each platform using the
CPACK GENERATOR variable. This ensures that the right package format is created
depending on the operating system.

if(WIN32)
set(CPACK_GENERATOR "MSI;ZIP")
elseif(APPLE)
set(CPACK_GENERATOR "TGZ;DMG")
elseif(UNIX)
set(CPACK_GENERATOR "DEB;RPM;TGZ")
endif()

This approach allows you to generate different package formats for different
platforms while still using the same CMake project.
5. Cross-Compiling and Building for Multiple OSes
For some cases, you may want to cross-compile your project for different platforms.
CMake and CPack provide support for cross-compiling, allowing you to build and
package software for one platform from another. This can be useful when
developing for multiple operating systems but only having access to a single
development machine.
380

To set up cross-compiling, you’ll need to configure CMake with appropriate


toolchains for the target platforms. You can use CMake's
CMAKE TOOLCHAIN FILE to specify a toolchain for cross-compilation. Once
you’ve set up the toolchain and cross-compiling environment, you can invoke CPack
to create packages for different platforms.

6. Testing Cross-Platform Packages


Once you’ve set up your cross-platform packaging, it’s essential to test the generated
packages on each supported operating system. CPack generates package formats that
can be installed on each platform, but to ensure the best user experience, it’s
important to validate the installation process. This involves testing the installation on
different operating systems, ensuring that the package installs correctly, and that
dependencies are handled as expected.
Testing tools and CI/CD (Continuous Integration/Continuous Deployment) pipelines
like Jenkins, GitLab CI, or GitHub Actions can automate the testing of
cross-platform builds. These tools can help ensure that the generated packages work
as intended on each supported platform.

10.4.3 Final Thoughts on Supporting Multiple Operating Systems

Supporting multiple operating systems for your CMake-based project is a powerful feature
that allows you to distribute your software to a wider audience. CPack simplifies this task
by providing robust configuration options for various platforms. By using conditional
logic, platform-specific configuration files, handling dependencies per OS, and generating
appropriate package formats, you can ensure that your project is well-packaged for a
variety of platforms.

To achieve full cross-platform compatibility, careful planning and testing are key. By
following the best practices discussed in this section, you can create a seamless packaging
381

process that works across all major operating systems, allowing your users to install and
use your software effortlessly, no matter their environment.
382

10.5 Distributing Projects to End Users

Once your project is packaged into a distributable format, the next step is ensuring that it
reaches the end users. Distribution is a critical phase in the lifecycle of a software project,
as it involves making the packaged software available to those who need it. CPack
simplifies this process by generating a variety of package formats that cater to different
operating systems, and it also integrates well with different distribution channels. In this
section, we will explore the key considerations and best practices for distributing your
packaged projects to end users.

10.5.1 Understanding Distribution Channels

The way you distribute your software largely depends on the target audience, the
platforms you are targeting, and the distribution channels available. There are several
common methods for distributing packaged projects:

1. Manual Distribution: This involves uploading the installation packages to a


website or sharing them via other means, such as email, FTP, or USB drives. This
method is straightforward but requires manual handling and is more suited to
smaller-scale projects or closed distributions.

2. Software Repositories: For more widespread distribution, especially on


Linux-based systems, software repositories (e.g., APT for Debian/Ubuntu,
YUM/DNF for RedHat/Fedora) provide an automated way to distribute software.
This method allows users to install packages with a simple command, and it supports
automatic updates.

3. App Stores and Package Managers: On macOS, Windows, and Linux, app stores
(e.g., the Mac App Store, Microsoft Store, Snap Store) provide a more structured and
383

user-friendly way to distribute software. These platforms often have requirements,


such as digital signing, to ensure security.
4. CI/CD Pipelines and Auto-Deployment: For projects that are updated frequently or
require constant delivery to users, CI/CD pipelines (e.g., GitHub Actions, GitLab CI)
automate the process of building, packaging, and distributing software. This method
is ideal for continuous delivery, making it easier to get the latest version of your
project into users' hands automatically.

Let’s break down each of these distribution methods in more detail.

1. Manual Distribution
For smaller-scale or internal projects, manual distribution may be the easiest and
most cost-effective method. After packaging the project using CPack, you can
upload the installation files to a file server, website, or cloud storage service like
Google Drive, Dropbox, or Amazon S3. Users can then download the relevant
installation files based on their operating system and manually install the software.

Steps for Manual Distribution:

1. Package the Project: Use CPack to generate the installation files in the desired
formats (e.g., .zip, .msi, .deb, .rpm, .tar.gz).
2. Upload the Packages: Upload the generated files to your distribution platform.
This could be a personal website, an FTP server, or cloud storage.
3. Share the Download Links: Share the direct links to the package files with
your users via email, on your website, or through a public-facing repository.
4. Provide Installation Instructions: Depending on the format, provide
installation instructions for the users (e.g., running an .msi installer on
Windows, using dpkg or apt for Debian-based Linux distributions, or
extracting .tar.gz archives for macOS and Linux).
384

While manual distribution is simple, it can be labor-intensive for larger audiences,


especially as software updates are rolled out. It also lacks automation, so users won’t
receive automatic updates.
2. Using Software Repositories
For open-source projects or software intended for use by a large audience,
distributing via package managers like APT, YUM, or DNF is an efficient way to
reach users. These package managers are built into most Linux distributions, and
once a package is available in a repository, users can install or update the software
with a single command.

Steps for Repository-Based Distribution:

1. Package the Project: Use CPack to generate platform-specific package formats,


such as .deb or .rpm.
2. Submit to a Repository
: For Linux distributions, you will need to either submit your package to a
well-known public repository or set up your own. For example:
• Debian/Ubuntu: Submit your .deb package to the official Debian/Ubuntu
repositories or to a Personal Package Archive (PPA).
• Red Hat/Fedora: Submit your .rpm package to the Fedora package
repository.
• Snap Store: You can also distribute your project via Snapcraft, which
allows you to create Snap packages that work across all major Linux
distributions.
3. Automate Updates: Once your package is in a repository, the software manager
will handle installation and updates for users. Users can install your software by
running a simple command like sudo apt install my-software or
sudo dnf install my-software.
385

Distributing via repositories is a highly automated process and is ideal for ensuring
your users always have access to the latest versions. However, it often requires
meeting specific packaging standards and may involve submission processes that
take time to complete.

3. Distributing via App Stores and Package Managers


For platforms like macOS and Windows, app stores provide an excellent distribution
mechanism. The Mac App Store and Microsoft Store are popular choices for
reaching end users. On Linux, other package managers such as Snap and Flatpak
provide similar functionality.

Steps for App Store and Package Manager Distribution:

1. Package the Project: Use CPack to generate platform-specific installer files


(e.g., .dmg for macOS, .msi for Windows, .tar.gz for Linux).
2. Register as a Developer: Sign up as a developer with the relevant app store
(e.g., Mac App Store, Microsoft Store). For Linux, create an account with the
Snap Store or Flatpak.
3. Package and Submit: Ensure that your package meets the app store or package
manager's guidelines, which may include requirements like digital signing and
adhering to packaging formats. Submit your package to the platform for review
and distribution.
4. End Users Install: Once accepted, users can install your software directly from
the app store or package manager, simplifying the process for both the
developer and the user.

Example: Distributing via Mac App Store:

1. Prepare the .dmg File: Use CPack to generate a .dmg file for macOS.
386

2. Sign the Package: Code-signing is required by the Mac App Store for security
purposes. You’ll need a valid Apple Developer certificate to sign the .dmg file.
3. Submit to the Mac App Store: Upload the signed .dmg file to the App Store
for review and release.

Distributing through app stores provides high visibility and ensures that your
software follows platform-specific guidelines. However, the submission process can
be rigorous, and your software must meet stringent standards.

4. Automating Distribution with CI/CD Pipelines


CI/CD pipelines allow for seamless, automated distribution of new versions of your
software. Tools like GitHub Actions, GitLab CI, and Jenkins can be used to
automatically build, package, and distribute your software each time you push a new
change to your repository. CI/CD tools integrate with CPack to build your project
and package it in multiple formats for different operating systems.

Steps for CI/CD Distribution:

1. Configure Your CI/CD Pipeline: Set up your CI/CD pipeline to run CMake
and CPack whenever code is committed to your repository.
2. Build and Package the Project: Each time a change is pushed, the pipeline
will automatically compile the project, run tests, and generate the appropriate
installation packages using CPack.
3. Distribute the Packages
: Depending on your setup, the pipeline can:
• Upload the packages to a server or cloud storage.
• Push the packages to software repositories (e.g., APT, YUM, Snap Store).
• Submit the packages to app stores for review.

Automating the distribution process ensures that your users always receive the latest
387

version without manual intervention. This method is particularly useful for projects
that have frequent updates.
5. Digital Signing and Security Considerations
When distributing software, especially in professional environments, security is an
important consideration. Digital signing of your packages is crucial for ensuring the
authenticity of the software and protecting users from malicious tampering. Many
operating systems and distribution platforms require software to be digitally signed.

Signing Packages:

• Windows: Sign .msi and .exe packages using tools like SignTool to
ensure that users trust the source of the software.
• macOS: Sign .dmg and .pkg files using your Apple Developer certificate.
• Linux: Sign .deb and .rpm packages using GPG or other signing methods.

By signing your packages, you provide users with confidence that the software they
are installing is genuine and has not been altered.

10.5.2 Final Thoughts on Distributing Projects

Distributing your software effectively is key to reaching your target audience and ensuring
smooth installations. By using the appropriate distribution methods—manual distribution,
software repositories, app stores, or CI/CD automation—you can make it easy for your
users to obtain and install your project. Additionally, security practices such as digital
signing are crucial to protect both the software and its users.
Whether you are distributing through a public repository, an app store, or via automated
pipelines, CPack and CMake provide the tools you need to streamline the distribution
process and ensure that your packaged project reaches its users efficiently and securely.
Chapter 11

CMake and CI/CD Integration

11.1 Using CMake with GitHub Actions

In this section, we will explore how to integrate CMake with GitHub Actions to automate
the process of building and testing C++ projects. GitHub Actions provides an easy-to-use
platform for Continuous Integration (CI) and Continuous Deployment (CD), which is
essential for modern software development, especially when collaborating in teams and
managing large codebases. By combining CMake's flexibility with GitHub Actions'
automation features, you can significantly improve your workflow, ensuring that your
code is always built and tested under different environments and conditions.

11.1.1 What is GitHub Actions?

*
GitHub Actions is an automation tool integrated into GitHub that allows you to define
workflows to build, test, and deploy your code. A workflow is made up of one or more

388
389

jobs, which can run concurrently or sequentially. Each job consists of a series of steps,
where each step performs a specific action, such as setting up dependencies, building the
project, running tests, or deploying the application. These workflows are defined in YAML
files, stored in the .github/workflows directory of your repository.

11.1.2 Why Use CMake with GitHub Actions?

CMake is a widely used build system for C++ projects that provides flexibility and
scalability. Integrating CMake with GitHub Actions can help automate the following
tasks:

• Automated Builds: Build your C++ projects on every push to your repository,
ensuring that the code is always in a buildable state.

• Cross-platform Testing: GitHub Actions allows you to run your tests across
different operating systems (Linux, macOS, and Windows) to ensure portability.

• Continuous Integration: Ensure that changes to your codebase are automatically


compiled and tested, preventing the introduction of build or runtime errors.

• Integration with External Services: You can easily connect to services like
coveralls, codecov, and others for code coverage reporting.

11.1.3 Setting Up GitHub Actions for CMake Projects

To get started, you need to create a GitHub Actions workflow configuration file for your
CMake project. This YAML file defines the steps for the entire build and test process.
Here’s a detailed guide on how to set up GitHub Actions for a CMake-based C++ project.

1. Creating the GitHub Actions Workflow File


390

First, you need to create a .github/workflows directory in your GitHub


repository if it doesn't already exist. Inside this directory, create a YAML file for
your workflow, such as ci.yml or build.yml.
Here’s a simple example of how the structure of your workflow might look:

name: CMake Build and Test

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest

steps:
# Step 1: Checkout code
- name: Checkout code
uses: actions/checkout@v2

# Step 2: Set up CMake


- name: Set up CMake
uses: actions/setup-cmake@v1
with:
cmake-version: '3.21.2'

# Step 3: Configure and build with CMake


- name: Configure and Build
391

run: |
mkdir build
cd build
cmake ..
cmake --build .

# Step 4: Run tests


- name: Run tests
run: |
cd build
ctest --output-on-failure

Explanation of Each Step:


1. name: This is the name of the workflow. In this case, we’ve called it ”CMake
Build and Test”.
2. on: This defines the triggers for when the workflow should run. In this example,
the workflow is triggered on any push or pull request to the main branch.
3. jobs: This section contains the steps that will be executed during the CI pipeline.
Each job runs on a different machine (or environment), and in this example, the
job runs on the latest version of Ubuntu (ubuntu-latest).
4. steps: The individual actions or steps that make up the job. This includes:
• Checkout code: The actions/checkout@v2 action checks out the
code from the repository, making it available for subsequent steps.
• Set up CMake: The actions/setup-cmake@v1 action installs the
specified version of CMake.
• Configure and Build: In this step, we create a build directory, configure
the project using CMake, and build it using the cmake --build .
command.
392

• Run tests: Finally, we run the tests using ctest to ensure everything is
working as expected.
2. Configuring CMake for GitHub Actions
In order to ensure that CMake works correctly on GitHub Actions, there are a few
important considerations:
• Build Directory: In CI/CD pipelines, it is standard practice to create a separate
build directory to keep the source tree clean. This is done with the command
mkdir build followed by cd build to change into the directory before
running CMake.
• CMake Version: You can specify the required version of CMake that is
compatible with your project. It is crucial to use the version of CMake that
works best for your project’s requirements.
• Caching Build Dependencies: To speed up the build process, you can use
caching mechanisms provided by GitHub Actions. This is useful for
dependencies or CMake configurations that don’t change frequently.
Here is an example of how to enable CMake cache:

- name: Cache CMake dependencies


uses: actions/cache@v2
with:
path: |
˜/.cache/CMake
build/
key: ${{ runner.os }}-cmake-${{
,→ hashFiles('**/CMakeLists.txt') }}
restore-keys: |
${{ runner.os }}-cmake-

This cache step ensures that the build dependencies and CMake configuration are
393

cached to speed up subsequent runs.

3. Running Tests and Reporting Results


In addition to building your project, you will likely want to run tests automatically as
part of the CI pipeline. To do this, you can use ctest, which is the CMake testing
tool.
You can enhance this section by adding extra tools or services to report test results.
For example, integrating with a code coverage service like Coveralls can help you
monitor how well your tests are covering your code.
Example step to upload test results to Codecov:

- name: Upload coverage to Codecov


uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}

11.1.4 Handling Multiple Operating Systems and Environments

One of the key benefits of using GitHub Actions with CMake is that it supports running
workflows on different operating systems and environments. GitHub Actions supports
ubuntu-latest, windows-latest, and macos-latest. You can run the same
workflow across all these environments to ensure cross-platform compatibility.

Here is an extended version of the workflow to run on multiple operating systems:

jobs:
build:
runs-on: ubuntu-latest
strategy:
394

matrix:
os: [ubuntu-latest, windows-latest, macos-latest]

steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up CMake
uses: actions/setup-cmake@v1
with:
cmake-version: '3.21.2'
- name: Configure and Build
run: |
mkdir build
cd build
cmake ..
cmake --build .
- name: Run tests
run: |
cd build
ctest --output-on-failure

This example uses a matrix strategy, which allows you to run the same steps on multiple
operating systems concurrently.

11.1.5 Advanced Topics

While the example workflow above covers basic CI/CD tasks, you can extend your
workflow with more advanced features:

• Deploying to different platforms: For example, using GitHub Actions to build and
deploy your C++ application to cloud platforms, such as AWS, Azure, or Google
395

Cloud.
• Custom Docker images: Running your builds inside Docker containers to ensure
consistency across different development environments.
• Notification and Slack Integration: You can add steps to notify your team of build
status via Slack or other communication platforms.

11.1.6 Conclusion

Integrating CMake with GitHub Actions allows for a fully automated and streamlined
development workflow. From building and testing to deploying, this integration improves
efficiency and reduces human error in the development lifecycle. By using GitHub
Actions’ powerful CI/CD features, along with the flexibility and portability of CMake,
you can ensure that your C++ projects are always ready for production, no matter what
changes are made to the codebase.
396

11.2 Integrating CMake with GitLab CI/CD

In this section, we’ll delve into how to integrate CMake with GitLab CI/CD to automate
your build, test, and deployment processes for C++ projects. GitLab CI/CD is a powerful,
flexible, and scalable continuous integration tool that provides a range of features,
including pipelines, runners, and automatic deployment. Combining GitLab’s CI/CD
pipeline with CMake's versatility as a build system will enable you to manage and
maintain high-quality code with ease.

What is GitLab CI/CD? GitLab CI/CD is a feature of GitLab that helps automate the
process of software development. It allows you to automatically test, build, and deploy
your projects using pipelines and jobs. A GitLab CI/CD pipeline consists of stages, and
each stage has one or more jobs. Jobs can run on GitLab Runners, which are the machines
that execute the CI/CD jobs defined in the .gitlab-ci.yml file.

Why Use CMake with GitLab CI/CD? CMake provides a standardized and flexible
method for managing build configurations in C++ projects. By integrating CMake with
GitLab CI/CD, you gain the following benefits:

• Automated Builds: Automatically build and test your project with every code
change or merge request.

• Cross-platform Support: GitLab CI/CD can run on various operating systems


(Linux, macOS, and Windows), making it easy to build and test CMake-based
projects across multiple platforms.

• Efficient Collaboration: By automating builds and tests, GitLab CI/CD ensures that
your project remains in a consistent and functional state for all contributors.
397

• Scalability: GitLab CI/CD is suitable for both small and large teams. As your
project grows, you can scale your CI/CD pipeline to support more complex
workflows.

Setting Up GitLab CI/CD for CMake Projects To use GitLab CI/CD with CMake,
you need to create a .gitlab-ci.yml file, which defines your CI pipeline. This
YAML file specifies the jobs, stages, and configuration for the build and test process.

1. Creating the GitLab CI/CD Configuration File


The first step in setting up GitLab CI/CD for your CMake project is to create the
.gitlab-ci.yml file at the root of your repository. This file describes the CI/CD
pipeline, including how the build and test process should be executed.
Here is a basic example of a .gitlab-ci.yml file for a CMake project:

stages:
- build
- test

# Define the build job


build:
stage: build
image: "ubuntu:20.04"
before_script:
- apt-get update && apt-get install -y build-essential cmake
script:
- mkdir build
- cd build
- cmake ..
- cmake --build .
artifacts:
398

paths:
- build/

# Define the test job


test:
stage: test
image: "ubuntu:20.04"
before_script:
- apt-get update && apt-get install -y build-essential cmake
script:
- cd build
- ctest --output-on-failure

Explanation of Each Section:


1. stages: This section defines the stages of the pipeline. In this case, there are two
stages: build and test. Each stage contains jobs that are executed
sequentially by default.
2. build:
• stage: Specifies that this job is part of the build stage.
• image: Specifies the Docker image used for the job. In this example, we are
using ubuntu:20.04, a common Ubuntu-based image.
• before script: This section runs commands before the main job script.
Here, we install the necessary dependencies (build-essential and
cmake).
• script: The core part of the job. It creates a build directory, configures
the project with CMake, and builds it using cmake --build ..
• artifacts: The artifacts section defines which files or directories should be
saved and passed to subsequent jobs in the pipeline. In this case, we’re
399

saving the entire build directory.


3. test:
• stage: Specifies that this job is part of the test stage.
• before script: Similarly, we install the necessary dependencies for testing,
such as build-essential and cmake.
• script: The script runs the tests using ctest
--output-on-failure, which will execute any tests defined in the
project and provide output on failure.
2. Configuring CMake for GitLab CI/CD
When integrating CMake with GitLab CI/CD, it’s essential to ensure that CMake is
properly configured and that the pipeline runs efficiently. Here are a few things to
keep in mind:
• Build Directory: Similar to best practices in CMake, create a separate build
directory to keep the source directory clean. In the above example, this is done
with mkdir build.
• Caching Dependencies: GitLab CI/CD supports caching, which helps speed up
the build process by reusing previously installed dependencies, CMake
configurations, or build files that do not change frequently. This can reduce the
time needed for successive builds.
Example of using cache:

cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- build/
- .cache/

This configuration caches the build/ and .cache/ directories, speeding up


400

subsequent builds by restoring these files from the cache.


• Parallel Execution: GitLab CI/CD allows jobs to run in parallel. For instance,
if your project has different components or modules, you can split the pipeline
into multiple jobs and run them concurrently. This can be particularly useful if
you’re working on large projects where building all components together can
take a significant amount of time.
Example of parallel jobs:

build_module_a:
stage: build
script:
- cmake -S module_a -B build/module_a
- cmake --build build/module_a

build_module_b:
stage: build
script:
- cmake -S module_b -B build/module_b
- cmake --build build/module_b

This configuration builds two separate modules (module a and module b) in


parallel, reducing the overall build time.

3. Running Tests and Reporting Results


In the previous example, we used ctest to run tests in the test job. You can
configure additional settings for reporting test results or collecting code coverage
data.

• Uploading Test Results: You can upload test results from your ctest run as
GitLab artifacts. This allows you to view the test results directly within GitLab.
Example of saving and uploading test results:
401

test:
stage: test
script:
- cd build
- ctest --output-on-failure
artifacts:
reports:
junit: build/test-results.xml

• Code Coverage: To ensure your tests cover enough of the code, you can use
tools like gcov (for GCC) or lcov to generate code coverage reports. You can
then upload these reports to GitLab to visualize the code coverage over time.
Example of generating and uploading code coverage:

test:
stage: test
script:
- cd build
- ctest --output-on-failure
- lcov --capture --directory . --output-file coverage.info
- lcov --remove coverage.info '/usr/*' --output-file
,→ coverage.info
- genhtml coverage.info --output-directory out
artifacts:
paths:
- out/

4. Handling Multiple Environments


Just like GitHub Actions, GitLab CI/CD allows you to define different environments
for building and testing your project. You can define jobs for various platforms
(Linux, macOS, Windows) or for specific configurations (debug, release).
402

For example, you can create separate jobs for different platforms using Docker
images or GitLab runners:

build_linux:
stage: build
image: "ubuntu:20.04"
script:
- mkdir build
- cd build
- cmake -DCMAKE_BUILD_TYPE=Release ..
- cmake --build .

build_windows:
stage: build
image: "mcr.microsoft.com/windows/servercore:ltsc2019"
script:
- mkdir build
- cd build
- cmake -DCMAKE_BUILD_TYPE=Release ..
- cmake --build .

This setup defines separate jobs for building the project on Linux and Windows.

11.2.0.1 Conclusion

Integrating CMake with GitLab CI/CD automates and streamlines your development
pipeline. With GitLab’s powerful CI/CD tools, you can easily build and test your C++
projects on multiple platforms, ensure consistency, and provide valuable feedback to your
team in real-time. By leveraging caching, parallel execution, and integration with testing
tools, you can speed up the build process, improve collaboration, and maintain the
integrity of your project.
403

11.3 Working with Jenkins and CMake

In this section, we will explore how to integrate CMake with Jenkins, one of the most
widely used open-source automation servers, to implement a robust CI/CD pipeline for
C++ projects. Jenkins allows for continuous integration and continuous delivery, enabling
you to automatically build, test, and deploy your software. Combining CMake's flexibility
and Jenkins' automation capabilities provides a powerful solution for managing and
maintaining C++ projects.

11.3.1 What is Jenkins?

Jenkins is an open-source automation server used to automate various stages of the


software development lifecycle, including building, testing, and deploying code. Jenkins
supports a vast array of plugins, which allows integration with various build systems,
source control tools, and deployment systems.
Jenkins uses a concept of jobs to execute tasks, and each job can be customized to
perform different tasks like checking out code from version control systems, running unit
tests, deploying software, and more. Jenkins jobs can be organized into pipelines, which
are defined using either the graphical interface or through a configuration file known as a
Jenkinsfile.

11.3.2 Why Use Jenkins with CMake?

Integrating CMake with Jenkins provides several advantages for C++ projects:

• Automated Builds: Jenkins can automatically trigger builds when code changes are
pushed to a version control system like Git. This ensures that your project is always
up to date and functioning correctly.
404

• Cross-platform Builds: Jenkins allows you to configure jobs that run on different
operating systems (Linux, macOS, and Windows). By using CMake as your build
system, you can ensure that your code is portable and consistently built across all
platforms.
• Parallel Execution: Jenkins can run jobs concurrently, which speeds up build times,
especially for large projects with many components.
• Integration with Other Tools: Jenkins supports integration with other tools, such as
code quality analyzers, test frameworks, and deployment systems. You can easily
integrate tools like Clang, GCC, MSVC, or other compilers, as well as testing
frameworks like Google Test or Catch2.
• Scalability: Jenkins can be set up on a single machine or across multiple machines
(also known as Jenkins agents or slaves) to distribute build and test tasks, which is
particularly useful for larger teams or projects.

11.3.3 Setting Up Jenkins for CMake Projects

To get started with Jenkins and CMake, you need to configure Jenkins to use CMake as
the build system. This involves setting up Jenkins on a server, creating a Jenkins job or
pipeline for your CMake project, and ensuring that the required tools and dependencies
are installed on your Jenkins environment.

1. Installing Jenkins and Setting Up a Job


Before you can configure Jenkins to work with CMake, you need to set up Jenkins
on your machine. Follow these steps to get started:

1. Install Jenkins: Download and install Jenkins from the official website
(https://www.jenkins.io/download/). Jenkins can be installed on
a variety of platforms, including Linux, macOS, and Windows.
405

2. Install Required Plugins:


• CMake Plugin: While Jenkins does not directly include CMake support,
you can use the CMake plugin or set up your job manually using a shell
script or batch commands. The CMake plugin integrates CMake into
Jenkins and allows you to configure CMake builds with ease.
• Git Plugin: If your project is hosted on Git, the Git plugin allows Jenkins to
pull your project code automatically.
3. Create a Jenkins Job: Once Jenkins is set up, create a new Freestyle Project
or Pipeline for your CMake-based project.
• Freestyle Project: A simple job that allows you to configure various build
steps through a web interface.
• Pipeline Project: This is more flexible, allowing you to define the entire
CI/CD process in a Jenkinsfile.
2. Configuring a Jenkins Freestyle Project for CMake
To configure Jenkins to build a CMake project with a Freestyle Project, follow these
steps:
1. Create a New Job:
• Go to the Jenkins dashboard and click on New Item.
• Choose Freestyle Project, and name it (e.g., CMake Build).
2. Source Code Management:
• In the Source Code Management section, select Git (or another version
control system) and configure it with the repository URL for your C++
project.
• If necessary, configure the credentials to allow Jenkins to access the
repository.
3. Build Environment:
406

• Make sure that your Jenkins server has the necessary build tools installed,
such as CMake, a C++ compiler (e.g., GCC, Clang, or MSVC), and any
required libraries.
• You can install CMake on your Jenkins server by using the package
manager for your operating system (e.g., apt on Ubuntu, brew on
macOS).
4. Build Steps:
• Add a build step to run a shell command (on Linux/macOS) or a batch
command (on Windows). The command will invoke CMake to configure
and build your project.
• Here’s an example shell script for Linux/macOS:

mkdir build
cd build
cmake ..
cmake --build .

• If you’re using Windows, you can use the following batch script:

mkdir build
cd build
cmake ..
cmake --build .

5. Post-build Actions:
• You can define post-build actions, such as publishing test results or
deploying the build artifacts.
• If you want to run tests, you can add a post-build step to execute the
407

ctest

command to run the unit tests:

ctest --output-on-failure

6. Save and Run the Job:


• Save the job and click Build Now to trigger the first build. Jenkins will pull
the latest code, configure the project with CMake, build it, and execute the
tests.

3. Configuring a Jenkins Pipeline for CMake


A Jenkins Pipeline is more flexible and powerful than a Freestyle project. A
pipeline allows you to define the entire build, test, and deployment process using a
Jenkinsfile, which is stored in your project’s repository.
To set up a Jenkins Pipeline for a CMake-based project, follow these steps:

1. Create a New Pipeline:


• Go to the Jenkins dashboard and click on New Item.
• Choose Pipeline, and name it (e.g., CMake Pipeline).
2. Configure the Pipeline:
• In the pipeline configuration, define the Jenkinsfile path in the
Pipeline Script section. The script defines the various stages of the CI/CD
pipeline.
3. Jenkinsfile Example: Below is an example of a simple Jenkinsfile that
automates the process of building and testing a CMake-based C++ project:
408

pipeline {
agent any

stages {
stage('Checkout') {
steps {
// Pull the latest code from the Git
,→ repository
git 'https://your-git-repository-url.git'
}
}

stage('Build') {
steps {
// Create build directory and configure with
,→ CMake
sh '''
mkdir build
cd build
cmake ..
cmake --build .
'''
}
}

stage('Test') {
steps {
// Run unit tests with CTest
sh '''
cd build
ctest --output-on-failure
'''
409

}
}
}

post {
success {
// Actions to take if the build succeeds, such as
,→ sending a notification
echo 'Build and tests completed successfully.'
}
failure {
// Actions to take if the build fails
echo 'Build or tests failed.'
}
}
}

In this Jenkinsfile, we define three stages:


• Checkout: This stage checks out the latest code from the repository.
• Build: This stage creates the build directory, runs CMake to configure the
project, and builds it using cmake --build ..
• Test: This stage runs tests using ctest.
The post block defines actions that happen after the pipeline finishes, such as
sending notifications on success or failure.
4. Save and Run the Pipeline:
• Save the pipeline and click Build Now to trigger the build. Jenkins will
automatically run the pipeline, pulling the latest code, building the project,
and running the tests.

4. Running Builds on Multiple Platforms


410

Jenkins can also run jobs on multiple platforms using Jenkins agents or Docker
containers. This is especially useful if your project needs to be built on different
operating systems (Linux, macOS, and Windows).

• Jenkins Agents: You can set up agents (also known as slaves) that run on
different machines with different operating systems. This allows Jenkins to
execute builds on a variety of platforms.
• Docker Containers: You can use Docker to run builds in isolated environments
with specific dependencies and versions of CMake and the C++ compiler.
Jenkins can spin up containers automatically as part of the build process.

For example, a Jenkins job might look like this to run the build process inside a
Docker container:

pipeline {
agent {
docker { image 'ubuntu:20.04' }
}
stages {
stage('Build') {
steps {
sh 'mkdir build && cd build && cmake .. && cmake
,→ --build .'
}
}
}
}

This setup allows you to build your CMake-based project inside an Ubuntu Docker
container, ensuring consistency across all builds.
411

11.3.4 Conclusion

Integrating CMake with Jenkins creates a powerful CI/CD pipeline that automates the
building, testing, and deployment of C++ projects. Jenkins provides flexibility, scalability,
and a wide range of plugins, while CMake ensures a standardized and portable build
system. By setting up Jenkins to work with CMake, you can ensure that your project
remains in a consistent and functional state, allowing you to focus more on coding and
less on manual build processes.
412

11.4 Automating Builds on Cloud Platforms

In this section, we’ll explore how to integrate CMake with cloud platforms for automating
builds and continuous integration (CI). Cloud platforms such as AWS (Amazon Web
Services), Google Cloud Platform (GCP), and Microsoft Azure provide scalable,
cost-effective environments to run builds, tests, and deployments. By utilizing these
platforms, developers can offload the heavy lifting of building and testing, gain access to
scalable resources, and integrate CMake with various cloud-based CI/CD services.

The shift to cloud-based build environments is becoming increasingly popular for teams of
all sizes due to the flexibility, scalability, and ease of management offered by cloud
platforms. By automating CMake-based builds on the cloud, you can ensure that your C++
projects are always up-to-date, tested, and ready for deployment.

11.4.1 Why Automate Builds on Cloud Platforms?

There are several compelling reasons to automate your builds on cloud platforms:

• Scalability: Cloud platforms allow you to scale up or down depending on the build
demand. You can handle increased build load during peak times without the need for
physical infrastructure.

• Cost Efficiency: With cloud services, you only pay for the resources you use. This
means you can run builds in parallel or use more powerful instances for short periods
without maintaining an expensive on-premise infrastructure.

• Access to Modern Infrastructure: Cloud platforms provide access to modern


infrastructure and services such as GPUs, high-performance computing instances,
and load balancing, which may be difficult or expensive to set up on-premise.
413

• Global Availability: Cloud platforms offer data centers worldwide, allowing you to
run builds on different operating systems and regions to ensure your CMake projects
are cross-platform and globally distributed.

• Flexibility: Cloud platforms are highly customizable. You can define the resources
you need, automate provisioning with Infrastructure as Code (IaC), and integrate
various services and tools to enhance your CI/CD pipeline.

In the following sections, we'll explore how to configure cloud-based CI systems such as
AWS CodeBuild, Google Cloud Build, and Azure Pipelines to automate builds for
CMake-based C++ projects.

1. Automating Builds with AWS CodeBuild


AWS CodeBuild is a fully managed continuous integration service that automates
building and testing code. It eliminates the need to provision your own build servers,
allowing you to focus on development. CodeBuild integrates seamlessly with other
AWS services and supports building CMake-based C++ projects efficiently.

Setting Up AWS CodeBuild for CMake

1. Create an AWS CodeBuild Project:


• Log into your AWS Management Console.
• Navigate to the CodeBuild service and click on Create build project.
• Define the project name and description.
2. Source Configuration:
• In the Source section, choose the repository type (e.g., GitHub, Bitbucket,
AWS CodeCommit).
• For GitHub, you’ll need to link your GitHub account and select the
repository that contains your CMake project.
414

3. Build Environment:
• In the Environment section, choose a build image that supports your
CMake project. You can either use AWS-provided images or create a
custom Docker image.
• For a CMake-based C++ project, you can use an Amazon Linux or Ubuntu
image and install CMake, GCC, or other necessary compilers.
4. Build Specification:
• AWS CodeBuild uses a buildspec.yml file to define the build process.
Create a buildspec.yml file in the root of your repository. The
buildspec.yml file defines the steps involved in compiling your
CMake project, running tests, and storing artifacts.
Example of a buildspec.yml for a CMake-based project:

version: 0.2
phases:
install:
runtime-versions:
gcc: 10
cmake: 3.x
commands:
- echo Installing dependencies...
- yum install -y gcc-c++ make
pre_build:
commands:
- echo Preparing the build environment...
- mkdir build
- cd build
- cmake ..
build:
commands:
415

- echo Building the project...


- cmake --build .
post_build:
commands:
- echo Running tests...
- ctest --output-on-failure
artifacts:
files:
- build/**

The buildspec.yml file includes several phases:


• Install Phase: This installs the necessary dependencies such as GCC, make,
and CMake.
• Pre-Build Phase: In this phase, the build directory is created, and CMake
is invoked to configure the project.
• Build Phase: The project is built using cmake --build ..
• Post-Build Phase: Any tests are executed using ctest.
• Artifacts: The build output and any other files (e.g., test results) are saved
as artifacts for later use or deployment.
5. Triggering Builds:
• You can trigger builds manually or automatically in response to changes in
the repository (via webhook integration with GitHub or AWS
CodeCommit).
• You can also schedule builds at regular intervals if required.
6. Monitoring and Logs:
• CodeBuild integrates with AWS CloudWatch, where you can view logs
from each build, allowing you to easily track errors and debug issues.
416

By using AWS CodeBuild, your CMake-based project will be built and tested in the
cloud with minimal setup. CodeBuild will handle scaling the build process
automatically based on the workload.
2. Automating Builds with Google Cloud Build
Google Cloud Build is a service that automates the building of code across multiple
platforms. It integrates well with Google Cloud’s ecosystem and can be used to build
CMake-based projects for C++ applications.

Setting Up Google Cloud Build for CMake

1. Create a Google Cloud Project:


• Start by creating a Google Cloud Project through the Google Cloud
Console.
• Enable the Cloud Build API for your project.
2. Source Configuration:
• In Cloud Build, configure your source code repository (e.g., GitHub,
Bitbucket, or Google Cloud Source Repositories).
• You will need to grant Google Cloud Build access to your repository.
3. Create a Cloud Build Configuration File:
• Google Cloud Build uses a cloudbuild.yaml file to define the build
steps. This file should be placed in the root of your repository.
Example of a cloudbuild.yaml for a CMake-based project:

steps:
- name: 'gcr.io/cloud-builders/cmake'
id: 'Install dependencies'
args: ['--version']
- name: 'gcr.io/cloud-builders/cmake'
417

id: 'Configure'
args:
- '-Bbuild'
- '-H.'
- name: 'gcr.io/cloud-builders/cmake'
id: 'Build'
args:
- '--build'
- 'build/'
- name: 'gcr.io/cloud-builders/ctest'
id: 'Test'
args: ['--output-on-failure']
artifacts:
objects:
location: 'gs://YOUR_BUCKET_NAME/artifacts/'
paths:
- 'build/**'

In this cloudbuild.yaml, each step specifies a command that will be run


in sequence:
• Install Dependencies: This step checks the version of CMake.
• Configure: The project is configured using cmake.
• Build: The project is built using cmake --build.
• Test: Unit tests are executed using ctest.
The artifacts section specifies where the build artifacts (such as compiled
binaries and test results) should be stored (e.g., in a Google Cloud Storage
bucket).
4. Triggering Builds:
• You can trigger builds manually or automatically when changes are pushed
to your source repository. Cloud Build integrates with GitHub, Bitbucket,
418

or Google Cloud Repositories, and you can set up webhooks to trigger


builds upon code changes.
5. Monitoring and Logs:
• Cloud Build logs are available through Google Cloud Console, and you can
also export logs to Cloud Logging for further analysis and alerting.
By using Google Cloud Build, you can automate the entire process of building,
testing, and deploying your CMake-based C++ projects while benefiting from the
scalability and speed of Google Cloud infrastructure.
3. Automating Builds with Azure Pipelines
Azure Pipelines is a cloud-based service provided by Microsoft as part of the Azure
DevOps suite. It offers powerful CI/CD capabilities for building and deploying
applications. Azure Pipelines supports building CMake-based C++ projects,
integrating testing, and automating deployment to cloud or on-premise environments.

Setting Up Azure Pipelines for CMake


1. Create an Azure DevOps Organization and Project:
• Start by creating an account on Azure DevOps and setting up an
organization and project.
• Link your repository (from GitHub, Bitbucket, or Azure Repos) to Azure
DevOps.
2. Create a Pipeline:
• Navigate to the Pipelines section in Azure DevOps.
• Create a new pipeline and select the repository that contains your
CMake-based project.
3. Define the Pipeline in YAML:
• Define the pipeline configuration using the azure-pipelines.yml
file, which is stored in the root of your repository.
419

Example of an azure-pipelines.yml for a CMake-based project:

trigger:
branches:
include:
- main

pool:
vmImage: 'ubuntu-latest'

steps:
- task: UseCMake@1
inputs:
cmakeVersion: '3.x'

- script: |
mkdir build
cd build
cmake ..
cmake --build .
displayName: 'Build CMake Project'

- script: |
cd build
ctest --output-on-failure
displayName: 'Run Tests'

- publish: $(Build.ArtifactStagingDirectory)
artifact: drop

In this pipeline:
• trigger: Specifies that the pipeline is triggered on changes to the main
branch.
420

• pool: Defines the build agent (in this case, an Ubuntu-based agent).
• steps: Specifies the steps for building the CMake project and running tests.
4. Triggering Builds:
• Azure Pipelines will automatically trigger builds based on commits to the
repository. You can configure manual triggers or schedule builds at specific
times.
5. Monitoring and Logs:
• Logs for each build can be monitored in the Azure DevOps portal, and the
build summary provides detailed information about the success or failure of
each pipeline step.

11.4.2 Conclusion

Automating builds on cloud platforms such as AWS, Google Cloud, and Azure offers a
scalable, efficient, and cost-effective solution for managing CMake-based projects. By
integrating CMake with cloud-based CI/CD tools like AWS CodeBuild, Google Cloud
Build, and Azure Pipelines, you can take full advantage of the cloud's flexibility and
power to manage the build, test, and deployment workflows for your C++ projects.
Chapter 12

Optimizing Build Performance with


CMake

12.1 Using ccache and distcc to Speed Up Compilation

In this section, we will explore two powerful tools that can significantly speed up the
compilation process for C++ projects: ccache and distcc. These tools can be
integrated with CMake to optimize your build times, especially when working with large
codebases or frequent builds. By leveraging caching and distributed compilation, you can
greatly reduce the time it takes to build your project, making your development workflow
more efficient.

12.1.1 Introduction to Build Performance Optimization

One of the key challenges in large-scale software development is managing build


performance. As the size of your project grows, the time it takes to compile the code

421
422

increases, which can significantly slow down development and testing cycles. To address
this issue, there are several optimization strategies you can use to speed up the build
process, including parallelization, incremental builds, and utilizing specialized tools for
caching and distributed compilation.

1. ccache: A Build Caching Tool


ccache (short for compiler cache) is a tool that helps speed up the compilation
process by caching previously compiled object files. When you compile a file,
ccache stores the result in a cache directory, and in subsequent builds, it checks
whether the compilation can be reused based on the input parameters (e.g., source
file, compiler flags, includes). If a file has been compiled previously with the same
input and no changes have been made, ccache will reuse the cached object file,
reducing the compilation time.

How ccache WorksHow ccache Works


ccache works by intercepting calls to the compiler (such as gcc, clang, or
clang++) and caching the results. It generates a cache key based on the source
code, compiler flags, and other relevant factors. If the same source file is compiled
with the same flags again, ccache will return the previously cached object file
instead of recompiling the source file.
This cache key is a hash of:

• The source code file’s content.


• Compiler flags.
• Include paths.
• Preprocessor definitions.

If these components remain unchanged, ccache can reuse the cached result,
significantly speeding up the build.
423

Benefits of ccache

• Faster builds: After an initial compilation, ccache can dramatically reduce


subsequent build times by reusing cached object files.
• No code changes required: ccache can be integrated into existing build
systems (including CMake) without requiring any changes to the build scripts or
the source code itself.
• Incremental builds: ccache accelerates incremental builds where only a
subset of files have changed.
• Disk-based cache: ccache stores object files in a dedicated cache directory,
which can be shared across different machines to optimize build times across
teams or CI systems.

Setting Up ccache with CMake


To use ccache with CMake, you need to ensure that ccache is installed and
configured properly, and that CMake is instructed to use ccache when invoking the
compiler.

1. Install ccache:
• On Linux:

sudo apt install ccache # For Debian/Ubuntu


sudo yum install ccache # For CentOS/RHEL

• On macOS (using Homebrew):

brew install ccache

• On Windows, ccache can be installed via Cygwin or the Windows


Subsystem for Linux (WSL).
424

2. Configure CMake to Use ccache: You can configure CMake to use ccache
by prepending the compiler commands with ccache. This can be done by
modifying your CMakeLists.txt or by setting environment variables.
To enable ccache for your CMake project, set the CXX and CC environment
variables to use ccache:

export CC="ccache gcc"


export CXX="ccache g++"

Alternatively, you can modify your CMakeLists.txt file to set these


variables:

set(CMAKE_C_COMPILER "ccache gcc")


set(CMAKE_CXX_COMPILER "ccache g++")

This ensures that ccache is used whenever CMake calls the compiler.
3. Verify ccache is Working: To check if ccache is working, you can monitor
the cache statistics by running the following command:

ccache -s

This will display the cache hit/miss statistics, showing how much time is saved
by reusing cached compilations.
2. distcc: Distributed Compilation
distcc is another tool designed to speed up compilation by distributing the work
across multiple machines. It allows you to distribute the compilation of source files
to different machines in a network, significantly reducing the overall build time. The
basic idea behind distcc is to offload the compilation of individual source files to
remote machines, which can be done in parallel. This is particularly useful when
building large projects with many files that can be compiled independently.
425

How distcc WorksHow distcc Works


distcc works by setting up a client-server architecture. You install distcc on
both the local and remote machines. When a compilation request is made, distcc
decides whether the file should be compiled locally or offloaded to a remote machine.
It manages the process of distributing the source files and collecting the results.
1. Local machine: When a compilation request is made, distcc first checks
whether the requested file can be compiled locally or needs to be sent to a
remote machine.
2. Remote machine: If the file is sent to a remote machine, distcc compiles the
file on that machine and sends the resulting object file back to the local machine
for linking.
3. Parallelism: Multiple source files can be compiled in parallel on different
machines, further speeding up the build process.

Benefits of distcc
• Significant speed-up for large projects: distcc can reduce build times
dramatically by distributing the compilation tasks across multiple machines.
• Scalability: You can scale your build system by adding more machines to the
distributed compilation pool, increasing the total available processing power.
• Transparency: distcc integrates seamlessly with the existing build system
and compilers (such as gcc and clang), requiring no changes to the code or
the build scripts.

Setting Up distcc with CMake


To use distcc with CMake, you need to configure your environment to use
distcc as the compiler for your project.
1. Install distcc:
426

• On Linux:

sudo apt install distcc # For Debian/Ubuntu


sudo yum install distcc # For CentOS/RHEL

• On macOS:

brew install distcc

2. Set Up a Distributed Compilation Environment:


• Install distcc on all the machines you want to use for distributed
compilation. These can be physical machines, virtual machines, or
cloud-based servers.
• Start the

distcc

daemon on each remote machine:

distccd --daemon --allow <IP_ADDRESS>

• On the local machine, set the environment variable to point to the

distcc

compiler:

export CC="distcc gcc"


export CXX="distcc g++"
427

3. Configure CMake to Use distcc: Just as with ccache, you can configure
CMake to use distcc by setting the compiler to distcc gcc and distcc
g++.
You can add the following to your CMakeLists.txt:

set(CMAKE_C_COMPILER "distcc gcc")


set(CMAKE_CXX_COMPILER "distcc g++")

4. Monitor Remote Compilation: Once distcc is configured, it will


automatically distribute the compilation of source files to remote machines. You
can monitor the process using:

distcc -j <N> <source_file>

Where <N> is the number of parallel jobs you want to run, and
<source file> is the file being compiled.

12.1.2 Combining ccache and distcc

One of the most powerful ways to optimize your CMake-based build system is to combine
both ccache and distcc. ccache speeds up the build process by caching object files,
while distcc distributes the compilation workload across multiple machines. Together,
they can help you achieve faster builds by reducing both individual compilation time and
distributing the compilation across many machines.

To use both tools together, simply ensure that CMake is configured to use ccache and
distcc simultaneously. For example:
428

export CC="ccache distcc gcc"


export CXX="ccache distcc g++"

This setup allows you to benefit from both caching (for repeated builds) and distributed
compilation (for parallelizing the compilation process).

12.1.3 Conclusion

By integrating ccache and distcc into your CMake build system, you can
significantly speed up the compilation process for large C++ projects. ccache reduces
build times by caching object files and reusing them when possible, while distcc
distributes the compilation process across multiple machines to parallelize the work.
These tools can be easily integrated into existing CMake-based workflows with minimal
setup and can make a noticeable difference in build performance, especially for large,
complex projects.
429

12.2 Working with Unity Builds

In this section, we’ll explore Unity Builds, an optimization technique that can
significantly improve build performance for C++ projects. Unity Builds (also known as
Jack or Single Compilation Unit builds) offer a way to reduce compile time by grouping
multiple source files into a single file for compilation, thus reducing the overhead of
compiling individual files separately.

Unity Builds can be particularly useful in projects with a large number of source files,
where the cost of handling each file independently becomes a bottleneck in the build
process. While Unity Builds introduce some trade-offs (such as potential compilation
dependencies becoming more challenging to manage), they provide an effective way to
optimize CMake-based build systems and reduce compilation time.

12.2.1 What is a Unity Build?

In traditional C++ projects, each source file (.cpp) is compiled into an object file (.o or
.obj), and the compiler is invoked separately for each source file. This process can
become slow, particularly when dealing with projects that have a large number of source
files, where the overhead of invoking the compiler multiple times can add up quickly.

A Unity Build works by combining several source files into a single larger file before
invoking the compiler. By doing so, the compiler only needs to process one file, instead of
many smaller ones. This reduces the number of compiler invocations, minimizes the
overhead, and can lead to a significant reduction in build times. The Unity Build approach
works especially well for projects with many header files and dependencies, as it
minimizes the time spent on repeatedly processing them.

For example, instead of compiling each source file separately like this:
430

g++ -c file1.cpp
g++ -c file2.cpp
g++ -c file3.cpp

With Unity Builds, multiple source files are combined into a single large file (e.g.,
unity.cpp), and the compilation is done for this single file:

g++ -c unity.cpp

This can lead to a significant reduction in build time, particularly in large codebases.

12.2.2 How Unity Builds Work

The process behind Unity Builds is simple:

1. Combining Source Files: You create a single ”unity file” (e.g., unity.cpp) that
includes multiple source files.
2. Compilation: This single unity file is compiled as if it were a regular source file.
3. Linking: Once compiled, the resulting object files are linked together to form the
final executable.

The key to Unity Builds is the mechanism for combining source files. Typically, you use a
preprocessor to concatenate multiple .cpp files into a single unity file. This can be done
automatically as part of the build process using CMake.

12.2.3 Benefits of Unity Builds

1. Reduced Compiler Overhead:


431

• Compiling source files individually often involves significant overhead,


especially when header files are heavily used. By compiling multiple files
together, Unity Builds reduce the overhead of invoking the compiler multiple
times.
• The reduced number of compiler invocations directly contributes to faster
builds.
2. Improved Cache Utilization:
• Since Unity Builds result in fewer compilation units, the compiler can cache
intermediate results more effectively. The larger compilation units are also more
likely to fit into the CPU cache, which can improve performance even further.
3. Better Parallelization:
• While Unity Builds do not directly speed up the parallel compilation process,
reducing the number of units being compiled can allow parallel compilation to
become more effective when there are fewer but larger files to compile.
4. Faster Incremental Builds:
• Unity Builds optimize incremental builds. When only a small portion of the
code changes, rebuilding just one large file instead of many smaller ones can
save time.
5. Reduced Preprocessor Overhead:
• Since the compiler processes one unity file, there is a reduction in the overhead
caused by preprocessing header files multiple times.

12.2.4 Drawbacks of Unity Builds

Despite their performance benefits, Unity Builds come with several trade-offs that should
be considered before implementing them:
432

1. Longer Link Time:

• While compilation time is reduced, the link time might increase. With Unity
Builds, the linker may have to deal with larger object files that can take longer
to process, especially if the unity file is very large.

2. Increased Compilation Complexity:

• Combining many source files into a single unity file can introduce compilation
dependencies that make it harder to track which source files depend on which
headers.
• It also may introduce issues with naming collisions, as multiple source files are
being compiled together. This can lead to conflicts in symbol names, which
must be resolved using techniques like #pragma once or #ifdef guards in
headers.

3. Potential for Debugging Issues:

• Debugging can become more difficult when using Unity Builds, as it is harder
to isolate individual files and track errors to their source. Tools like gdb might
behave differently because of the combined nature of the source files.

4. Increased Memory Usage:

• With Unity Builds, since multiple files are included in a single compilation unit,
the memory usage during the compilation process can be higher, as the compiler
has to hold larger amounts of data in memory at once.

12.2.5 Setting Up Unity Builds in CMake

To use Unity Builds in CMake, you will need to modify your CMakeLists.txt file to
automatically generate the unity files and compile them instead of individual source files.
433

CMake does not have native support for Unity Builds, but it’s easy to set up using a
custom function or macro.
Here’s how you can configure Unity Builds in CMake:

• Step 1: Create a Macro for Unity Builds


You can define a macro in your CMakeLists.txt file to generate the unity files
and add them to the build process. Here is an example:

# Macro for Unity Builds


macro(create_unity_build SOURCES)
set(UNITY_FILE "${CMAKE_BINARY_DIR}/unity.cpp")
file(WRITE ${UNITY_FILE} "// Unity Build\n")

foreach(SOURCE ${SOURCES})
file(APPEND ${UNITY_FILE} "#include \"${SOURCE}\"\n")
endforeach()

list(APPEND CMAKE_SOURCE_FILES ${UNITY_FILE})


endmacro()

This macro will create a unity.cpp file in the build directory by concatenating
the specified source files. You can call this macro in your CMakeLists.txt to
group the .cpp files you want to include in the unity build.
• Step 2: Apply the Unity Build Macro
Now, use the create unity build() macro to add your source files into a
single unity file:

# List of source files


set(SOURCE_FILES
file1.cpp
434

file2.cpp
file3.cpp
)

# Apply Unity Build


create_unity_build(${SOURCE_FILES})

# Define the executable or library target


add_executable(my_project ${CMAKE_SOURCE_FILES})

In this example:
– We specify the list of source files we want to include in the Unity Build.
– The create unity build() macro combines them into a single
unity.cpp file, which will then be compiled by CMake as part of the build.
• Step 3: Optimize Unity Build Configuration
You can further optimize the Unity Build process by controlling the number of files
included in each unity file. For instance, grouping too many source files into a single
unity file can create excessively large object files that may lead to longer link times.
Instead, you can group files in smaller batches:

# Group files in smaller batches


set(SOURCE_FILES_1
file1.cpp
file2.cpp
)

set(SOURCE_FILES_2
file3.cpp
file4.cpp
)
435

create_unity_build(${SOURCE_FILES_1})
create_unity_build(${SOURCE_FILES_2})

This method ensures that the unity files do not become too large, reducing potential
issues with link time or memory usage.
• Step 4: Control Unity Build Compilation in Debug/Release Modes
In some cases, you might want to enable Unity Builds only in certain build
configurations (e.g., in the release build but not in the debug build). You can
conditionally apply the Unity Build based on the build type:

if(CMAKE_BUILD_TYPE STREQUAL "Release")


create_unity_build(${SOURCE_FILES})
else()
add_executable(my_project ${SOURCE_FILES})
endif()

This allows you to disable Unity Builds when debugging, where you may want to
retain faster link times and avoid issues with debugging large unity files.

12.2.6 Conclusion

Unity Builds are an effective optimization technique to speed up the compilation process
of large C++ projects. By grouping multiple source files into a single compilation unit,
Unity Builds reduce the overhead of invoking the compiler multiple times, leading to
faster builds. However, this technique comes with trade-offs, including the potential for
longer link times, increased memory usage, and debugging challenges.
Using CMake, Unity Builds can be easily implemented by generating unity files
dynamically and adjusting the number of files included in each unity build to optimize the
436

process. By carefully considering the size and complexity of the unity files and applying
conditional logic based on the build configuration, you can achieve faster build times
without compromising the quality of your project.
437

12.3 Reducing Link Time with Link Time Optimization


(LTO)

In this section, we will dive into Link Time Optimization (LTO), a powerful technique
that can reduce link times and improve the performance of your final executable. LTO
optimizes across translation units at the link stage, enabling the compiler to perform
interprocedural optimizations that are not possible during the individual compilation of
each source file. This section will explain how LTO works, how to enable it in CMake,
and the trade-offs involved in its use.

12.3.1 What is Link Time Optimization (LTO)?

Link Time Optimization (LTO) is an advanced optimization technique where the


compiler performs optimizations across all object files during the link stage, rather than
during the individual compilation of each source file. By enabling LTO, the linker can see
all the compiled code at once and perform optimizations that go beyond what’s possible
with traditional optimizations at the compilation stage.
In traditional compilation, the compiler optimizes each translation unit (i.e., each source
file) independently. This means that the compiler can only apply optimizations that are
within a single file. However, some optimizations are not possible without considering the
entire program. LTO allows the compiler to analyze and optimize across multiple
translation units (source files), which results in better performance, smaller executables,
and potentially faster link times.

How LTO Works


LTO works by generating an intermediate representation (IR) of the program during the
compilation stage. This IR is usually in the form of a LLVM bitcode file (for
438

LLVM-based compilers like Clang) or a GCC intermediate file. Rather than producing
regular object files with machine code, the compiler produces these intermediate
representations, which can be optimized further by the linker.
When the linker runs, it takes the intermediate representations from all the object files and
applies optimizations, including:

• Inlining functions across translation units.


• Removing duplicate code.
• Eliminating unused code.
• Reordering functions for better cache locality.
• Constant propagation and folding across translation units.

These optimizations can result in better performance in terms of both execution speed and
binary size. Additionally, LTO can reduce link time by removing redundant code and
making the linking process more efficient.

12.3.2 Benefits of Link Time Optimization

Enabling LTO can provide several benefits:

1. Improved Performance:

• Function Inlining: Functions that are small or frequently called across multiple
source files can be inlined, eliminating the overhead of function calls.
• Dead Code Elimination: LTO can remove code that is never used, even if it
resides in different translation units.
• Better Instruction Scheduling: The compiler can optimize instruction
scheduling across the entire program, leading to better performance.
439

2. Smaller Executable Size:

• LTO enables aggressive dead code elimination, which removes unused


functions and data, reducing the size of the final binary.
• By inlining small functions and removing redundancy, LTO can shrink the size
of the executable significantly.

3. Faster Link Times (in some cases):

• While LTO can introduce an overhead in the linking phase, it can also speed up
the link process by eliminating unnecessary symbols and resolving function
calls across translation units more efficiently.
• The overall link time may be reduced because the linker can eliminate
redundant symbols and references, leading to fewer symbols to resolve.

4. Interprocedural Optimizations:

• LTO allows optimizations that require visibility into the entire program, such as
interprocedural constant propagation and cross-file inlining. These
optimizations are impossible with traditional, per-file compilation strategies.

5. Reduced Redundancy:

• By optimizing across translation units, LTO helps eliminate duplicate code that
may have been compiled separately in different translation units, leading to a
leaner program.

12.3.3 Drawbacks and Considerations

While LTO provides numerous benefits, there are trade-offs that need to be carefully
considered before enabling it:

1. Increased Compilation and Link Times:


440

• Compilation Time: The initial compilation with LTO can be slower because
the compiler produces intermediate representations (IR) instead of regular
object files.
• Link Time: While LTO can reduce redundant code and improve the linking
process, the link phase itself can become slower, especially in large projects
with many object files, as the linker has to process the entire program to apply
LTO optimizations.
2. Memory Usage:
• LTO requires the linker to keep all the intermediate representations in memory.
In large projects, this can lead to high memory usage during the linking process.
• Some linkers (such as gold and lld) are optimized for LTO and can reduce
the memory footprint during linking, but memory usage remains a
consideration.
3. Incompatibility with Some Debugging Tools:
• In some cases, LTO can interfere with debugging tools, as the compiler may
optimize away certain symbols or alter the structure of the binary in ways that
make debugging more difficult.
• Debugging with LTO-enabled binaries may be more challenging, as some
optimizations (e.g., inlining) can obscure the relationship between source code
and the compiled binary.
4. Compiler and Linker Support:
• LTO requires both the compiler and linker to support it. For example, if you're
using GCC or Clang, you need to ensure that both the compiler and the linker
support LTO.
• Some build systems and platforms may not fully support LTO or may have
specific quirks that make enabling it more difficult.
441

5. Potential for Larger Object Files:

• The object files generated during the compilation stage may be larger than usual
because they contain intermediate representations, which can slow down the
overall build process if not handled efficiently.

12.3.4 Enabling LTO in CMake

To enable Link Time Optimization in CMake, you need to adjust your


CMakeLists.txt to add the appropriate flags for the compiler and linker. Both GCC
and Clang support LTO, and it can be enabled easily in your CMake project.

1. Step 1: Enabling LTO for GCC or Clang


In CMake, you can use the CMAKE CXX FLAGS or CMAKE C FLAGS to pass LTO
flags to the compiler and linker. To enable LTO for GCC or Clang, you need to add
the -flto flag.
Add the following to your CMakeLists.txt:

# Enable LTO for both C and C++ compilers


set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto")

# Enable LTO at the linker stage


set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -flto")

This will enable LTO for both the C and C++ compilers, as well as during the linking
stage.

2. Step 2: Enabling LTO Based on Build Type


442

You might want to enable LTO only for release builds and leave it disabled for debug
builds (where faster compile times are often preferred). To enable LTO conditionally,
you can modify your CMakeLists.txt like this:

if(CMAKE_BUILD_TYPE STREQUAL "Release")


set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -flto")
endif()

This ensures that LTO is only enabled when building the release version of your
project.
3. Step 3: Optimizing LTO with Linker Flags
When working with LTO, the linker needs to apply the optimizations to the entire
program, and this may require additional linker flags. If you are using GCC or Clang,
you can use the -fuse-ld=gold or -fuse-ld=lld linker flag to improve the
performance of the linking stage, as these linkers are optimized for LTO.

# Use the gold linker for faster LTO


set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}
,→ -fuse-ld=gold")

Alternatively, you can use the LLVM linker (lld), which is known to handle LTO
more efficiently in certain cases.

# Use the LLVM linker for faster LTO


set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}
,→ -fuse-ld=lld")

4. Step 4: Check for LTO Support


443

Not all compilers and platforms support LTO, so it’s important to check whether the
compiler supports LTO before enabling it. You can use CMake’s try compile()
function to check if LTO is supported on the current platform:

include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-flto" HAS_LTO)
if(HAS_LTO)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -flto")
else()
message(WARNING "LTO is not supported on this compiler")
endif()

This code snippet ensures that LTO is enabled only if the compiler supports it.

12.3.5 Conclusion

Link Time Optimization (LTO) is a powerful technique that can significantly reduce the
size of your executable and improve runtime performance by enabling optimizations that
span multiple translation units. While it introduces some trade-offs in terms of increased
memory usage and potentially slower link times, the performance benefits can be
substantial for large, complex C++ projects.
By enabling LTO in your CMake build system, you can easily leverage these
optimizations with minimal configuration. As with any optimization technique, it is
important to consider the specific needs of your project and evaluate the impact of LTO on
both build times and runtime performance. Experimenting with different linker options
and adjusting LTO settings based on the build type can help you achieve the best balance
between performance and build efficiency.
444

12.4 Optimizing Compilation Flags


CMAKE CXX FLAGS RELEASE)

In this section, we will delve into the role of compilation flags in optimizing build
performance, specifically focusing on CMAKE CXX FLAGS RELEASE. These flags are
used to control the behavior of the C++ compiler when building in different configurations,
such as release or debug builds. By fine-tuning these flags, you can significantly impact
the performance of your C++ application, both during compilation and at runtime.

Understanding how to optimize compilation flags is crucial for achieving faster builds and
ensuring that your project runs efficiently in production environments.

12.4.1 What are Compilation Flags?

Compilation flags are options passed to the compiler during the build process that control
various aspects of the compilation and optimization process. These flags can affect:

• Optimization level: How aggressively the compiler optimizes the generated


machine code.

• Debug information: Whether and how debugging information is included in the


binary.

• Code generation: Whether the compiler applies specific instructions or techniques


to generate more efficient code.

• Warnings: What kind of compiler warnings are emitted to help catch potential
issues in the code.

In CMake, these flags are typically defined in variables like CMAKE CXX FLAGS, which
445

affects all build types, or CMAKE CXX FLAGS RELEASE, which specifically influences
the compilation when the project is being built in release mode.

12.4.2 Why Focus on CMAKE CXX FLAGS RELEASE?

When building C++ projects for production (i.e., in release mode), the goal is to produce
the most optimized, efficient executable. By default, CMake applies certain flags to the
compiler, but these can be modified for better performance.

CMAKE CXX FLAGS RELEASE is a variable in CMake that holds the flags to be applied
when building in the release configuration. These flags can be used to instruct the
compiler to optimize code for performance, remove debugging information, and disable
certain features that are unnecessary for production builds. Customizing this variable
ensures that the release build of your application is as optimized as possible.

12.4.3 Key Optimization Flags for CMAKE CXX FLAGS RELEASE

When configuring a C++ project for release, several compiler flags can be used to control
optimization behavior. These flags will vary depending on the compiler (GCC, Clang,
MSVC, etc.), but the following are common optimizations you can apply to improve the
performance of the release build.

1. Optimization Level (-O3 or -O2)


The optimization level determines how aggressively the compiler will optimize the
code for performance.

• -O2 (default): This optimization level enables a reasonable amount of


optimizations while maintaining compile time and debugging ability. It strikes a
balance between performance and build speed.
446

• -O3: This is the highest level of optimization. It enables more aggressive


optimizations, such as loop unrolling, vectorization, and function inlining,
which can significantly improve runtime performance. However, it might result
in longer compile times and larger binary sizes.

Example:

set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")

2. Link Time Optimization (-flto)


Link Time Optimization (LTO) allows the compiler to perform optimizations
across object files during the linking stage. This enables interprocedural
optimizations like inlining functions across different translation units and removing
unused code. LTO can be enabled by adding the -flto flag.
Example:

set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto")

LTO can significantly reduce the size of the generated binary and improve
performance by allowing more aggressive optimizations. However, as mentioned in
Section 3, LTO can increase link times and memory usage during the linking process,
so it is essential to test its impact on your build process.
3. Fast Math (-ffast-math)
The -ffast-math flag instructs the compiler to enable floating-point
optimizations that can lead to faster math operations at the cost of potentially less
strict conformance to the IEEE floating-point standard. This can be useful when
performance is a higher priority than absolute numerical accuracy.
Example:
447

set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}
,→ -ffast-math")

Use with caution: While this flag can make math-heavy programs run faster, it may
result in slightly less accurate results, especially in edge cases involving
floating-point precision.
4. Function Inlining (-finline-functions)
Inlining functions can speed up code execution by removing the overhead of
function calls. The -finline-functions flag enables the compiler to replace
small functions with their code at the call site. This is particularly effective for small
functions that are frequently called, as it can avoid the performance cost of function
calls.
Example:

set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}
,→ -finline-functions")

5. Loop Unrolling (-funroll-loops)


Loop unrolling is an optimization that allows the compiler to increase the
performance of loops by expanding them. This reduces the number of iterations and
overhead for each loop, but it increases the size of the binary. This can be beneficial
in certain performance-critical applications, especially in tight loops that are
frequently executed.
Example:

set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}
,→ -funroll-loops")
448

6. Strict Aliasing (-fstrict-aliasing)


The -fstrict-aliasing flag tells the compiler to assume that pointers to
different types will not alias (i.e., point to the same memory address). This allows
the compiler to perform more aggressive optimizations, as it can treat objects of
different types as separate and unrelated entities.
Example:

set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}
,→ -fstrict-aliasing")

Caution: This flag assumes that the code adheres to the strict aliasing rule, so it can
lead to unexpected behavior if violated. It’s important to ensure that your codebase
correctly follows this rule if you decide to use this flag.
7. Optimization for Size (-Os)
While most release configurations prioritize performance, some situations demand
optimization for binary size, such as embedded systems or applications running on
resource-constrained devices. The -Os flag instructs the compiler to optimize for
size rather than performance.
Example:

set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os")

This flag will reduce the size of the final binary by turning off some performance
optimizations that result in larger code, such as loop unrolling.
8. No Debug Information (-g0)
By default, the compiler generates debug information for release builds in some
setups. This can unnecessarily bloat the binary size. To reduce this, you can use the
-g0 flag to disable debug information for release builds.
449

Example:

set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -g0")

If you do need to keep some level of debug information (for profiling, for example),
you can instead use -g with optimizations like -O3 and -flto to balance the
trade-offs.

12.4.4 Combining Compilation Flags for Release Builds

When optimizing the CMAKE CXX FLAGS RELEASE variable, it is common to combine
several of the flags mentioned above. Here’s an example of how to combine flags for a
highly optimized release build:

set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -flto


,→ -ffast-math -funroll-loops -fstrict-aliasing -g0")

This combination applies:

• Maximum optimization with -O3

• Link Time Optimization with -flto

• Faster math computations with -ffast-math

• Loop unrolling with -funroll-loops

• Strict aliasing with -fstrict-aliasing

• Removal of debug information with -g0


450

12.4.5 Conclusion

Optimizing compilation flags in the release configuration


(CMAKE CXX FLAGS RELEASE) is a powerful way to enhance the performance of your
C++ project. By selecting the right flags, you can minimize compile time, improve
runtime performance, reduce the binary size, and fine-tune your application's performance
based on specific needs.
It is important to note that while these flags can significantly improve performance, the
impact varies based on the nature of the project and the target platform. Therefore, it is
recommended to test different combinations of optimization flags and profile the results to
find the best configuration for your project.
451

12.5 Using Ninja for Faster Builds

In this section, we will explore how to use Ninja, a fast build system, to optimize the build
performance of your CMake-based C++ projects. Ninja is designed to be small, efficient,
and highly parallel, making it an excellent choice for speeding up build times, especially
in larger projects. We will cover the benefits of using Ninja, how to configure it within
CMake, and provide some tips on leveraging its strengths for faster builds.

12.5.1 What is Ninja?

Ninja is a small, low-level build system that was originally developed by Google. Unlike
traditional build systems like Make, Ninja focuses on being fast and optimized for parallel
builds. It reads a build description (usually a build.ninja file) that specifies the rules
for building the project and executes them efficiently, ensuring the shortest possible time
for building or rebuilding files that have changed.

Ninja works by focusing on the smallest possible operations to rebuild only the necessary
parts of the project, making it especially effective in large projects with complex
dependencies. It is designed to be used as a backend for higher-level build systems, such
as CMake, which generates the necessary build.ninja file based on your project
configuration.

12.5.2 Why Use Ninja?

The main reasons to use Ninja for building your CMake-based projects are:

1. Faster Builds:
452

• Ninja is optimized for speed. It minimizes overhead and executes tasks more
efficiently than traditional systems like Make, especially when dealing with
complex projects that require large numbers of files to be built.
• Ninja performs parallel builds more effectively by managing dependencies and
utilizing available CPU cores. This results in faster build times, especially on
multi-core systems.
2. Smaller Overhead:
• Ninja's minimalistic design means that it has less overhead compared to other
build systems. It doesn't have the complex features that may slow down other
build tools, such as Make or MSBuild.
• Ninja focuses solely on the job of building files, keeping the process as
streamlined and direct as possible.
3. Parallelism:
• Ninja’s parallel build capabilities are second to none. It determines the
dependencies between tasks and intelligently decides which tasks can run
concurrently. This is particularly useful when building large codebases, as
multiple processes can be executed simultaneously, fully utilizing modern
multi-core processors.
4. Incremental Builds:
• Ninja performs incremental builds extremely well. If only a small part of the
project has changed, Ninja will rebuild only the affected files, making the
rebuild process much faster than traditional systems.
5. Ease of Integration with CMake:
• Ninja is integrated directly with CMake, meaning it can be used with minimal
configuration. CMake has built-in support for generating Ninja build files,
allowing you to easily switch to Ninja without changing your project structure.
453

12.5.3 How to Use Ninja with CMake

Using Ninja with CMake is relatively simple. Here’s a step-by-step guide to configuring
and using Ninja for faster builds.

• Step 1: Install Ninja


First, you need to install Ninja on your system. The installation process differs
depending on your operating system:
– Linux (Ubuntu/Debian):

sudo apt-get install ninja-build

– macOS (using Homebrew):

brew install ninja

– Windows:

* You can download the Ninja binary from the official Ninja website:
https://ninja-build.org/
* Alternatively, if you're using
Chocolatey
, you can install it with:

choco install ninja

• Step 2: Configure CMake to Use Ninja


Once Ninja is installed, the next step is to configure your CMake project to use Ninja
as the build system. This can be done by specifying the generator when you invoke
CMake.
454

Navigate to your project’s root directory, and then run the following CMake
command:

cmake -G Ninja .

This tells CMake to generate a build.ninja file instead of the default


Makefile. The -G flag specifies the generator, and in this case, we are using
Ninja.
You can also specify additional options for your build configuration, such as setting
the build type:

cmake -G Ninja -DCMAKE_BUILD_TYPE=Release .

This command will configure your CMake project for a Release build and generate
the necessary build.ninja file for Ninja to use.

• Step 3: Build with Ninja


Once the configuration is complete, you can build your project using the following
Ninja command:

ninja

This will invoke Ninja to start the build process. Ninja will automatically determine
which files need to be rebuilt based on the dependencies specified in the
build.ninja file and execute the necessary commands. It will also execute tasks
in parallel when possible to speed up the build process.
To build a specific target, use:
455

ninja <target>

For example, to build a target named my target, you would run:

ninja my_target

• Step 4: Incremental Builds with Ninja


One of the standout features of Ninja is its ability to perform incremental builds.
This means that Ninja will only rebuild parts of the project that have changed, saving
time on subsequent builds.
For example, if you make a change to a single source file, Ninja will detect that only
the related object file and executable need to be rebuilt, while the other parts of the
project remain unchanged. This leads to faster rebuilds compared to traditional build
systems like Make.
• Step 5: Clean and Rebuild
If you need to clean your project (remove all generated files) and perform a full
rebuild, use the ninja clean command followed by ninja:

ninja clean
ninja

This will remove all build artifacts and recompile everything from scratch, ensuring
a fresh build.

12.5.4 Fine-Tuning Ninja for Maximum Performance

While Ninja’s default configuration is already optimized for speed, there are a few tips you
can follow to ensure you're getting the most out of your builds.
456

1. Use Parallel Builds:

• Ninja automatically determines the number of jobs (parallel tasks) to run based
on the system’s CPU cores. However, you can manually specify the number of
parallel jobs with the -j flag:

ninja -j 8

This will instruct Ninja to run up to 8 tasks in parallel. You can adjust this number
based on the number of cores in your machine. For modern multi-core systems, this
can drastically reduce build time.

2. Enable Caching:

• CMake can generate a cache file that stores information about previously built
objects. This can speed up incremental builds by reusing previously compiled
object files instead of recompiling them.

3. Use Ninja with Clang and GCC:

• Ninja works excellently with Clang and GCC compilers, which are often much
faster than MSVC, especially when combined with the parallelism and
optimizations that Ninja brings.

4. Optimizing CMake’s Generated build.ninja:

• CMake's Ninja generator produces a build.ninja file that is tailored for


the project. However, you can customize the build system by modifying the
CMakeLists.txt file to control how Ninja works.

For example, you can add extra flags or parameters that will be passed to Ninja
during the build process:
457

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native")


set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")

These flags will be included in the build instructions, optimizing your project further.
5. Use CMake Presets:
• With the introduction of CMake presets (CMake 3.19+), you can pre-define
build settings, including Ninja options. By creating a CMakePresets.json
file, you can easily switch between build configurations, making it even easier
to optimize your project for different platforms.

12.5.5 Ninja vs. Make: Why Ninja is Faster

While Make has been the traditional choice for many developers, Ninja has significant
advantages when it comes to build performance. Here’s a quick comparison between
Ninja and Make:

Feature Ninja Make


Parallelism Highly efficient parallelism Limited parallelism
capabilities
Build Speed Extremely fast, minimal Slower due to extra overhead
overhead
Incremental Build Optimized incremental builds Slower incremental builds
due to overhead
Complexity Simple and lightweight More complex and feature-
heavy

Ninja excels because it is designed specifically to handle large-scale builds efficiently. It


performs fewer tasks per build, which reduces overhead and speeds up the entire process.
458

12.5.6 Conclusion

Incorporating Ninja into your CMake workflow is one of the most effective ways to speed
up the build process. It provides a lightweight, parallelized, and incremental build system
that drastically reduces build times, especially for large projects. With its integration into
CMake, using Ninja is simple, and it provides an immediate improvement over traditional
systems like Make.
By configuring your CMake project to use Ninja and optimizing the compilation process
with appropriate flags, you can ensure faster, more efficient builds. Whether you're
developing a small application or a massive codebase, Ninja is an excellent tool to help
streamline your build process.
Chapter 13

Writing Custom CMake Modules

13.1 Creating Custom Modules (FindMyLib.cmake)

In CMake, modules are an essential tool for managing dependencies, external libraries,
and various configurations within your build system. A custom module, like
FindMyLib.cmake, allows you to extend CMake's functionality by providing
user-defined logic for locating libraries, packages, or tools that aren't supported
out-of-the-box by CMake. This section dives into how you can write your own
FindMyLib.cmake module to simplify the management of external libraries in your
project.

13.1.1 Understanding the Purpose of FindMyLib.cmake

The purpose of creating a custom CMake module such as FindMyLib.cmake is to


search for a particular library (in this case, a library named MyLib) and provide a way to
integrate it into your CMake project. By writing a custom find module, you can automate

459
460

the discovery and inclusion of the library into your build process. This ensures that your
build system remains flexible and portable across various systems and environments.

When dealing with external dependencies, CMake provides a set of predefined modules
(e.g., FindOpenSSL.cmake, FindBoost.cmake) that can automatically find
certain well-known libraries. However, when working with custom or lesser-known
libraries, writing your own module becomes necessary.

The custom module FindMyLib.cmake will perform tasks such as:

• Locating the library's files (header files, binary files, etc.)

• Verifying the presence and correct version of the library

• Providing information to CMake about the found library

• Handling potential errors or missing dependencies

13.1.2 Key Structure of FindMyLib.cmake

A typical FindMyLib.cmake file is organized into specific sections that follow a


standardized structure to ensure CMake can process it correctly. Here’s a basic structure
of a custom CMake module:

1. Define the Library Variables:

• The module will set up various variables that will contain the paths to the library
files once found. For example, it may set variables like
MyLib INCLUDE DIRS for the header files and MyLib LIBRARIES for the
binary files.
461

set(MyLib_INCLUDE_DIRS "")
set(MyLib_LIBRARIES "")

2. Check for the Library in Common Locations:

• The module will attempt to locate the library in common installation paths, such
as /usr/lib, /usr/local/lib, or any custom directories defined by the
user or the environment. This might involve using CMake's find path() and
find library() commands.

find_path(MyLib_INCLUDE_DIR NAMES mylib.h PATHS


,→ /usr/local/include /usr/include)
find_library(MyLib_LIBRARY NAMES libmylib.so libmylib.a PATHS
,→ /usr/local/lib /usr/lib)

3. Check for the Correct Version of the Library:

• If the library has a version requirement, the module will check whether the
version of the found library is appropriate for your project. This could be done
using find package() or version comparison logic.

if (NOT MyLib_LIBRARY)
message(FATAL_ERROR "MyLib not found")
endif()

# Optionally, check the version


find_package(MyLib 1.2 REQUIRED)

4. Provide Information About the Found Library:


462

• After successfully locating the library, you should define CMake variables that
contain information about the library. This includes its include directories,
libraries, and possibly its version.

set(MyLib_INCLUDE_DIRS ${MyLib_INCLUDE_DIR})
set(MyLib_LIBRARIES ${MyLib_LIBRARY})

5. Handle Missing Dependencies:

• If the module can't find the library, it should handle this failure gracefully by
either providing informative warnings or terminating the process with an error
message.

if (NOT MyLib_INCLUDE_DIRS)
message(FATAL_ERROR "Could not find MyLib include directory")
endif()

if (NOT MyLib_LIBRARIES)
message(FATAL_ERROR "Could not find MyLib library")
endif()

6. Expose the Found Results:

• After the search is completed, the variables containing the results will be made
available to the rest of the project, which can then link against the found library
or include its headers.

mark_as_advanced(MyLib_INCLUDE_DIRS MyLib_LIBRARIES)

The mark as advanced() command ensures that these variables are not shown
by default in CMake's GUI or when listing all variables.
463

13.1.3 Example of FindMyLib.cmake

Here’s a complete example of what FindMyLib.cmake might look like for a


hypothetical library MyLib:

# FindMyLib.cmake - CMake module to find the MyLib library

# Define variables to store paths to MyLib


set(MyLib_INCLUDE_DIRS "")
set(MyLib_LIBRARIES "")

# Look for MyLib headers in typical locations


find_path(MyLib_INCLUDE_DIR NAMES mylib.h PATHS /usr/local/include
,→ /usr/include)

# Look for MyLib library binaries


find_library(MyLib_LIBRARY NAMES libmylib.so libmylib.a PATHS
,→ /usr/local/lib /usr/lib)

# If MyLib is not found, report an error


if (NOT MyLib_INCLUDE_DIR)
message(FATAL_ERROR "MyLib not found: No header files")
endif()

if (NOT MyLib_LIBRARY)
message(FATAL_ERROR "MyLib not found: No library files")
endif()

# Store the paths in the final variables


set(MyLib_INCLUDE_DIRS ${MyLib_INCLUDE_DIR})
set(MyLib_LIBRARIES ${MyLib_LIBRARY})
464

# Mark the variables as advanced


mark_as_advanced(MyLib_INCLUDE_DIRS MyLib_LIBRARIES)

# Provide information to CMake about the found MyLib


message(STATUS "Found MyLib: ${MyLib_INCLUDE_DIRS}
,→ ${MyLib_LIBRARIES}")

13.1.4 How to Use FindMyLib.cmake in a CMake Project

Once you have created your FindMyLib.cmake file, you can use it in your main
CMakeLists.txt file to locate and use MyLib. Here's how you can integrate it:

1. Place FindMyLib.cmake in a Module Directory:

• Put the FindMyLib.cmake file inside a cmake/Modules directory or any


directory of your choice. Ensure the directory is in your CMake module path by
specifying it in your CMakeLists.txt.

list(APPEND CMAKE_MODULE_PATH
,→ "${CMAKE_SOURCE_DIR}/cmake/Modules")

2. Use find package() to Find the Library:

• You can now use the find package() command to locate MyLib in your
CMakeLists.txt. CMake will automatically search for
FindMyLib.cmake in the directories specified in CMAKE MODULE PATH.
465

find_package(MyLib REQUIRED)

3. Link Against MyLib:

• After successfully finding the library, you can link it to your targets using the
variables set by your module.

target_include_directories(MyApp PRIVATE ${MyLib_INCLUDE_DIRS})


target_link_libraries(MyApp PRIVATE ${MyLib_LIBRARIES})

13.1.5 Conclusion

Creating a custom module like FindMyLib.cmake is a powerful way to streamline the


process of managing external dependencies in CMake-based projects. By writing this
module, you gain flexibility in handling libraries that CMake doesn’t support natively.
Once created, it can be reused across multiple projects, providing a clean and efficient way
to integrate libraries into your build system.
By following the steps outlined in this section, you will not only be able to locate and link
external libraries but also handle complex configurations and ensure portability across
different environments. This is a crucial skill for any advanced CMake user working on
complex C++ projects.
466

13.2 Defining Custom CMake Commands (macro,


function)

In CMake, the ability to create custom commands is a powerful feature that allows you to
extend the functionality of the build system. You can define new commands using
macro() and function() to encapsulate repeated or complex logic in your CMake
projects. By creating your own commands, you can make your CMake scripts more
modular, reusable, and easier to maintain. This section will explain how to define custom
commands in CMake using both macro() and function(), and outline the
differences between them.

13.2.1 Introduction to Custom CMake Commands

CMake provides built-in commands for common tasks such as add executable(),
add library(), include directories(), and
target link libraries(). However, sometimes the built-in functionality is not
enough for your project's needs, and you need to define custom behavior that suits your
specific requirements.
Custom commands allow you to bundle a set of CMake commands into a single reusable
unit, which can be invoked by other parts of the CMake script. CMake provides two
primary ways to define custom commands: macros and functions. While they are similar,
they have distinct behaviors and use cases.

13.2.2 Using macro()

The macro() command in CMake defines a custom command that behaves like a
CMake script block, with an important distinction that the variables you modify inside the
467

macro affect the caller's environment. This means that any changes made to variables
within the macro will persist after the macro finishes executing, unless the variable was
explicitly set as CACHE or made private to the macro’s scope.

Syntax:

macro(<name> [arguments])
# Commands to execute
endmacro()

• : The name of the new custom macro.

• [arguments]: An optional list of arguments that the macro will accept. These can be
used within the macro for more flexible behavior.

Example of Using macro():

Here’s a simple example where we create a custom add warning() macro that adds a
compiler warning for a specific flag to the target:

# Define a macro to add compiler warning flags to a target


macro(add_warning target)
target_compile_options(${target} PRIVATE -Wall -Wextra)
endmacro()

# Usage of the macro


add_executable(MyApp main.cpp)
add_warning(MyApp)

In this example:
468

• The macro add warning() is defined to accept a target name and adds the
-Wall and -Wextra flags for compiler warnings.
• When we call add warning(MyApp), the macro adds these options to the
MyApp target.

The key thing to note about macros is that they affect the caller's environment, meaning
that target compile options() changes apply to the caller’s scope.

When to Use macro():

• Use macros when you want the custom command to directly modify variables or
target properties in the calling scope.
• Macros are particularly useful for tasks where you need to apply settings or options
to a set of targets, especially when those targets are created dynamically or within
loops.

13.2.3 Using function()

The function() command in CMake, like macro(), defines a custom command, but
with one critical difference: the variables modified inside a function do not affect the
caller’s environment. This makes functions ideal when you want to encapsulate logic
without modifying the caller's state. Functions are typically used for more contained,
self-contained operations.

Syntax:

function(<name> [arguments])
# Commands to execute
endfunction()
469

• : The name of the new custom function.


• [arguments]: An optional list of arguments that the function will accept.

Example of Using function():


Here’s a simple example where we create a custom
add definitions for target() function that adds preprocessor definitions to a
target:

# Define a function to add preprocessor definitions to a target


function(add_definitions_for_target target)
target_compile_definitions(${target} PRIVATE MY_DEFINE=1)
endfunction()

# Usage of the function


add_executable(MyApp main.cpp)
add_definitions_for_target(MyApp)

In this example:

• The function add definitions for target() adds the preprocessor


definition MY DEFINE=1 to the specified target.
• When we call add definitions for target(MyApp), the function applies
the preprocessor definition to MyApp.

The key difference from the macro is that changes made inside the function do not persist
in the caller’s scope. This behavior ensures that functions don't unintentionally affect the
environment outside their scope, leading to more predictable and safer builds.

When to Use function():


470

• Use functions when you want to encapsulate logic that should not modify the caller's
environment.
• Functions are ideal when you want to perform operations like adding compile
definitions, setting properties, or performing checks without affecting the calling
CMake script's state.

13.2.4 Differences Between macro() and function()

While both macro() and function() allow you to define custom CMake commands,
they differ in terms of scope and variable handling. Here’s a summary of the key
differences:

Feature macro() function()


Scope Modifies the caller’s Has its own scope, doesn’t
environment affect caller
Variable Changes to variables persist Changes to variables are local
Modification outside macro to the function
Usage Useful when modifying Ideal for encapsulating self-
targets or global state contained logic
Arguments Passes arguments by value Passes arguments by value

In most cases, if you need to modify variables or state outside of the custom command,
use macro(). If you want the custom logic to stay isolated without affecting the
environment, use function().

13.2.5 Best Practices for Custom Commands

Here are some best practices when defining custom CMake commands:
471

1. Use functions for isolated logic: If your command doesn't need to modify the
caller's environment or global state, use a function to avoid unexpected side effects.
2. Keep macros small and focused: Since macros modify the calling environment,
they should be used sparingly and only when necessary. Avoid large macros that
could introduce unintended side effects.
3. Use set() with CACHE cautiously: If you intend to make a variable accessible
globally or persist across multiple CMake files, use set() with CACHE. However,
use this only when truly necessary.
4. Documentation and clarity: Be clear in naming your custom commands (e.g.,
add warning(), add definitions for target()) to make it obvious
what the command does. Also, consider documenting the arguments and expected
behavior of your custom commands.

13.2.6 Conclusion

Defining custom commands with macro() and function() is an essential skill for
advanced CMake users. It allows you to abstract repetitive or complex logic, making your
CMake scripts more modular and reusable. The choice between macro() and
function() depends on whether you want to modify the caller’s environment or keep
the changes isolated within the custom command itself.
By understanding the differences between macros and functions, and knowing when to
use each, you will be able to create highly flexible and efficient CMake scripts for
managing and building your C++ projects.
472

13.3 Managing Environment Variables Inside CMake


Modules

When working with CMake, environment variables can play a crucial role in determining
how the build system behaves. These variables can influence the configuration of
compilers, libraries, paths, and various other aspects of the project. Managing
environment variables within custom CMake modules becomes important when you need
to tailor the behavior of your build system to different environments, especially when
handling external dependencies, toolchains, or specific build configurations.

In this section, we will explore how to manage environment variables effectively within
CMake modules. This includes how to read, modify, and pass environment variables, as
well as ensuring that changes made to them are correctly reflected during the configuration
and build process.

13.3.1 Introduction to Environment Variables in CMake

In CMake, environment variables are variables that are typically defined outside the
CMake build system, often at the operating system level or within the user’s shell session.
These environment variables can affect how the CMake toolchain behaves or how certain
external dependencies are discovered.

For example:

• The PATH environment variable is often used to locate executable files (e.g.,
compilers, utilities).

• The CXX and CC variables specify which compilers should be used for C++ and C
code, respectively.
473

• Custom environment variables, such as MYLIB DIR, can be used to provide


additional information about the location of external libraries.

Managing these environment variables inside custom CMake modules allows your project
to be adaptable to different systems and setups.

13.3.2 Accessing Environment Variables in CMake

CMake provides a way to read environment variables using the ENV{} syntax. This
allows you to query the environment of the current process (which may include
information set before running CMake). This can be especially useful when you need to
conditionally configure your build system based on system settings, or when integrating
external tools or libraries that rely on environment variables.

Syntax:

set(MY_VAR $ENV{MY_ENV_VARIABLE})

• $ENV{MY ENV VARIABLE}: This retrieves the value of the environment


variable MY ENV VARIABLE at the time the CMake script is executed.

Example:

Suppose we want to read the HOME environment variable to configure a specific path in
the build system:

set(HOME_DIR $ENV{HOME})
message(STATUS "Home directory: ${HOME_DIR}")
474

This would print the value of the HOME environment variable on Unix-based systems. On
Windows, it could print the path to the user's home directory.

13.3.3 Modifying Environment Variables in CMake

Unlike traditional shell scripts, changes to environment variables in CMake are typically
local to the CMake process and its subprocesses. Once the CMake process finishes, any
modifications to environment variables will not persist. However, changes to environment
variables can be passed down to external processes (such as compilers or custom build
steps) via CMake's set() and add custom command() commands.

Modifying Environment Variables for Subprocesses:


To modify an environment variable for subprocesses spawned by CMake (such as during
the build process), you can use the set() command to define or modify environment
variables for the scope of a CMake script or function. This can be particularly useful when
you want to configure environment-specific tools during the build.

set(ENV{MY_TOOL_PATH} "/path/to/tool")

This would set the MY TOOL PATH environment variable for any commands or processes
invoked after this line in the CMake script.

Example of Passing Modified Environment Variables to a Custom Command:

# Set environment variable for subprocesses


set(ENV{MY_LIB_PATH} "/opt/mylib")

# Custom command that uses the environment variable


add_custom_command(
475

TARGET MyApp POST_BUILD


COMMAND $ENV{MY_LIB_PATH}/bin/mylib_tool
COMMENT "Running mylib_tool"
)

In this example, MY LIB PATH is set to /opt/mylib, and this value is passed to the
mylib tool during the post-build phase of the MyApp target.

13.3.4 Managing Environment Variables in CMake Modules

CMake modules are often used to detect and configure external libraries or tools. To
handle environment variables within a module, you can check for their existence, set
defaults if needed, and modify them based on user input or system-specific settings.

Example 1: Checking for an Environment Variable

Suppose you need to detect if an environment variable MYLIB PATH is set and, if not,
provide a default path:

# Check if MYLIB_PATH is set in the environment


if (NOT $ENV{MYLIB_PATH})
set(MYLIB_PATH "/default/path/to/mylib")
message(STATUS "MYLIB_PATH not set, using default:
,→ ${MYLIB_PATH}")
else()
message(STATUS "Found MYLIB_PATH: $ENV{MYLIB_PATH}")
endif()

In this example:
476

• If the MYLIB PATH environment variable is not set, it will be assigned a default
value (/default/path/to/mylib).
• If MYLIB PATH is set, the script will output the value to the user.

Example 2: Modifying Environment Variables for a Subprocess


In a more complex scenario, you might need to pass environment variables to a
subprocess, such as when calling a build tool that requires specific environmental settings.
You can set these variables temporarily within a module to configure the environment for
that subprocess:

# Set a custom environment variable for a build step


set(ENV{MYLIB_VERSION} "1.2.3")

# Use the environment variable in a custom command


add_custom_command(
TARGET MyApp POST_BUILD
COMMAND $ENV{MYLIB_VERSION}
COMMENT "Running version check for mylib"
)

This example demonstrates how you can temporarily set an environment variable and then
use it in a custom command that is executed during the build process.

13.3.5 Persistent Changes to Environment Variables

To make persistent changes to environment variables that apply across multiple CMake
runs, you can modify the CMakeCache.txt file or use set() with the CACHE option.
This allows you to store environment variable values across different configurations or
project runs.
477

# Set a cache variable that can be accessed across multiple CMake


,→ runs
set(MYLIB_PATH "/opt/mylib" CACHE PATH "Path to MyLib")

This example sets the MYLIB PATH variable in the CMake cache, which can be modified
by the user through the CMake GUI or via command-line options. It ensures that the path
is preserved between builds, allowing for a consistent environment.

13.3.6 Best Practices for Managing Environment Variables

Managing environment variables inside CMake modules requires care to avoid conflicts,
ensure portability, and make the project environment predictable. Here are some best
practices for working with environment variables in CMake:

1. Avoid Global Changes: Modifying environment variables globally can have


unintended consequences, especially when the same environment variable is used for
different purposes. Prefer to scope changes to the specific commands or tools that
need them.
2. Use Defaults When Possible: Always provide default values for environment
variables where appropriate. This ensures that your project can still build even if the
expected environment variables are not set.
3. Explicitly Document Required Environment Variables: When you rely on
environment variables in your CMake modules, make sure to clearly document them.
This allows users of your project to configure the environment correctly.
4. Make Environment Variables Optional: Where possible, make the use of
environment variables optional. Use them only if they are present, and fall back to
defaults if they are not.
478

5. Use Cache Variables for User Configuration: If your project requires user-defined
paths or settings, consider using CMake’s CACHE to persist environment variables or
configuration settings. This allows users to customize their environment and avoid
having to reset values with every CMake run.

13.3.7 Conclusion

Managing environment variables inside CMake modules is a vital aspect of making a


CMake-based build system flexible and adaptable to various environments. By
understanding how to read, modify, and pass environment variables, you can configure
your CMake scripts to automatically adjust to system-specific settings, integrate with
external tools, and ensure smooth builds. Properly managing these variables allows for
cleaner, more maintainable CMake modules that work reliably across different systems
and configurations.
This section provided the foundational tools and practices for managing environment
variables within CMake, ensuring that you can handle complex external dependencies and
environments with ease.
479

13.4 Improving Reusability of CMake Code

CMake is a powerful tool that allows for building and managing C++ projects across
different platforms. However, as projects grow in complexity, it is essential to structure
CMake scripts in a way that improves modularity, reduces duplication, and enhances the
ease of maintenance. One of the most important principles for managing complex projects
is reusability. By focusing on reusability, you ensure that the same logic can be applied
across multiple projects, configurations, or platforms without having to rewrite the code.

This section delves into strategies and best practices for improving the reusability of
CMake code. It covers various approaches, including modular design, use of functions,
macros, custom modules, and configuration files, all aimed at making your CMake scripts
more efficient and adaptable.

13.4.1 Introduction to Reusability in CMake

CMake scripts often grow large and complex, particularly when handling multiple
dependencies, toolchains, platforms, and configurations. Reusability in this context means
writing CMake code that can be easily reused across different parts of a project or even
across different projects. This can save time, reduce errors, and make the CMake build
system much more maintainable.

To achieve reusability, we can break down CMake scripts into smaller, self-contained
modules, write reusable functions or macros, and take advantage of existing libraries or
tools to manage common tasks.
480

13.4.2 Modularizing CMake Code

The first step in improving the reusability of your CMake code is to modularize it. This
means breaking your CMake scripts into smaller, logical components (modules) that can
be easily reused in different parts of the project or in entirely different projects.

Creating Reusable CMake Modules

A CMake module is essentially a CMake script that encapsulates a specific task or set of
related tasks. These modules can be easily included into other CMake scripts using
include() or find package(). They can also be packaged as external CMake
modules for use across different projects.

To create a reusable module, you need to:

1. Identify a specific task or set of tasks that can be encapsulated into a module.

2. Define the module in a separate CMake file.

3. Use include() or find package() to integrate the module into your project.

Example of a CMake Module

Let's say you frequently need to check for the presence of a particular library, such as
Boost. You can create a CMake module that encapsulates this logic and reuse it across
multiple projects.

# File: FindBoost.cmake
find_package(Boost REQUIRED)
if (Boost_FOUND)
message(STATUS "Boost found at ${Boost_INCLUDE_DIRS}")
else()
481

message(FATAL_ERROR "Boost not found!")


endif()

Then, in your main CMake script, you simply include this module:

# File: CMakeLists.txt
include(FindBoost)

add_executable(MyApp main.cpp)
target_link_libraries(MyApp Boost::Boost)

By breaking out the logic into a separate module (FindBoost.cmake), you can now
reuse it across other projects without having to repeat the same code in each CMake script.

Organizing CMake Modules

To improve maintainability, consider organizing your CMake modules into directories by


category. For instance, you might have directories like:

/cmake/modules/
/find_packages/
/utilities/
/platform_specific/

This structure makes it easy to find and manage reusable components for different tasks,
and it allows you to scale the reusability as the number of modules grows.
482

13.4.3 Reusing Functions and Macros

While CMake modules are an excellent way to modularize the configuration, you can also
improve the reusability of your code by defining functions and macros. These allow you
to encapsulate frequently used logic into reusable units that can be invoked with different
parameters.

Functions for Reusability


Functions in CMake have their own scope, which means any changes made to variables
inside a function will not affect the calling scope unless explicitly passed as arguments or
returned. Functions are ideal for performing isolated tasks or configurations.
For example, consider a function to add a common set of compiler flags to a target:

# File: CMakeLists.txt
function(add_common_flags target)
target_compile_options(${target} PRIVATE -Wall -Wextra)
endfunction()

# Usage:
add_executable(MyApp main.cpp)
add_common_flags(MyApp)

By using a function, we can easily reuse the add common flags() logic across
different targets in the same or other projects, simply by calling the function with the
target name as an argument.

Macros for Reusability


Macros in CMake work similarly to functions, but they do not have their own scope.
Instead, they modify the calling environment, which makes them suitable for scenarios
483

where you want to directly alter the calling CMake context. Macros are often used for
settings or changes that need to be applied globally across multiple parts of the build
system.

Here’s an example of a reusable macro that adds custom warning flags to multiple targets:

# File: CMakeLists.txt
macro(add_warning_flags target)
target_compile_options(${target} PRIVATE -Wall -Wextra
,→ -Wpedantic)
endmacro()

# Usage:
add_executable(MyApp main.cpp)
add_warning_flags(MyApp)

add_executable(AnotherApp another.cpp)
add_warning_flags(AnotherApp)

This macro applies the same set of compiler warning flags to multiple targets without
needing to rewrite the logic each time.

13.4.4 Use of find package() for External Dependencies

When working with external libraries or tools, it’s common to use the find package()
command in CMake. However, rather than writing the logic for finding a package or
library every time, you can use the find package() command within reusable CMake
modules.

For example, to reuse the logic for finding the Boost library, you could create a custom
module called FindBoost.cmake (as shown earlier), which will be automatically
484

included in different projects. The module would be reusable because it abstracts away the
complexity of finding the library and setting the appropriate flags.

Example of Reusing find package() for External Tools:

# File: FindMyTool.cmake
find_package(MyTool REQUIRED)
if (MyTool_FOUND)
message(STATUS "MyTool found at ${MyTool_INCLUDE_DIR}")
else()
message(FATAL_ERROR "MyTool not found!")
endif()

Now, in any project, you can simply include FindMyTool.cmake to find and configure
MyTool:

# File: CMakeLists.txt
include(FindMyTool)

add_executable(MyApp main.cpp)
target_link_libraries(MyApp MyTool::MyTool)

This modular approach enables the reuse of the same external dependency configuration
across different projects.

13.4.5 Using CMake Config Files for Reusability

In addition to creating CMake modules, functions, and macros, another powerful tool for
improving reusability is CMake configuration files. These configuration files allow you
485

to store project settings, variables, and options in an external file that can be easily shared
and reused across different projects.

Example: Using a CMake Configuration File

Suppose you have a set of common configuration options (e.g., compiler flags, library
paths, version numbers) that you want to share across multiple projects. You can create a
config.cmake file:

# File: config.cmake
set(MYLIBRARY_PATH "/path/to/mylibrary")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")

Then, you can include this configuration file in your CMake script:

# File: CMakeLists.txt
include(config.cmake)

message(STATUS "Library path: ${MYLIBRARY_PATH}")


message(STATUS "Compiler flags: ${CMAKE_CXX_FLAGS}")

By using external configuration files, you ensure that these shared settings can be reused
and maintained in one central location.

13.4.6 Best Practices for Reusability

To ensure your CMake code remains modular and reusable, consider the following best
practices:
486

1. Encapsulation: Break down complex CMake scripts into smaller, reusable modules
that encapsulate specific tasks or logic. This prevents your main
CMakeLists.txt file from becoming too cluttered.
2. Use Functions and Macros Effectively: Functions should be used for tasks that
need to operate within a local scope (e.g., modifying specific targets), while macros
should be used for tasks that need to modify the calling scope.
3. Avoid Hardcoding Paths and Settings: Use environment variables, cache variables,
or configuration files to manage paths and tool settings. This makes your CMake
scripts more flexible and adaptable to different environments.
4. Document Reusable Code: Provide clear documentation for any reusable modules,
functions, and macros you create. This will help others (and your future self)
understand their purpose and usage.
5. Testing Reusability: Regularly test the reusability of your modules, functions, and
macros in different projects and environments to ensure they remain adaptable and
functional.

13.4.7 Conclusion

Improving the reusability of CMake code is an essential skill for managing larger and
more complex projects. By modularizing your CMake scripts, leveraging functions and
macros, reusing find modules, and using configuration files, you can ensure that your build
system is flexible, maintainable, and scalable. These strategies will allow you to avoid
code duplication, simplify maintenance, and make your CMake configurations easier to
adapt to different project requirements or environments.
By following the best practices outlined in this section, you can create a build system that
grows with your project and remains efficient and easy to manage over time.
Chapter 14

Cross-Platform Support and


Compatibility

14.1 Writing Cross-Platform CMakeLists.txt for Windows,


Linux, macOS

In Chapter 14 of CMake: The Comprehensive Guide to Managing and Building C++


Projects (From Basics to Mastery), we delve into the best practices and techniques for
writing CMake files that are compatible across different platforms. These platforms
include the most common operating systems used for C++ development: Windows, Linux,
and macOS. Creating cross-platform CMakeLists.txt files can save time, reduce
maintenance overhead, and ensure that your C++ project works seamlessly across
different environments.

487
488

14.1.1 Overview of Cross-Platform CMake

CMake is a powerful tool designed to manage and automate the build process for C++
projects. One of its primary benefits is that it can generate native build files for various
operating systems, including Windows, Linux, and macOS. This allows developers to
write a single CMakeLists.txt file that will work across these platforms, reducing the need
for separate platform-specific build scripts.

However, differences between operating systems (such as file paths, compilers, libraries,
and system tools) require that some platform-specific logic be incorporated into the
CMake configuration. Writing a CMakeLists.txt that works for all platforms involves
detecting the operating system, setting platform-specific flags, and handling
platform-specific dependencies.

14.1.2 Basic Structure of CMakeLists.txt

Regardless of the platform, the basic structure of a CMakeLists.txt file remains consistent.
A typical file includes:

1. Project Declaration: Declares the project and its properties.

project(MyProject VERSION 1.0 LANGUAGES CXX)

2. Minimum CMake Version: Specifies the minimum required version of CMake.

cmake_minimum_required(VERSION 3.10)

3. Source Files: Specifies the source files for the project.


489

add_executable(MyProject main.cpp)

4. Target Link Libraries: Links libraries to the project.

target_link_libraries(MyProject PRIVATE my_library)

5. Platform-Specific Logic: Introduces conditional logic to handle different platforms.

While the basic structure stays the same, platform-specific code can be incorporated as
needed.

14.1.3 Platform-Specific Handling in CMake

1. Detecting the Operating System


CMake provides the CMAKE SYSTEM NAME variable, which can be used to detect
the target platform. The value of CMAKE SYSTEM NAME will be set to ”Windows”,
”Linux”, or ”Darwin” (macOS) depending on the system you're building on. You can
use this variable to conditionally add platform-specific configurations.
Example:

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")


# Windows-specific configuration
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
# Linux-specific configuration
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# macOS-specific configuration
endif()
490

2. Handling Compiler-Specific Flags


Each platform may have different default compilers and flags. For example, gcc or
clang on Linux and macOS, and MSVC (Microsoft Visual Studio Compiler) on
Windows. You can handle compiler-specific options using
CMAKE CXX COMPILER ID.
Example:

if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")


# MSVC-specific flags
target_compile_options(MyProject PRIVATE /W4 /O2)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR
,→ CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
# GCC/Clang-specific flags
target_compile_options(MyProject PRIVATE -Wall -O2)
endif()

3. Setting Compiler Definitions and Options


Different compilers might require different definitions or flags for certain features.
For example, when targeting a Windows platform, it might be necessary to define
macros such as WIN32 or NOMINMAX. On Linux, you might need to enable certain
compiler extensions.
Example:

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")


add_definitions(-DWIN32 -D_NOMINMAX)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_definitions(-DLINUX)
endif()

4. Linking Platform-Specific Libraries


491

Certain libraries may be required for specific platforms. For example, on Linux, you
might need to link to pthread or on macOS, the CoreFoundation library
might be required. You can use find package() or
target link libraries() conditionally based on the operating system.
Example:

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")


find_package(Threads REQUIRED)
target_link_libraries(MyProject PRIVATE Threads::Threads)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
target_link_libraries(MyProject PRIVATE "-framework
,→ CoreFoundation")
endif()

14.1.4 Handling File Paths

File paths are another area where platform-specific differences can occur. While
UNIX-based systems (Linux and macOS) use forward slashes (/) in file paths, Windows
uses backslashes (\). CMake abstracts most of these differences, but there are still places
where platform-specific handling may be needed.

For example, when specifying a list of include directories or source files, CMake
automatically translates paths between Windows and UNIX-style systems. However,
when using third-party tools or working with non-standard file systems, you may need to
ensure paths are written correctly.

Example:
492

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")


set(MY_LIB_PATH "C:\\path\\to\\libs")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_SYSTEM_NAME
,→ STREQUAL "Darwin")
set(MY_LIB_PATH "/path/to/libs")
endif()

14.1.5 Cross-Platform Tools and Libraries

For complex C++ projects, you may need to rely on libraries or tools that need to be
available across platforms. CMake provides find package() to locate and configure
third-party dependencies like Boost, OpenGL, and others. You can use
find package() with version control, and conditionally link to the appropriate
libraries for each platform.

Example:

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")


find_package(Boost REQUIRED)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(PkgConfig REQUIRED)
pkg_check_modules(PC_LIBNAME REQUIRED libname)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
find_package(OpenGL REQUIRED)
endif()
493

14.1.6 CMake Configuration Options

In addition to platform-specific logic within the CMakeLists.txt, it’s a good practice


to define configurable options that users can modify depending on the target platform. You
can use the option() function to create flags that users can set when configuring the
project.
Example:

option(USE_SDL "Use SDL2 for graphics" ON)


if(USE_SDL)
find_package(SDL2 REQUIRED)
target_link_libraries(MyProject PRIVATE SDL2::SDL2)
endif()

By default, the option is set to ON, but users can override it at configuration time.

14.1.7 Best Practices for Cross-Platform CMake

To summarize the essential points for writing cross-platform CMakeLists.txt files:

1. Use CMake's built-in platform detection: Rely on CMAKE SYSTEM NAME and
CMAKE CXX COMPILER ID to handle platform-specific logic.
2. Minimize platform-specific logic: Keep platform-specific code to a minimum to
maintain portability. Try to use cross-platform libraries whenever possible.
3. Keep paths platform-agnostic: Always rely on CMake’s path-handling features,
such as CMAKE CURRENT LIST DIR, to ensure proper path management.
4. Test on all target platforms: The only way to guarantee that your CMakeLists.txt
will work across all platforms is by testing it on all of them (Windows, Linux, and
macOS).
494

By following these practices and incorporating platform-specific adjustments where


needed, you can create a truly cross-platform CMake configuration that ensures your
project builds and runs seamlessly across Windows, Linux, and macOS environments.

14.1.8 Conclusion

Writing cross-platform CMakeLists.txt files requires understanding the nuances and


differences between operating systems. By leveraging CMake's powerful platform
detection and configuration capabilities, you can automate the build process across
Windows, Linux, and macOS with minimal platform-specific code. This approach reduces
the need for maintaining separate build scripts for each platform and streamlines your
development workflow.
495

14.2 Handling Platform-Specific Library Differences


(#ifdef WIN32, #ifdef linux )

In Chapter 14 of CMake: The Comprehensive Guide to Managing and Building C++


Projects (From Basics to Mastery), we explore how to handle platform-specific library
differences using preprocessor directives like #ifdef WIN32 for Windows and
#ifdef linux for Linux. These preprocessor directives allow you to conditionally
compile platform-specific code and manage different libraries, headers, and configurations
that are unique to each operating system. Handling these differences in a consistent and
maintainable way is a crucial aspect of writing cross-platform code that works seamlessly
across different platforms.

14.2.1 Overview of Platform-Specific Code in C++

When developing a C++ application that targets multiple platforms, you may encounter
platform-specific libraries and APIs that are available only on certain operating systems.
For example, Windows may require Windows-specific libraries (such as the Windows API
or COM libraries), while Linux may use libraries such as pthread or X11, and macOS
may require frameworks like Cocoa or CoreFoundation. The goal is to write code
that can conditionally compile depending on the target platform, allowing you to manage
these differences efficiently.

Preprocessor directives like #ifdef WIN32 and #ifdef linux are used to
determine which platform the code is being compiled for, and to include or exclude
specific code snippets depending on the operating system.
496

14.2.2 Using Preprocessor Directives for Platform-Specific Code

The most common way to handle platform-specific code is through the use of
preprocessor directives that check for specific platform macros. These macros are
automatically defined by the compiler based on the platform it’s targeting. For example:

• Windows: WIN32 is defined when compiling for any version of Windows


(including 64-bit and 32-bit versions).

• Linux: linux is defined when compiling on a Linux-based system.

• macOS: APPLE and MACH are defined when compiling on macOS.

This allows you to write conditional code based on the platform, including
platform-specific libraries, headers, and functionality.

Example:

#ifdef _WIN32
// Code specific to Windows
#elif defined(__linux__)
// Code specific to Linux
#elif defined(__APPLE__)
// Code specific to macOS
#endif

In the following sections, we’ll look in more detail at how to use these preprocessor
checks to manage platform-specific library differences and ensure that your project builds
properly on each platform.
497

14.2.3 Platform-Specific Libraries and APIs

1. Windows: Working with Windows-Specific Libraries


Windows has a number of system libraries and APIs that are specific to the platform.
Some common ones include:

• Windows API: This is the native set of APIs that allow programs to interact
with the operating system for tasks like window management, file I/O, and
networking.
• Windows SDK Libraries: Libraries like Windows.h provide access to
system-level functionality.
• DirectX: A collection of APIs for handling multimedia, particularly game
development and graphical interfaces.

When you write cross-platform code that targets Windows, you may need to use
specific libraries or system calls available only on Windows. For example, the
Windows.h header must be included when working with the Windows API.
Example:

#ifdef _WIN32
#include <Windows.h>
// Use Windows-specific functionality
void win_specific_function() {
// Windows-specific API call, e.g., CreateFile
}
#endif

In addition to including Windows-specific headers, you may need to handle


compiler-specific macros for Microsoft compilers. For example, WIN32 or
MSC VER can be used to check for the Microsoft Visual C++ compiler.
498

2. Linux: Working with Linux-Specific Libraries


Linux has its own set of libraries and APIs, which can differ from platform to
platform (e.g., between Ubuntu and Fedora). Common libraries used in Linux
development include:

• POSIX Libraries: These provide a standard set of APIs for Unix-like operating
systems, including file I/O, networking, threading, etc.
• pthread Library: Used for multithreading support in Linux.
• X11: A protocol and set of libraries used for GUI applications in Unix-like
systems.
• libdl: Used for dynamically loading shared libraries.

When writing cross-platform code that targets Linux, you will often need to include
POSIX headers, pthread for multithreading, or X11 for GUI applications. These
libraries are not available on Windows, so conditional compilation is required.
Example:

#ifdef __linux__
#include <pthread.h>
// Linux-specific threading functionality
void linux_specific_function() {
// Use POSIX threading functions
pthread_t thread;
pthread_create(&thread, NULL, some_function, NULL);
}
#endif

Additionally, libraries like libm (math library) or libdl may need to be linked on
Linux to ensure the program works properly.
3. macOS: Working with macOS-Specific Frameworks
499

macOS development often involves working with Apple-specific frameworks like


Cocoa, CoreFoundation, and CoreGraphics. These frameworks are part of macOS’s
proprietary environment, and code that depends on them will not compile on
Windows or Linux.
To ensure your code is portable across platforms, you must conditionally include
macOS-specific headers and link macOS frameworks only when compiling on
macOS.
Example:

#ifdef __APPLE__
#include <CoreFoundation/CoreFoundation.h>
// macOS-specific functionality
void macos_specific_function() {
// Use macOS-specific framework APIs
CFStringRef myStr =
,→ CFStringCreateWithCString(kCFAllocatorDefault, "Hello,
,→ macOS", kCFStringEncodingUTF8);
}
#endif

macOS-specific frameworks are typically linked during the CMake configuration


using find library() or target link libraries().
Example for linking with CoreFoundation:

if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")


target_link_libraries(MyProject PRIVATE "-framework
,→ CoreFoundation")
endif()
500

14.2.4 Combining Platform-Specific Code with CMake

CMake allows you to manage these platform-specific code differences seamlessly. By


integrating preprocessor checks directly into the CMake configuration, you can ensure that
platform-specific headers and libraries are included properly during the build process.

CMake provides the ability to set compile definitions and flags for each platform. For
example:

• On Windows, you can set the WIN32 macro to ensure Windows-specific code is
included.

• On Linux, the linux macro is defined, so Linux-specific functionality can be


added.

• On macOS, the APPLE macro can be used to include macOS-specific code.

Example:

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")


add_definitions(-D_WIN32)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_definitions(-D__linux__)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
add_definitions(-D__APPLE__)
endif()

This ensures that each platform will correctly identify its own specific preprocessor
macros and conditionally compile the appropriate sections of code.
501

14.2.5 Managing External Dependencies with Platform-Specific


Requirements

When dealing with external libraries or tools that are platform-specific, such as GUI
libraries or system APIs, CMake’s find package() and find library()
commands are invaluable. You can use these commands in conjunction with platform
checks to automatically link platform-specific libraries during the build process.

Example:

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")


find_package(WindowsSDK REQUIRED)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(Threads REQUIRED)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
find_package(OpenGL REQUIRED)
endif()

This ensures that the right libraries are found and linked depending on the platform the
user is building on.

14.2.6 Conclusion

Handling platform-specific library differences is an essential aspect of writing portable


C++ code. By leveraging preprocessor directives like #ifdef WIN32, #ifdef
linux , and #ifdef APPLE , you can include platform-specific code only when
necessary, keeping your project cross-platform and reducing complexity.

When working with external dependencies, CMake’s platform-specific detection features


combined with the appropriate conditional compilation help ensure that only the necessary
502

libraries and headers are included for each target platform. By following best practices
and understanding how to manage platform-specific differences, you can create robust,
cross-platform C++ projects that compile and run seamlessly across Windows, Linux, and
macOS.
503

14.3 Building Multi-Platform Applications

In Chapter 14 of CMake: The Comprehensive Guide to Managing and Building C++


Projects (From Basics to Mastery), we dive into the complexities and strategies behind
building multi-platform applications using CMake. Multi-platform applications are crucial
for developers who want their projects to run across various operating systems, such as
Windows, Linux, and macOS, without requiring separate codebases or build
configurations. CMake is an invaluable tool in this context, enabling cross-platform
compatibility by abstracting the differences between operating systems and providing an
automated, unified build process.

This section discusses the various challenges, techniques, and best practices for building
applications that can target multiple platforms seamlessly using CMake.

14.3.1 Overview of Multi-Platform Development

Building multi-platform applications involves addressing several challenges:

• Platform-Specific Code: As discussed earlier, different platforms provide different


libraries, system calls, and APIs. Writing platform-agnostic code or using
conditional compilation (#ifdef) helps manage these differences.

• Cross-Platform Toolchains: Compiling code across various platforms requires


using the right compilers, build systems, and tools suited for each target platform.
CMake provides a way to configure different toolchains for different platforms.

• External Dependencies: Projects often depend on third-party libraries and


frameworks. These dependencies may behave differently or need to be linked
differently on various platforms.
504

• Testing and Debugging: Ensuring the application works across all platforms can be
a challenge, as bugs may only manifest on certain systems. Testing must be done on
each supported platform to ensure consistency.

By using CMake's cross-platform capabilities, developers can write a single


CMakeLists.txt configuration that manages these challenges, allowing the same
codebase to be compiled and run on multiple operating systems with minimal adjustments.

14.3.2 Setting Up CMake for Multi-Platform Builds

CMake can generate build systems for various platforms, including Visual Studio on
Windows, Makefiles on Linux, and Xcode projects on macOS. One of the keys to
multi-platform development is ensuring that CMake is configured correctly to work with
the platform-specific tools and dependencies.

1. Defining Cross-Platform CMake Configuration


To start building a multi-platform application, you'll typically define a generic
CMakeLists.txt file. This file may contain platform-specific logic to ensure that
the correct tools, libraries, and settings are used depending on the target platform.
Example:

cmake_minimum_required(VERSION 3.10)

# Declare the project and its languages


project(MyMultiPlatformApp VERSION 1.0 LANGUAGES CXX)

# Detect the platform


if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
# Platform-specific configurations for Windows
505

add_definitions(-D_WINDOWS)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
# Platform-specific configurations for Linux
add_definitions(-D_LINUX)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# Platform-specific configurations for macOS
add_definitions(-D_MACOS)
endif()

# Add the source files


add_executable(MyMultiPlatformApp main.cpp)

# Link platform-specific libraries (for example, pthread on


,→ Linux)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_libraries(MyMultiPlatformApp PRIVATE pthread)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
target_link_libraries(MyMultiPlatformApp PRIVATE "-framework
,→ CoreFoundation")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_link_libraries(MyMultiPlatformApp PRIVATE
,→ windows_specific_lib)
endif()

2. Configuring Toolchains for Different Platforms


CMake supports different toolchains for different platforms, allowing developers to
specify which compilers, flags, and other settings to use. You can create a custom
CMake toolchain file for each platform, or rely on CMake’s built-in detection
mechanisms.
• Toolchain File for Windows: On Windows, CMake uses Visual Studio or
MinGW (for GCC) to generate build systems. You can specify a toolchain file
506

that defines the compiler, architecture, and additional settings.

Example:

# Windows toolchain file (windows_toolchain.cmake)


set(CMAKE_CXX_COMPILER "C:/Path/To/VisualStudio/clang++.exe")
set(CMAKE_C_COMPILER "C:/Path/To/VisualStudio/clang.exe")

• Toolchain File for Linux: For Linux, you typically use GCC or Clang as the
compiler. A toolchain file can be used to configure the appropriate settings.

Example:

# Linux toolchain file (linux_toolchain.cmake)


set(CMAKE_CXX_COMPILER "/usr/bin/g++")
set(CMAKE_C_COMPILER "/usr/bin/gcc")

CMake can then be configured to use the right toolchain based on the platform by
specifying the toolchain file during the CMake generation step.

cmake -DCMAKE_TOOLCHAIN_FILE=windows_toolchain.cmake ..

3. Configuring CMake for IDEs (Xcode, Visual Studio, etc.)


CMake can also generate build files for different Integrated Development
Environments (IDEs). This is especially useful for projects that need to be built and
tested across different environments. By default, CMake generates Makefiles, but it
also supports Xcode, Visual Studio, and other IDEs. This is helpful for building
multi-platform applications where developers may need to work in a specific IDE
depending on the platform.
Example:
507

# For macOS, generating Xcode project files


cmake -G "Xcode" ..
# For Windows, generating Visual Studio project files
cmake -G "Visual Studio 16 2019" ..

14.3.3 Managing Multi-Platform Dependencies

One of the most challenging aspects of multi-platform development is managing


platform-specific dependencies. These dependencies may include third-party libraries or
system libraries that have different names, locations, or installation procedures on each
platform.

1. Using CMake's find package() and find library()


CMake provides commands like find package() and find library() to
locate external libraries on different platforms. When building a multi-platform
application, it’s essential to use these commands to ensure that the correct libraries
are found, regardless of the platform.
Example of cross-platform library search:

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")


find_package(SpecialLibrary REQUIRED)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(LinuxLib REQUIRED)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
find_package(MacOSLib REQUIRED)
endif()

In cases where the library locations differ per platform, CMake provides the HINTS
or PATHS options, allowing you to specify custom paths.
508

Example:

find_package(SpecialLibrary REQUIRED HINTS "/path/to/library")

2. Managing Conditional Linking


Sometimes, libraries are linked conditionally based on the platform. For example, on
Linux, you may need to link pthread for multithreading, but on Windows, you
may need to link windows threading.lib. You can use conditional logic in
your CMakeLists.txt file to handle this.
Example:

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")


target_link_libraries(MyMultiPlatformApp PRIVATE pthread)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_link_libraries(MyMultiPlatformApp PRIVATE
,→ windows_threading)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
target_link_libraries(MyMultiPlatformApp PRIVATE "-framework
,→ CoreFoundation")
endif()

14.3.4 Platform-Specific Features and Optimizations

For performance reasons, some platform-specific optimizations may be necessary. For


example, Windows users might benefit from using the Windows Performance API,
while Linux users could take advantage of Linux-specific tools like gprof or perf.

CMake allows you to specify platform-specific flags or settings for compilers or linkers.
This includes using compiler-specific optimizations (e.g., /O2 for MSVC, -O3 for
509

GCC/Clang) or enabling/disabling certain features that are only available or efficient on


specific platforms.
Example of setting compiler flags:

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")


set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O2")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")
endif()

14.3.5 Continuous Integration (CI) for Multi-Platform Builds

To ensure that your multi-platform application is always built correctly across all
supported platforms, it’s a good idea to set up a Continuous Integration (CI) pipeline. CI
tools such as GitHub Actions, GitLab CI, or Jenkins can automatically build your
application for different platforms in virtualized environments.
By setting up different build configurations for each platform (e.g., Windows, Linux, and
macOS), CI ensures that changes to the codebase are tested in all environments, reducing
the chances of cross-platform bugs.
Example CI configuration:

jobs:
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Setup CMake
uses: cschwarz/setup-cmake@v1
510

- run: cmake -G "Visual Studio 16 2019" ..


- run: cmake --build . --config Release

linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup CMake
uses: cschwarz/setup-cmake@v1
- run: cmake ..
- run: cmake --build .

14.3.6 Conclusion

Building multi-platform applications with CMake requires careful management of


platform-specific dependencies, compiler flags, and external libraries. By leveraging
CMake's powerful tools for cross-platform configuration, conditional compilation, and
dependency management, developers can create a unified project that can be built and run
on Windows, Linux, and macOS without maintaining separate codebases or complex build
scripts for each platform.
By following the best practices and strategies outlined in this section, developers can
streamline the process of building and maintaining multi-platform applications, ensuring a
smoother development cycle and providing consistent experiences across different
operating systems.
Chapter 15

CMake and Different Compilers

15.1 Working with GCC, Clang, and MSVC

15.1.1 Introduction to Compiler Support in CMake

CMake is designed to be compiler-agnostic, allowing developers to configure, build, and


manage C++ projects using different compilers like GCC (GNU Compiler Collection),
Clang (LLVM-based), and MSVC (Microsoft Visual C++ Compiler). These compilers
have their own flags, optimizations, and quirks, and CMake provides mechanisms to
detect and configure them appropriately.
This section covers:

• How CMake detects and selects a compiler.


• Using CMake with GCC, Clang, and MSVC.
• Setting compiler-specific flags and options.
• Managing compiler compatibility issues.

511
512

15.1.2 How CMake Detects and Selects a Compiler

When CMake is run in an empty build directory, it attempts to automatically detect the
system’s default C and C++ compilers. The compiler selection process follows these steps:

1. CMake checks for user-specified compilers

• If CMAKE C COMPILER or CMAKE CXX COMPILER is set, CMake uses the


specified compilers.
• These can be set via command-line arguments:

cmake -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ ..

2. If not specified, CMake searches for available compilers

• On Linux/macOS, it typically finds GCC or Clang.


• On Windows, it may detect MSVC (Visual Studio), MinGW GCC, or Clang.

3. CMake stores the compiler selection in the cache (CMakeCache.txt)

• To change compilers after initial configuration, clear the cache:

rm -rf CMakeCache.txt CMakeFiles/

To check which compiler CMake has selected:

cmake --version

or inside the CMake project:


513

message(STATUS "C++ Compiler: ${CMAKE_CXX_COMPILER}")

15.1.3 Working with GCC (GNU Compiler Collection)

1. Overview of GCC
GCC is the standard compiler for Linux and is available on Windows (via
MinGW/MSYS2) and macOS. It is widely used for open-source projects and
supports various C++ standards.

2. Setting Up CMake for GCC


To explicitly use GCC with CMake:

cmake -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ ..

CMake automatically detects GCC and applies appropriate settings.

3. Configuring GCC-Specific Compiler Flags


To set optimization levels, warnings, and other GCC-specific flags:

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")


target_compile_options(MyProject PRIVATE -Wall -Wextra -O2)
endif()

• -Wall -Wextra: Enables extra warnings.


• -O2: Optimizes for speed without excessive compilation time.

4. Example: Building a Project with GCC


514

cmake_minimum_required(VERSION 3.10)
project(MyProject)

add_executable(MyProject main.cpp)

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")


target_compile_options(MyProject PRIVATE -Wall -Wextra -O2)
endif()

To compile with GCC:

cmake -G "Unix Makefiles" -DCMAKE_C_COMPILER=gcc


,→ -DCMAKE_CXX_COMPILER=g++ ..
cmake --build .

15.1.4 Working with Clang (LLVM)

1. Overview of Clang
Clang is an alternative to GCC and is widely used for:
• macOS development (Xcode uses Clang by default).
• Linux (many distributions offer Clang as an alternative to GCC).
• Windows (Clang is supported in Visual Studio).
2. Setting Up CMake for Clang
To configure CMake to use Clang:

cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ ..

If using Clang with MinGW on Windows:


515

cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++


,→ -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" ..

3. Configuring Clang-Specific Compiler Flags


Clang shares many flags with GCC but offers additional tools like clang-tidy
and scan-build for static analysis.
Example of setting Clang-specific flags in CMake:

if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")


target_compile_options(MyProject PRIVATE -Wall -Wextra
,→ -Wpedantic -O2)
endif()

• -Wpedantic: Enables strict compliance with the C++ standard.

4. Example: Building a Project with Clang

cmake -G "Unix Makefiles" -DCMAKE_C_COMPILER=clang


,→ -DCMAKE_CXX_COMPILER=clang++ ..
cmake --build .

15.1.5 Working with MSVC (Microsoft Visual C++ Compiler)

1. Overview of MSVC
MSVC is the standard compiler for Windows development and is integrated with
Visual Studio. It has strong support for C++ standards and Microsoft-specific
optimizations.
516

2. Setting Up CMake for MSVC


If Visual Studio is installed, CMake can generate a Visual Studio project:

cmake -G "Visual Studio 16 2019" ..

To build with MSVC from the command line:

cmake --build . --config Release

MSVC supports /MT and /MD flags for static and dynamic linking.
3. Configuring MSVC-Specific Compiler Flags

if(MSVC)
target_compile_options(MyProject PRIVATE /W4 /O2
,→ /permissive-)
endif()

• /W4: Enables additional warnings.


• /O2: Optimizes for speed.
• /permissive-: Ensures stricter C++ standard compliance.

4. Example: Building a Project with MSVC

cmake -G "Visual Studio 16 2019" ..


cmake --build . --config Release

15.1.6 Managing Cross-Compiler Compatibility

1. Detecting the Compiler in CMake


517

CMake provides built-in variables to detect the compiler:

message(STATUS "C++ Compiler: ${CMAKE_CXX_COMPILER}")


message(STATUS "Compiler ID: ${CMAKE_CXX_COMPILER_ID}")
message(STATUS "Compiler Version: ${CMAKE_CXX_COMPILER_VERSION}")

2. Handling Compiler-Specific Features


To use specific features depending on the compiler:

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")


target_compile_options(MyProject PRIVATE -Wno-deprecated)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
target_compile_options(MyProject PRIVATE -Wshadow)
elseif(MSVC)
target_compile_options(MyProject PRIVATE /bigobj)
endif()

3. Ensuring Code Portability

• Use C++ standard libraries instead of platform-specific ones.


• Use CMake’s built-in functions to manage dependencies instead of hardcoding
paths.
• Test on multiple platforms using Continuous Integration (CI).

15.1.7 Conclusion

CMake makes it easy to work with multiple compilers by automatically detecting and
configuring compiler settings. By understanding the differences between GCC, Clang,
and MSVC, and how to set compiler-specific flags in CMake, developers can ensure that
their projects compile and run correctly on all platforms.
518

Key Takeaways:

• Use CMAKE C COMPILER and CMAKE CXX COMPILER to specify the compiler.
• Use if(CMAKE CXX COMPILER ID STREQUAL "...") to apply
compiler-specific settings.
• Use GCC for open-source projects, Clang for performance and static analysis,
and MSVC for Windows development.

This knowledge is essential for writing cross-platform C++ projects that work
seamlessly across Windows, Linux, and macOS.
519

15.2 Configuring Compilation Flags (CMAKE CXX FLAGS)

15.2.1 Introduction to Compilation Flags in CMake

Compilation flags control how source code is translated into machine code by the
compiler. These flags influence optimization levels, debugging information, warning
levels, standard compliance, and more.

CMake provides various ways to specify compilation flags, including:

• Global flags (CMAKE CXX FLAGS)

• Target-specific flags (target compile options())

• Build type-specific flags (CMAKE CXX FLAGS DEBUG,


CMAKE CXX FLAGS RELEASE)

In this section, we explore how to configure and manage these flags for different compilers
like GCC, Clang, and MSVC.

15.2.2 Setting Global Compilation Flags

CMake provides the CMAKE CXX FLAGS variable to set global compiler flags that apply
to all targets.

Example of setting global flags in CMakeLists.txt:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic")

• -Wall: Enables most common warnings.


520

• -Wextra: Enables additional warnings.


• -Wpedantic: Ensures strict compliance with the C++ standard.

Note: This approach affects all C++ targets in the project. If different targets
require different flags, target compile options() is recommended.

15.2.3 Setting Build-Type Specific Flags

CMake allows setting different compilation flags based on the build type (e.g., Debug,
Release, RelWithDebInfo).
By default, CMake provides these variables:

• CMAKE CXX FLAGS DEBUG (Flags for Debug builds)


• CMAKE CXX FLAGS RELEASE (Flags for Release builds)
• CMAKE CXX FLAGS RELWITHDEBINFO (Flags for Release builds with
debugging info)
• CMAKE CXX FLAGS MINSIZEREL (Flags for optimized builds with minimal size)

Example:

set(CMAKE_CXX_FLAGS_DEBUG "-g -O0") # Debug: No optimization,


,→ include debug symbols
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG") # Release: Optimize for
,→ speed, disable assertions
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g") # Optimize but keep
,→ debug info

To select a build type:


521

cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .

15.2.4 Setting Compiler-Specific Flags

Different compilers require different flags for optimization, warnings, and features.
CMake provides CMAKE CXX COMPILER ID to detect the compiler and apply specific
flags.

1. Detecting the Compiler


To determine which compiler is being used:

message(STATUS "Compiler: ${CMAKE_CXX_COMPILER_ID}")

2. GCC-Specific Flags
Example:

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")


set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra
,→ -Wpedantic -O2")
endif()

3. Clang-Specific Flags
Example:

if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")


set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Weverything -O2
,→ -march=native")
endif()
522

• -Weverything: Enables almost all warnings.


• -march=native: Optimizes for the current CPU architecture.

4. MSVC-Specific Flags
Example:

if(MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /O2 /EHsc")
endif()

• /W4: Enables strict warnings.


• /O2: Enables optimizations.
• /EHsc: Enables standard exception handling.

15.2.5 Using target compile options() (Recommended)

Instead of modifying CMAKE CXX FLAGS, a better practice is to set flags per target using
target compile options().

Example:

add_executable(MyApp main.cpp)

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")


target_compile_options(MyApp PRIVATE -Wall -Wextra -O2)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
target_compile_options(MyApp PRIVATE -Weverything -O2)
elseif(MSVC)
523

target_compile_options(MyApp PRIVATE /W4 /O2)


endif()

Advantages of target compile options()


• Avoids modifying global flags (CMAKE CXX FLAGS).
• Allows different targets to have different flags.
• Provides better control over project configurations.

15.2.6 Using add compile definitions() for Preprocessor Flags

For preprocessor definitions, use add compile definitions().


Example:

add_compile_definitions(MY_FEATURE_ENABLED)

For compiler-specific defines:

if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")


add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
endif()

15.2.7 Setting Linker Flags (CMAKE EXE LINKER FLAGS)

Compilation flags control how code is compiled, but linker flags control how the final
binary is generated.
Set linker flags globally:
524

set(CMAKE_EXE_LINKER_FLAGS "-Wl,-O1")

Or for specific targets:

target_link_options(MyApp PRIVATE -Wl,-O1)

15.2.8 Example: Complete CMakeLists.txt with Compiler-Specific


Flags

cmake_minimum_required(VERSION 3.10)
project(MyApp)

add_executable(MyApp main.cpp)

# Set warnings and optimizations per compiler


if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(MyApp PRIVATE -Wall -Wextra -Wpedantic
,→ -O2)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
target_compile_options(MyApp PRIVATE -Weverything -O2)
elseif(MSVC)
target_compile_options(MyApp PRIVATE /W4 /O2)
endif()

# Set preprocessor definitions


add_compile_definitions(ENABLE_LOGGING)

# Set linker flags


target_link_options(MyApp PRIVATE "-Wl,-O1")
525

To configure and build:

cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .

15.2.9 Conclusion

• Use CMAKE CXX FLAGS for global flags, but prefer


target compile options().
• Use CMAKE CXX FLAGS DEBUG and CMAKE CXX FLAGS RELEASE for
build-type-specific flags.
• Detect the compiler with CMAKE CXX COMPILER ID and set appropriate flags.
• Use target link options() for linker settings.

By managing compilation flags properly, you can ensure better performance, debugging,
and portability across different compilers and platforms.
526

15.3 Checking Compiler Feature Support


(CheckCXXCompilerFlag)

15.3.1 Introduction to Compiler Feature Checking

Different C++ compilers support various flags and features that impact optimizations,
warnings, debugging, and compliance with C++ standards. However, not all compilers
support the same flags, and some flags may change between compiler versions.
To write portable and robust CMake configurations, it's crucial to check whether a
compiler supports specific flags before applying them. CMake provides the
check cxx compiler flag() function to test if a compiler supports a given flag.
This section covers:

• The importance of checking compiler flag support.


• Using check cxx compiler flag().
• Applying feature checks to set safe compiler options.
• Alternative methods like CMAKE CXX STANDARD and
CheckCXXSourceCompiles.

15.3.2 Why Check Compiler Flag Support?

Compiler flags may vary across:

• Different compilers (GCC, Clang, MSVC).


• Different versions of the same compiler (e.g., GCC 7 vs. GCC 11).
• Different operating systems (Windows, Linux, macOS).
527

If an unsupported flag is used, the compiler may issue warnings or errors, breaking the
build. Checking compiler support ensures: Portability – The same CMake project builds
across different compilers.
Robustness – No unexpected errors due to unsupported flags.
Better debugging and performance – Enables compiler-specific optimizations where
available.

15.3.3 Using check cxx compiler flag()

CMake provides the check cxx compiler flag() function to test if a specific C++
compiler flag is supported.

Basic Syntax

include(CheckCXXCompilerFlag)

check_cxx_compiler_flag("-fstack-protector-strong"
,→ STACK_PROTECTOR_SUPPORTED)

if(STACK_PROTECTOR_SUPPORTED)
message(STATUS "Compiler supports -fstack-protector-strong")
else()
message(STATUS "Compiler does NOT support
,→ -fstack-protector-strong")
endif()

• "-fstack-protector-strong": The flag to test.

• STACK PROTECTOR SUPPORTED: A variable that stores TRUE if the flag is


supported, otherwise FALSE.
528

Note: This check is performed at configuration time, meaning that CMake


will not proceed to compilation if an unsupported flag is added.

15.3.4 Applying Flag Checks in CMake

Once a flag is verified as supported, it can be conditionally added to compiler options.

1. Example: Adding a Safe Compilation Flag

include(CheckCXXCompilerFlag)

check_cxx_compiler_flag("-march=native" SUPPORTS_MARCH_NATIVE)

if(SUPPORTS_MARCH_NATIVE)
target_compile_options(MyProject PRIVATE -march=native)
endif()

This ensures:

• If -march=native is supported, it is applied.


• If not supported, it is not added, preventing errors.

2. Example: Setting Multiple Compiler Flags Safely

include(CheckCXXCompilerFlag)

check_cxx_compiler_flag("-Wall" SUPPORTS_WALL)
check_cxx_compiler_flag("-Wextra" SUPPORTS_WEXTRA)
check_cxx_compiler_flag("-Wpedantic" SUPPORTS_WPEDANTIC)

add_executable(MyApp main.cpp)
529

if(SUPPORTS_WALL)
target_compile_options(MyApp PRIVATE -Wall)
endif()

if(SUPPORTS_WEXTRA)
target_compile_options(MyApp PRIVATE -Wextra)
endif()

if(SUPPORTS_WPEDANTIC)
target_compile_options(MyApp PRIVATE -Wpedantic)
endif()

Why is this better than blindly adding flags?


It avoids incompatibility issues where a compiler does not support a
specific flag, preventing build failures.

15.3.5 Checking Flags for Specific Compilers

Since not all compilers support the same flags, check cxx compiler flag() can be
combined with CMAKE CXX COMPILER ID to apply flags per compiler.

include(CheckCXXCompilerFlag)

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")


check_cxx_compiler_flag("-fstack-clash-protection"
,→ SUPPORTS_STACK_CLASH)
if(SUPPORTS_STACK_CLASH)
target_compile_options(MyApp PRIVATE
,→ -fstack-clash-protection)
endif()
530

elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")


check_cxx_compiler_flag("-fsanitize=undefined" SUPPORTS_UBSAN)
if(SUPPORTS_UBSAN)
target_compile_options(MyApp PRIVATE -fsanitize=undefined)
endif()
elseif(MSVC)
check_cxx_compiler_flag("/permissive-" SUPPORTS_MSVC_STRICT)
if(SUPPORTS_MSVC_STRICT)
target_compile_options(MyApp PRIVATE /permissive-)
endif()
endif()

This approach:

• Ensures compiler flags are checked only for relevant compilers.


• Avoids cross-compiler errors when using flags that are specific to one compiler.

15.3.6 Checking Compiler Features Instead of Flags

Instead of checking flags, you can check compiler support for a language feature (e.g.,
C++17, C++20).

1. Using CMAKE CXX STANDARD for C++ Features


To ensure the compiler supports a C++ standard, use:

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

• CMAKE CXX STANDARD 17: Requires C++17 or higher.


531

• CMAKE CXX STANDARD REQUIRED ON: Forces an error if C++17 is not


supported.
• CMAKE CXX EXTENSIONS OFF: Ensures standard compliance (no
compiler-specific extensions).
2. Using CheckCXXSourceCompiles for Feature Checks
For finer control, use CheckCXXSourceCompiles to check if a code snippet
compiles successfully.
Example: Checking for std::filesystem (C++17 feature)

include(CheckCXXSourceCompiles)

check_cxx_source_compiles("
#include <filesystem>
int main() { std::filesystem::path p; return 0; }
" SUPPORTS_FILESYSTEM)

if(SUPPORTS_FILESYSTEM)
message(STATUS "Compiler supports std::filesystem")
else()
message(WARNING "Compiler does NOT support std::filesystem,
,→ falling back to boost::filesystem")
endif()

This method is useful for checking language feature availability before using them
in the project.

15.3.7 Alternative: CheckCXXCompilerFlag vs. try compile()

Another way to check for compiler support is using try compile(), which compiles a
small test program.
532

Example:

try_compile(
COMPILE_SUCCESS
${CMAKE_BINARY_DIR}
SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/test_flag.cpp
)

if(COMPILE_SUCCESS)
message(STATUS "Test program compiled successfully")
else()
message(STATUS "Test program failed to compile")
endif()

While more flexible, try compile() is slower and requires a test source file.

15.3.8 Conclusion

Using check cxx compiler flag() ensures that only supported compiler flags
are applied, improving portability and reliability across different compilers.

Key Takeaways
Use check cxx compiler flag() to test flag support before adding it.
Combine with CMAKE CXX COMPILER ID to check flags per compiler.
Use CMAKE CXX STANDARD for standard compliance.
Use check cxx source compiles() for checking language features.
Avoid using unsupported flags to prevent build failures and compatibility issues.
By applying these techniques, you can create highly portable, compiler-agnostic C++
projects with CMake!
533

15.4 Handling Compiler-Specific Errors and Warnings

15.4.1 Introduction

Compiler errors and warnings provide crucial feedback when building C++ projects.
Different compilers—GCC, Clang, and MSVC—have their own ways of reporting
issues, and sometimes a warning in one compiler may be an error in another. Managing
these warnings and errors consistently across multiple platforms ensures:
Better code quality
Easier debugging and troubleshooting
Greater portability across compilers

In this section, we explore:

• The difference between errors and warnings.

• How to enable stricter warnings across different compilers.

• How to treat warnings as errors (-Werror, /WX).

• Handling compiler-specific errors with preprocessor directives.

• Suppressing unwanted warnings.

15.4.2 Understanding Compiler Errors and Warnings

Compiler errors indicate issues that prevent compilation (e.g., syntax errors, type
mismatches). Compiler warnings indicate potential problems but do not stop compilation
(e.g., unused variables, implicit conversions).

Examples:
534

// Warning: Implicit conversion from double to int


int x = 5.7; // Warning in GCC/Clang: "conversion from 'double' to
,→ 'int' changes value"

void unusedFunction() { } // Warning: "unused function"

Most compilers allow configuring how warnings are handled—they can be ignored,
enabled selectively, or elevated to errors.

15.4.3 Enabling Stricter Warnings

To write cleaner, more robust code, enable warnings in CMake for different compilers.

1. GCC and Clang Warnings


GCC and Clang share many common warning flags:

if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")


target_compile_options(MyApp PRIVATE -Wall -Wextra
,→ -Wpedantic)
endif()

• -Wall: Enables most common warnings.


• -Wextra: Enables additional warnings.
• -Wpedantic: Enforces strict standard compliance.

2. MSVC Warnings
MSVC has a different warning flag system:
535

if(MSVC)
target_compile_options(MyApp PRIVATE /W4)
endif()

• /W1, /W2, /W3, /W4: Increasing levels of strictness (/W4 is


recommended).
• /Wall: Enables all warnings (but can be too aggressive).

15.4.4 Treating Warnings as Errors (-Werror, /WX)

For stricter code quality enforcement, convert warnings into errors:

1. GCC and Clang

if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")


target_compile_options(MyApp PRIVATE -Werror)
endif()

• -Werror: Converts all warnings into errors.

2. MSVC

if(MSVC)
target_compile_options(MyApp PRIVATE /WX)
endif()

• /WX: Treats warnings as errors.

Example:
536

void unusedFunction() { } // This would be an error with -Werror


,→ or /WX

Warning: Be cautious when using -Werror or /WX in production, as it


may cause issues with new compiler versions that introduce additional
warnings.

15.4.5 Handling Compiler-Specific Errors with Preprocessor Directives

Sometimes, different compilers generate different types of errors. Use preprocessor


directives (#ifdef) to handle these cases.

Example: Handling MSVC vs. GCC/Clang Differences

#ifdef _MSC_VER
#pragma warning(disable : 4996) // Disable MSVC-specific warning
#elif defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif

• MSC VER: Defined for MSVC.

• GNUC : Defined for GCC.

• clang : Defined for Clang.

This ensures that compiler-specific errors are addressed properly.


537

15.4.6 Suppressing Unwanted Warnings

Some warnings may be overly strict or triggered by third-party libraries. These


warnings can be selectively disabled.

1. Suppressing GCC and Clang Warnings


Use -Wno-<warning-name> to disable specific warnings.

target_compile_options(MyApp PRIVATE -Wno-unused-variable)

Example: Suppressing the unused variable warning:

int main() {
int unusedVar = 42; // Warning suppressed with
,→ -Wno-unused-variable
return 0;
}

2. Suppressing MSVC Warnings


Use /wd<warning-number> to disable specific warnings.

if(MSVC)
target_compile_options(MyApp PRIVATE /wd4996) # Disable
,→ deprecated function warnings
endif()

Example:

#define _CRT_SECURE_NO_WARNINGS // Suppresses MSVC warnings


,→ about unsafe functions
#include <stdio.h>
538

int main() {
printf("Hello, World!\n"); // MSVC would normally warn about
,→ unsafe `printf`
return 0;
}

15.4.7 Using target compile options() vs. CMAKE CXX FLAGS

Although CMAKE CXX FLAGS can be used to set warnings globally,


target compile options() is preferred because:
It applies only to specific targets.
It avoids unintended conflicts with third-party libraries.

Example:

add_executable(MyApp main.cpp)

if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")


target_compile_options(MyApp PRIVATE -Wall -Wextra -Wpedantic)
elseif(MSVC)
target_compile_options(MyApp PRIVATE /W4)
endif()

Best Practice: Apply compiler-specific options per target rather than


modifying global flags.
539

15.4.8 Example: Comprehensive Handling of Compiler Warnings

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyApp)

add_executable(MyApp main.cpp)

# Enable warnings for all compilers


if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(MyApp PRIVATE -Wall -Wextra -Wpedantic
,→ -Werror)
elseif(MSVC)
target_compile_options(MyApp PRIVATE /W4 /WX)
endif()

# Suppress specific warnings


if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(MyApp PRIVATE -Wno-unused-variable)
elseif(MSVC)
target_compile_options(MyApp PRIVATE /wd4996)
endif()

main.cpp

#include <iostream>

#ifdef _MSC_VER
#pragma warning(disable : 4996) // Suppress MSVC deprecated
,→ warnings
540

#elif defined(__GNUC__) || defined(__clang__)


#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif

int main() {
int unusedVar = 42; // Warning suppressed
std::cout << "Hello, World!" << std::endl;
return 0;
}

Result:

• The program will compile without warnings or errors across GCC,


Clang, and MSVC.
• Unused variable warnings will be suppressed.
• Warnings are treated as errors to enforce code quality.

15.4.9 Conclusion

Properly managing compiler warnings and errors ensures clean, portable, and
maintainable code.

Key Takeaways

• Enable strict warnings using -Wall, -Wextra, /W4.

• Treat warnings as errors with -Werror, /WX (use carefully).

• Use #ifdef to handle compiler-specific errors.

• Suppress unwanted warnings only when necessary.


541

• Prefer target compile options() over CMAKE CXX FLAGS.

By following these best practices, you can ensure better code quality and
cross-platform compatibility in your CMake projects!
Chapter 16

Troubleshooting and Debugging CMake


Issues

16.1 Understanding CMake Error Messages

(From Chapter 16: Troubleshooting and Debugging CMake Issues of the book **CMake:
The Comprehensive Guide to Managing and Building C++ Projects (From Basics to
Mastery))*

CMake is a powerful build system generator, but users frequently encounter error
messages when configuring, generating, or building a project. These errors can stem from
misconfigured scripts, missing dependencies, incorrect variable settings, or version
mismatches. Understanding how to interpret and resolve these error messages is crucial
for efficient troubleshooting.

542
543

16.1.1 The Structure of a CMake Error Message

CMake error messages typically follow a consistent structure that helps identify the cause
and location of the issue. A typical error message includes:

• Error Type: Indicates whether it is a fatal error, warning, or deprecation notice.


• File and Line Number: Points to the location in the CMakeLists.txt file or
included .cmake file where the error occurred.
• Description: Provides details about what went wrong.
• Stack Trace (if applicable): Shows the function calls leading to the error.

Example 1: A Simple Syntax Error

CMake Error at CMakeLists.txt:5 (invalid_command):


Unknown CMake command "invalid_command".

• Error Type: CMake Error


• Location: CMakeLists.txt, line 5
• Description: invalid command is not a recognized CMake command.

Example 2: Missing Required Dependency

CMake Error at CMakeLists.txt:10 (find_package):


By not providing "FindBoost.cmake" in CMAKE_MODULE_PATH, CMake is
,→ unable to
find the Boost package.
544

• Error Type: CMake Error

• Location: CMakeLists.txt, line 10

• Description: find package(Boost REQUIRED) failed because Boost is not


found.

16.1.2 Common Categories of CMake Errors

CMake errors can be classified into several categories:

1. Configuration Errors
Errors occurring during the CMake configuration phase (cmake ..) usually
result from incorrect syntax, missing files, or invalid variables.

• Example: Invalid Syntax

CMake Error at CMakeLists.txt:7 (set):


set given unknown argument "-INVALID".

Fix: Ensure the syntax of the set() command is correct.


• Example: Undefined Variable

CMake Error at CMakeLists.txt:12 (message):


Variable MY_VAR is not defined.

Fix: Define MY VAR before using it.

2. Missing Dependencies
Missing packages, libraries, or toolchains can lead to errors.

• Example: Missing C++ Compiler


545

-- The CXX compiler identification is unknown


CMake Error: C++ compiler not found!

Fix: Install a valid C++ compiler and ensure it is in the system’s PATH.
• Example: Package Not Found

CMake Error at CMakeLists.txt:20 (find_package):


Could not find package Eigen3.

Fix: Install Eigen3 and set CMAKE PREFIX PATH to its installation directory.
3. Generator Errors
CMake requires a generator to create build files. If a required generator is missing or
incompatible, errors occur.
• Example: Unsupported Generator

CMake Error: Could not create named generator "NinjaX"

Fix:
Ensure

NinjaX

is installed, or use

cmake --help

to list available generators.


4. Linking Errors
Errors during the build phase (e.g., cmake --build .) often indicate missing
or incorrect library linkages.
546

• Example: Undefined Reference

/usr/bin/ld: CMakeFiles/main.dir/main.cpp.o: undefined


,→ reference to `MyFunction'

Fix:
Ensure the target links against the correct library with

target_link_libraries()

5. Policy and Deprecation Warnings


Newer versions of CMake introduce policies that may deprecate old behavior.
• Example: Deprecated Command

CMake Warning (dev) at CMakeLists.txt:15:


The use of "include_directories()" is deprecated.

Fix:
Replace with

target_include_directories()

16.1.3 Debugging CMake Errors

Here are effective strategies to debug CMake errors:

1. Use cmake --trace


Enabling tracing logs each executed command, helping identify issues.
547

cmake .. --trace

2. Check the CMakeCache.txt File


CMake stores detected configurations in CMakeCache.txt. If a variable is
incorrect, it can be manually edited or deleted.
3. Enable Debug Mode
Using --debug-output provides more verbose messages.

cmake .. --debug-output

4. Verify Dependencies
Ensure dependencies are installed and correctly detected using:

cmake --find-package -DNAME=Boost -DCOMPILER_ID=GNU

16.1.4 Summary

Understanding CMake error messages is essential for efficient debugging. By carefully


reading error messages, classifying them, and using debugging techniques, developers can
quickly resolve issues and improve their CMake workflow. The next sections will explore
specific debugging techniques in depth.
548

16.2 Debugging with message(STATUS) and


message(DEBUG)

(From Chapter 16: Troubleshooting and Debugging CMake Issues of the book **CMake:
The Comprehensive Guide to Managing and Building C++ Projects (From Basics to
Mastery))*

16.2.1 Introduction

CMake provides the message() command, which is a powerful tool for debugging
CMake scripts. It allows developers to print messages during the configuration phase,
helping them inspect variable values, check execution flow, and diagnose issues. Among
the different modes of message(), STATUS and DEBUG are particularly useful for
troubleshooting and debugging.
This section explores how to effectively use message(STATUS) and
message(DEBUG), when to use them, and best practices for debugging CMake scripts.

16.2.2 The message() Command in CMake

The message() function in CMake takes a message type and a string to print. The
syntax is as follows:

message(<mode> "Your message here")

The <mode> defines how the message is displayed. The most commonly used modes
are:
549

Mode Description
STATUS Prints general status messages during configuration.
WARNING Issues a warning but allows execution to continue.
FATAL ERROR Stops configuration immediately with an error.
DEBUG Prints debugging information, but only when
--log-level=DEBUG is used.
VERBOSE Prints messages only when verbose mode is enabled
(--log-level=VERBOSE).

This section focuses on STATUS and DEBUG, which are particularly useful for debugging.

16.2.3 Using message(STATUS) for Debugging

The STATUS mode prints messages to standard output during the configuration phase. It
is used for:

• Checking variable values.

• Confirming execution of specific commands.

• Monitoring CMake execution flow.

1. Checking Variable Values


One of the most common debugging techniques in CMake is to print variable values
using message(STATUS).

Example 1: Printing a Variable Value


550

set(MY_VAR "Hello, CMake!")


message(STATUS "MY_VAR is set to: ${MY_VAR}")

Output:

-- MY_VAR is set to: Hello, CMake!

This helps verify whether a variable is assigned the expected value.

2. Checking Paths and Dependencies


CMake relies heavily on environment variables, paths, and package dependencies. If
an issue occurs due to a missing file or incorrect path, message(STATUS) can be
used to inspect values.

Example 2: Checking CMake Paths

message(STATUS "CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}")


message(STATUS "CMAKE_MODULE_PATH: ${CMAKE_MODULE_PATH}")
message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}")

This helps diagnose issues related to missing dependencies or incorrect installation


prefixes.

3. Confirming Execution Flow


CMake executes commands sequentially, but certain commands may be skipped due
to conditions or policies. Using message(STATUS), you can confirm whether
specific sections of CMakeLists.txt are being executed.

Example 3: Debugging Execution Flow


551

if(DEFINED MY_VAR)
message(STATUS "MY_VAR is defined")
else()
message(STATUS "MY_VAR is NOT defined")
endif()

If MY VAR is undefined, the second message will appear in the output, helping
identify conditional execution issues.

4. Checking Compiler and Toolchain Settings


Compiler settings and toolchains often cause issues, especially when
cross-compiling. You can use message(STATUS) to print detected compilers.

Example 4: Displaying Compiler Information

message(STATUS "CMake detected C++ compiler:


,→ ${CMAKE_CXX_COMPILER}")
message(STATUS "Compiler version: ${CMAKE_CXX_COMPILER_VERSION}")
message(STATUS "C++ standard: ${CMAKE_CXX_STANDARD}")

Output:

-- CMake detected C++ compiler: /usr/bin/g++


-- Compiler version: 11.3.0
-- C++ standard: 17

This helps confirm that the correct compiler and standard are being used.
552

16.2.4 Using message(DEBUG) for More Granular Debugging

While STATUS messages are always printed, DEBUG messages are only shown when the
CMake log level is explicitly set to DEBUG.

1. Enabling Debug Mode


By default, message(DEBUG "...") does not display anything unless CMake
is run with --log-level=DEBUG. To enable debug messages, run:

cmake .. --log-level=DEBUG

2. Example: Printing Debug Information

message(DEBUG "This is a debug message. It will only appear in


,→ debug mode.")

Output (only in debug mode):

[DEBUG] This is a debug message. It will only appear in debug


,→ mode.

Using message(DEBUG) allows keeping debug logs available without cluttering


normal output.

3. Combining STATUS and DEBUG for Effective Debugging


A common approach is to use STATUS for general debugging and DEBUG for
detailed, optional logs.

Example 5: Debugging Variable Issues with Both STATUS and DEBUG


553

set(PROJECT_NAME "MyApp")

message(STATUS "Project Name: ${PROJECT_NAME}")


message(DEBUG "Checking if PROJECT_NAME is properly set...")

When run normally:

-- Project Name: MyApp

When run with --log-level=DEBUG:

-- Project Name: MyApp


[DEBUG] Checking if PROJECT_NAME is properly set...

This approach provides optional verbosity when needed.

16.2.5 Best Practices for Using message(STATUS) and


message(DEBUG)

To make debugging more effective, follow these best practices:

1. Use STATUS for Essential Debugging Messages

• Print key variable values, paths, and settings.


• Confirm execution of important sections in CMakeLists.txt.

2. Use DEBUG for Optional Detailed Logging

• Add deeper insights but only enable them when debugging.


• Prevent cluttering the normal output.
554

3. Prefix Messages for Clarity


Instead of:

message(STATUS "Hello")
message(STATUS "Path is: /usr/local")

Use:

message(STATUS "[INFO] Starting configuration...")


message(STATUS "[PATH] Detected path: ${CMAKE_INSTALL_PREFIX}")

This improves readability.


4. Avoid Excessive message(STATUS) Calls
Overuse can clutter output. Use only when necessary.
5. Use FATAL ERROR for Critical Issues
If a required condition is missing:

if(NOT DEFINED REQUIRED_VAR)


message(FATAL_ERROR "ERROR: REQUIRED_VAR is not defined!")
endif()

16.2.6 Summary

message(STATUS) and message(DEBUG) are essential tools for debugging CMake


projects.

• STATUS is used for general debugging and always prints messages.


• DEBUG provides detailed logs, but only when explicitly enabled.
555

• Using them effectively helps diagnose variable issues, execution flow, dependency
problems, and toolchain configurations.
• Following best practices ensures readable and maintainable debugging output.
556

16.3 Tracking Environment Variables and Build Settings

(From Chapter 16: Troubleshooting and Debugging CMake Issues of the book **CMake:
The Comprehensive Guide to Managing and Building C++ Projects (From Basics to
Mastery))*

16.3.1 Introduction

CMake relies heavily on environment variables and build settings to configure and
generate a project correctly. Incorrectly set environment variables or misconfigured build
settings can lead to errors, missing dependencies, or unexpected behavior.

This section explores how to:

• Track and debug environment variables in CMake.

• Inspect and verify build settings such as compiler flags, linker settings, and build
types.

• Use CMake’s built-in tools to diagnose configuration issues.

By mastering these techniques, developers can troubleshoot complex build problems more
effectively.

16.3.2 Tracking Environment Variables in CMake

CMake interacts with environment variables to:

• Locate compilers, libraries, and tools.

• Set custom installation or build paths.


557

• Influence the behavior of find package() and find program().

1. Checking Environment Variables in CMake


CMake provides the $ENV{VAR NAME} syntax to read environment variables.

Example 1: Printing an Environment Variable

message(STATUS "PATH: $ENV{PATH}")

Output (truncated example):

-- PATH: /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

This helps verify that necessary paths (such as compiler directories) are set.
2. Setting Environment Variables in CMake
Use set(ENV{VAR NAME} VALUE) to modify environment variables during
CMake execution.

Example 2: Modifying the PATH Variable

set(ENV{PATH} "/custom/path:$ENV{PATH}")
message(STATUS "Updated PATH: $ENV{PATH}")

However, note that changes only affect the current CMake run and do not persist
after execution.
3. Important Environment Variables for CMake
Some common environment variables that influence CMake behavior:
558

Environment Variable Purpose


PATH Used to find executables (e.g., compilers, tools).
CMAKE PREFIX PATH Helps find package() locate dependencies.
CMAKE MODULE PATH Specifies additional paths for .cmake modules.
CC, CXX Manually set the C and C++ compilers.
LD LIBRARY PATH Specifies locations of shared libraries at runtime.
PKG CONFIG PATH Helps pkg-config find installed libraries.

4. Debugging Missing Dependencies with CMAKE PREFIX PATH


If find package() fails to locate a dependency, check CMAKE PREFIX PATH.

Example 3: Checking CMAKE PREFIX PATH

message(STATUS "CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}")

If the output is empty, manually set it:

export CMAKE_PREFIX_PATH="/custom/install/path"

or in CMakeLists.txt:

set(CMAKE_PREFIX_PATH "/custom/install/path")

This helps CMake find libraries installed in non-standard locations.

5. Listing All Environment Variables in CMake


To print all available environment variables:
559

execute_process(COMMAND env OUTPUT_VARIABLE ENV_VARS)


message(STATUS "Environment Variables:\n${ENV_VARS}")

This helps identify unexpected values that might interfere with the build.

16.3.3 Tracking Build Settings in CMake

Apart from environment variables, CMake relies on various build settings that control
compiler options, build types, and linking behavior. Misconfigurations here can lead to
compilation failures, performance issues, or incorrect binary generation.

1. Checking CMake Build Type


CMake supports different build types, which affect compiler flags and optimizations.

Example 4: Checking Build Type

message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}")

Typical values:
• Debug → Includes debug symbols (-g).
• Release → Enables optimizations (-O3).
• RelWithDebInfo → Optimized build with debug info.
• MinSizeRel → Optimization for minimal size.
If CMAKE BUILD TYPE is missing, explicitly set it:

cmake -DCMAKE_BUILD_TYPE=Release ..

or inside CMakeLists.txt:
560

set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type")

2. Verifying Compiler and Linker Settings


CMake automatically detects the compiler, but sometimes an incorrect one may be
selected. Use the following to verify:

Example 5: Checking Compiler Information

message(STATUS "C++ Compiler: ${CMAKE_CXX_COMPILER}")


message(STATUS "C Compiler: ${CMAKE_C_COMPILER}")
message(STATUS "Compiler Version: ${CMAKE_CXX_COMPILER_VERSION}")

Expected Output:

-- C++ Compiler: /usr/bin/g++


-- C Compiler: /usr/bin/gcc
-- Compiler Version: 11.3.0

If the wrong compiler is detected, manually set it:

cmake -DCMAKE_CXX_COMPILER=/usr/bin/clang++
,→ -DCMAKE_C_COMPILER=/usr/bin/clang ..

3. Checking Compiler Flags


Compiler flags influence optimizations, warnings, and debugging features.

Example 6: Printing Compiler Flags


561

message(STATUS "CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}")


message(STATUS "CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}")

To modify flags:

set(CMAKE_CXX_FLAGS "-Wall -Wextra -O2")

This ensures necessary warnings and optimizations are enabled.

4. Debugging Linker Issues


If CMake fails at the linking stage, check CMAKE EXE LINKER FLAGS:

message(STATUS "Linker Flags: ${CMAKE_EXE_LINKER_FLAGS}")

To add linker options:

set(CMAKE_EXE_LINKER_FLAGS "-Wl,-rpath,/custom/lib")

This is useful for resolving missing symbols or undefined references.

16.3.4 Debugging Using CMake Cache

CMake stores many build settings in CMakeCache.txt. If unexpected values appear,


check or delete the cache.

1. Viewing Cached Settings


Run:
562

cat CMakeCache.txt | grep "CMAKE_"

Or use:

message(STATUS "CMake Install Prefix: ${CMAKE_INSTALL_PREFIX}")

2. Resetting CMake Cache


If settings are incorrect, clear the cache:

rm -rf CMakeCache.txt CMakeFiles/


cmake ..

This forces a fresh configuration.

16.3.5 Summary

Tracking environment variables and build settings is critical for debugging CMake
issues.

• Environment Variables:

– Use $ENV{VAR NAME} to check and set(ENV{VAR NAME}) to modify.


– Key variables: CMAKE PREFIX PATH, PATH, CC, CXX,
LD LIBRARY PATH.
– Debug missing dependencies using CMAKE PREFIX PATH.

• Build Settings:

– Check CMAKE BUILD TYPE, CMAKE CXX COMPILER, and


CMAKE CXX FLAGS.
563

– Ensure correct compiler and linker settings using message(STATUS).


– Debug linking issues with CMAKE EXE LINKER FLAGS.

• CMake Cache:

– Inspect CMakeCache.txt for unexpected values.


– Delete the cache to reset configuration.

Mastering these techniques makes troubleshooting more systematic and efficient. In the
next section, we will explore advanced debugging tools such as cmake --trace and
cmake-gui.
564

16.4 Solutions to Common CMake Errors

(From Chapter 16: Troubleshooting and Debugging CMake Issues of the book **CMake:
The Comprehensive Guide to Managing and Building C++ Projects (From Basics to
Mastery))*

16.4.1 Introduction

CMake is a powerful tool for configuring and managing C++ projects, but developers
often encounter errors related to missing dependencies, incorrect configurations, or syntax
issues. Understanding common CMake errors and their solutions helps streamline
debugging and improve build reliability.

This section covers:

• Common configuration errors and how to fix them.

• Dependency resolution issues and missing package errors.

• Compiler and linker problems.

• Generator-related errors.

Each error is explained with examples and step-by-step solutions.

16.4.2 CMake Configuration Errors and Solutions

Configuration errors occur when running cmake .. and usually result from incorrect
syntax, missing files, or uninitialized variables.

1. Error: Unknown CMake Command


565

Example

CMake Error at CMakeLists.txt:5 (invalid_command):


Unknown CMake command "invalid_command".

Cause

• The command invalid command is not a valid CMake command.


• A typo or incorrect syntax in CMakeLists.txt.

Solution

1. Check for typos in the command.


2. Ensure required modules are included using include() or
find package().
3. Verify CMake version in case the command is available in newer versions.

Fixed Code

# Correct command or include missing module


include(SomeModule)

2. Error: Variable Not Defined

Example

CMake Error at CMakeLists.txt:10 (message):


Variable MY_VAR is not defined.

Cause
566

• MY VAR is referenced before being set.


• A required cache variable is missing.

Solution

1. Initialize variables before use with set().


2. Use if(DEFINED VAR) to check existence before accessing.

Fixed Code

set(MY_VAR "Hello, CMake!") # Ensure variable is set


message(STATUS "MY_VAR: ${MY_VAR}")

16.4.3 Dependency and Package Errors

These errors occur when find package() or find library() fails to locate
required dependencies.

1. Error: Missing Required Package

Example

CMake Error at CMakeLists.txt:15 (find_package):


Could not find package Boost.

Cause

• The package is not installed or not found in search paths.


• CMAKE PREFIX PATH is not set correctly.
567

Solution

1. Ensure package is installed.

sudo apt install libboost-dev # Ubuntu


brew install boost # macOS

2. Set CMAKE PREFIX PATH


if the package is in a custom location.

set(CMAKE_PREFIX_PATH "/custom/path/to/boost")
find_package(Boost REQUIRED)

3. Use find package() with HINTS.

find_package(Boost REQUIRED HINTS "/custom/path/to/boost")

2. Error: find library() Cannot Locate Library

Example

CMake Error at CMakeLists.txt:20 (find_library):


Could not find library MyLib.

Cause

• The library is not installed or located in an unknown directory.


• CMAKE LIBRARY PATH does not include the correct location.

Solution
568

1. Check library existence


in system paths:

find /usr/lib /usr/local/lib -name "libMyLib.so"

2. Set CMAKE LIBRARY PATH.

set(CMAKE_LIBRARY_PATH "/custom/path/to/lib")
find_library(MyLib NAMES MyLib)

3. Use HINTS for additional search paths.

find_library(MyLib NAMES MyLib HINTS "/custom/path/to/lib")

16.4.4 Compiler and Linker Errors

Errors during the build phase (cmake --build .) often indicate incorrect compiler
settings or missing link libraries.

1. Error: No C++ Compiler Found

Example

CMake Error: C++ compiler is not found!

Cause

• C++ compiler is not installed or not in PATH.


• CMAKE CXX COMPILER is not set.
569

Solution

1. Install a compiler :

sudo apt install g++ # Ubuntu


brew install gcc # macOS

2. Manually specify the compiler.

cmake -DCMAKE_CXX_COMPILER=/usr/bin/g++ ..

2. Error: Undefined Reference During Linking

Example

/usr/bin/ld: CMakeFiles/main.dir/main.cpp.o: undefined reference


,→ to `MyFunction'

Cause

• Missing library linkage in target link libraries().

Solution

1. Ensure target link libraries() includes the required library.

target_link_libraries(MyApp PRIVATE MyLib)

2. Verify the library path.


570

find_library(MyLib NAMES MyLib PATHS "/custom/lib/path")


target_link_libraries(MyApp PRIVATE ${MyLib})

16.4.5 CMake Generator Errors

CMake requires a generator (e.g., Ninja, Makefiles). Incorrect or unsupported generators


cause errors.

1. Error: Generator Not Found

Example

CMake Error: Could not create named generator "NinjaX"

Cause

• The specified generator is not installed or incorrect.

Solution

1. List available generators:

cmake --help

2. Use a valid generator:

cmake -G "Ninja" ..

3. Install missing generator:


571

sudo apt install ninja-build # Ubuntu


brew install ninja # macOS

2. Error: Incorrect Build Directory

Example

CMake Error: The source directory does not appear to contain


,→ CMakeLists.txt.

Cause

• Running cmake .. in the wrong directory.

Solution

1. Ensure CMakeLists.txt exists in the project root.


2. Run CMake from the correct directory.

mkdir build && cd build


cmake ..

16.4.6 Summary

Common CMake errors can be resolved by:

• Verifying configurations (CMakeLists.txt syntax, variables).


• Ensuring dependencies exist (find package(), CMAKE PREFIX PATH).
• Checking compiler settings (CMAKE CXX COMPILER, CMAKE CXX FLAGS).
572

• Fixing linker issues (target link libraries()).


• Using the correct generator (cmake --help to check available ones).

By systematically debugging these errors, developers can build robust CMake-based


projects efficiently.
Conclusion

Comprehensive Summary of the Book

This book, CMake: The Comprehensive Guide to Managing and Building C++ Projects
(From Basics to Mastery), has taken you on a structured journey through the world of
CMake, equipping you with the knowledge and skills to efficiently manage and build C++
projects. We started with the basics of CMake, explaining its role as a cross-platform build
system generator, and then progressed into more advanced topics such as toolchain
configurations, dependency management, and performance optimizations.

Key Takeaways from the Book:

1. Introduction to CMake – We explored what CMake is, why it is essential for


modern C++ development, and how it simplifies the build process across multiple
platforms.
2. Basic CMake Setup – You learned how to write simple CMakeLists.txt files,
define targets, link libraries, and configure project structures.
3. Project Structure and Organization – Best practices for structuring projects were
discussed, including modular design and using subdirectories for better
maintainability.

573
574

4. Managing Dependencies – We covered find package, FetchContent, and


third-party package managers such as Conan and vcpkg for handling dependencies
efficiently.
5. Toolchain Configuration and Cross-Compilation – The book provided insights
into setting up CMake for different platforms and cross-compiling for embedded
systems or other architectures.
6. Debugging and Troubleshooting CMake Issues – In this chapter, we explored
strategies for identifying and resolving common CMake problems using logging,
debugging tools, and best practices.
7. Optimization and Best Practices – We discussed techniques to optimize build
times, use CMake presets, manage compiler flags, and write reusable CMake scripts
for large projects.
8. Integration with CI/CD Pipelines – The book also demonstrated how to integrate
CMake with modern continuous integration tools such as GitHub Actions, GitLab
CI, and Jenkins for automated builds and testing.

By now, you should have a deep understanding of CMake and be capable of managing and
scaling projects efficiently, whether for small personal projects or enterprise-level software
development.

Advanced Tips for Mastering CMake

Even though this book provides a comprehensive foundation, mastering CMake requires
continuous learning and hands-on experience. Below are some advanced tips to further
refine your CMake skills:

1. Use cmake --trace-expand for Deep Debugging


575

If cmake --trace gives too much output, using --trace-expand allows


you to see variable expansions, which can be invaluable in debugging complex
CMakeLists.txt files.
2. Isolate Problems Using message(DEBUG ...) and VERBOSE=1
For diagnosing build issues, strategically placing message(DEBUG ...)
statements and using make VERBOSE=1 or ninja -v can help track how
commands are being executed.
3. Use CTest for Advanced Testing and Debugging
Leverage CTest with --output-on-failure to debug failing tests more
effectively. Combine this with sanitizers (ASan, UBSan) to catch runtime errors.
4. Enable CMAKE DEBUG TARGET PROPERTIES to Inspect Targets
To diagnose linking and dependency issues, setting
CMAKE DEBUG TARGET PROPERTIES provides an in-depth look at target
properties.
5. Prefer FetchContent Over add subdirectory for External Dependencies
Using FetchContent helps manage external dependencies in a more modular
way compared to directly using add subdirectory. It prevents version conflicts
and allows for better dependency isolation.
6. Use Precompiled Headers and Unity Builds for Faster Compilation
To speed up large C++ projects, consider using Precompiled Headers (PCH) and
Unity Builds (building multiple translation units together), which CMake supports
natively.
7. Employ CMakePresets.json for Multi-Configuration Builds
CMakePresets.json helps manage build configurations (e.g., Debug, Release,
RelWithDebInfo) across different environments without modifying
CMakeLists.txt.
576

8. Profile and Optimize CMake Execution with --profiling


Use the cmake --profiling option (or external tools like Ninja’s profiling
mode) to find bottlenecks in the CMake configuration process.
By applying these advanced techniques, you can significantly improve the
maintainability, efficiency, and debugging capabilities of your CMake-based
projects.

Additional Resources

To stay up to date with the latest CMake features and best practices, consider referring to
the following resources:

1. Books
• Professional CMake: A Practical Guide – Craig Scott
• Mastering CMake – Ken Martin, Bill Hoffman
• Modern CMake for C++ – Rafal Swidzinski
2. Official Documentation
• CMake Official Documentation – The most authoritative and up-to-date source
for CMake features and commands.
• CMake Wiki – Contains additional guides and community discussions.
3. Useful Websites and Blogs
• CMake Discourse Forum – A community-driven forum for discussing CMake
issues.
• Kitware Blog – Articles on CMake, VTK, and related technologies.
• Modern CMake GitHub Guide – A collection of CMake examples covering
various use cases.
577

• CppCon YouTube Channel – Talks on modern C++ and CMake best practices.

4. CMake Cheat Sheets

• CMake Quick Reference – A concise and practical summary of CMake


commands and best practices.
• CMake Best Practices GitHub – Covers do’s and don’ts for writing clean
CMakeLists.txt files.

By leveraging these resources, you can continue to improve your CMake expertise
and stay up to date with evolving best practices.

Open-Source Projects for Real-World Examples

To deepen your understanding of CMake, studying real-world open-source projects can be


invaluable. Below are some well-structured projects that showcase advanced CMake
usage:

1. LLVM/Clang (GitHub)

• A large-scale C++ project using modular CMake configurations.


• Demonstrates advanced toolchain configurations and cross-compilation.

2. CMake Itself (GitHub)

• The official CMake repository—an excellent case study in writing portable


CMake code.
• Uses CTest and CDash for automated testing.

3. Google’s Abseil (GitHub)

• A well-structured project demonstrating best practices for header-only C++


libraries with CMake.
578

4. OpenCV (GitHub)

• A cross-platform C++ project showcasing CMake’s flexibility in handling


optional dependencies and building for different platforms.

5. Boost Libraries (GitHub)

• A widely used C++ project demonstrating multi-platform builds and complex


dependency management.

6. GoogleTest (GitHub)

• A practical example of using CMake for testing frameworks with proper test
discovery mechanisms.

7. Qt Framework (GitHub)

• A large-scale example of integrating CMake with GUI frameworks and


managing extensive module dependencies.

By studying and contributing to these projects, you can gain practical experience
with real-world CMake configurations and improve your problem-solving skills.

Final Thoughts
CMake is an incredibly powerful tool for managing and building C++ projects across
multiple platforms. By mastering the techniques covered in this book, you will be able to
tackle complex build challenges, optimize workflows, and enhance project scalability.
Keep experimenting, stay updated with the latest CMake advancements, and contribute to
open-source projects to refine your expertise.
Happy coding with CMake!
Appendices

Appendix A: CMake Command Reference

This appendix is a comprehensive list of the most commonly used CMake commands,
providing a quick reference for when you're working on CMake-based projects.

Key CMake Commands:

1. cmake
The main command to configure, generate, and build a project.

cmake <path-to-source>

Example:

cmake /path/to/project

2. add executable
Defines an executable target in a CMake project.

579
580

add_executable(MyApp main.cpp)

3. add library
Defines a library target (static or shared).

add_library(MyLibrary STATIC mylib.cpp)

4. target link libraries


Links a target with libraries or other targets.

target_link_libraries(MyApp MyLibrary)

5. find package
Searches for and configures external dependencies, such as libraries or tools.

find_package(OpenCV REQUIRED)

6. include directories
Adds directories to the compiler's search path for include files.

include_directories(${CMAKE_SOURCE_DIR}/include)

7. set
Sets a variable or cache entry.

set(SOURCE_FILES main.cpp app.cpp)


581

8. message
Displays messages during the configuration process.

message(STATUS "Building MyApp with OpenCV")

9. install
Defines installation rules for files and targets.

install(TARGETS MyApp DESTINATION /usr/local/bin)

10. enable testing / add test


Enables the testing framework and adds tests.

enable_testing()
add_test(NAME MyTest COMMAND MyTestExecutable)

This appendix allows you to quickly locate command syntax and descriptions, which can
be especially helpful when debugging or experimenting with advanced CMake
configurations.
582

Appendix B: CMake Best Practices

This appendix provides a collection of best practices for writing clean, efficient, and
maintainable CMakeLists.txt files. These best practices are essential for scaling
projects and ensuring that your build system is easy to manage in the long term.

1. Keep CMakeLists.txt Simple and Modular


Avoid placing all logic in a single CMakeLists.txt file. Use subdirectories to
break your project into logical modules. This makes the project easier to maintain
and scale.

add_subdirectory(src)
add_subdirectory(tests)

2. Use Variables for Paths and Repeated Values


Instead of hardcoding paths and values, store them in variables to avoid duplication
and simplify updates.

set(MY_LIBRARY_PATH ${CMAKE_SOURCE_DIR}/libs)

3. Use target include directories and target link libraries


For better target management, always use target include directories and
target link libraries for linking dependencies, rather than global
commands like include directories or link directories.

target_include_directories(MyApp PRIVATE
,→ ${CMAKE_SOURCE_DIR}/include)
target_link_libraries(MyApp PRIVATE MyLibrary)
583

4. Prefer Modern CMake Syntax


Use target * commands instead of global commands whenever possible, as they
are more specific and help avoid accidental global settings.

target_compile_options(MyApp PRIVATE -Wall)

5. Use find package for External Dependencies


Whenever possible, rely on CMake’s find package command for managing
third-party libraries. This allows your project to work seamlessly with external
dependencies across various platforms.

find_package(OpenCV REQUIRED)

6. Ensure Consistent Formatting


Follow consistent naming conventions, indentation, and spacing. This makes your
CMakeLists.txt files easier to read and maintain.

7. Add Comments for Clarity


Even though CMake code is relatively readable, adding clear comments will help
anyone reviewing or updating the configuration.

# Add the main application


add_executable(MyApp main.cpp)

8. Handle Build Types Properly


Ensure you configure different build types (e.g., Debug, Release, RelWithDebInfo)
to provide the best development and performance experience.
584

set(CMAKE_BUILD_TYPE "Release")
585

Appendix C: CMake Troubleshooting Guide

The troubleshooting guide in this appendix offers solutions to common issues and tips for
diagnosing problems during the CMake configuration or build process.

1. Common CMake Errors and Solutions:


1. Error: ”Could not find CMakeLists.txt”
This error occurs when you run CMake from a directory that does not contain a
CMakeLists.txt file. To fix this, ensure you are in the correct directory
containing the root CMakeLists.txt.
2. Error: ”No CMAKE CXX COMPILER could be found”
This usually means that CMake cannot find a suitable C++ compiler. Ensure that
your system has a valid C++ compiler installed and available in the system path.
3. Error: ”Target already exists”
This error occurs if a target is defined more than once in your
CMakeLists.txt. Check for duplicate calls to add executable or
add library for the same target name.
4. Error: ”Unknown CMake command”
If you receive an error about an unknown command, double-check the spelling
of the CMake command and verify that the required version of CMake supports
the command.
5. Error: ”FindXXX.cmake module not found”
This error appears when CMake cannot locate a FindXXX.cmake module for
a required package. Ensure that the package is installed and that CMake's
CMAKE MODULE PATH variable is correctly set.
2. General Debugging Tips:
• Use cmake --trace to log all actions taken by CMake during configuration.
586

• Run CMake with -DCMAKE VERBOSE MAKEFILE=ON for verbose output


during the build process.
• Look at CMake’s cache file (CMakeCache.txt) for stored variables that
might be affecting the build.
587

Appendix D: CMake Project Examples

Here, we provide practical, real-world project examples with detailed


CMakeLists.txt files. These examples will help you understand how to structure
your CMake-based projects and handle common CMake configurations.

• Example 1: Simple CMake Project


This example shows how to set up a basic CMake project with a single executable
target.

cmake_minimum_required(VERSION 3.10)
project(SimpleProject)

add_executable(MyApp main.cpp)

• Example 2: Multi-Target Project


This example demonstrates how to organize a project with multiple targets
(executables and libraries).

cmake_minimum_required(VERSION 3.10)
project(MultiTargetProject)

add_library(MyLibrary STATIC lib.cpp)


add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLibrary)

• Example 3: External Dependency (OpenCV)


This example shows how to configure CMake to use an external library (OpenCV)
via find package.
588

cmake_minimum_required(VERSION 3.10)
project(OpenCVExample)

find_package(OpenCV REQUIRED)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE ${OpenCV_LIBS})

These examples provide a foundation for building larger, more complex CMake
projects. You can adapt these templates as needed for your own projects.
589

Appendix E: CMake Tools and Integrations

This appendix lists various tools and IDE integrations that work seamlessly with CMake
to streamline your development process.

1. CMake GUI
A graphical interface that simplifies the configuration process for projects. It allows
you to set variables and configure your project without using the command line.

2. Visual Studio Code (VSCode)


VSCode has excellent CMake support via the CMake Tools extension, providing
integration with CMake commands, build systems, and debugging tools.

3. CLion
A powerful IDE for C++ development, CLion has native CMake support, which
makes it a great choice for managing large C++ projects with CMake.

4. Ninja
A small build system with a focus on speed, Ninja can be used with CMake to speed
up builds and optimize the build process.

5. Continuous Integration Tools (GitHub Actions, GitLab CI, Jenkins)


These CI tools integrate seamlessly with CMake to automate builds, tests, and
deployment pipelines. Configuration is typically handled via .yml files and
CMake’s CMakeLists.txt setup.

Final Thoughts

The appendices in this book offer essential reference material and troubleshooting advice
to support your ongoing CMake journey. By utilizing these resources, you can navigate
590

common pitfalls, write more efficient CMakeLists.txt files, and manage complex
builds with ease.
References

Books

1. ”Professional CMake: A Practical Guide” by Craig Scott


This book is widely regarded as one of the best resources for learning CMake. It
provides a practical, hands-on approach, covering basic to advanced topics in great
detail. Craig Scott's writing style makes complex topics easy to understand, and the
book is full of real-world examples, making it an invaluable resource for developers
who want to master CMake.

• Key Topics: CMake fundamentals, writing portable CMake code, handling


external dependencies, and continuous integration with CMake.
• Why It’s Useful: It complements this book by diving deeper into advanced
techniques, offering more extensive coverage of CMake in production-level
projects.

2. ”Mastering CMake” by Ken Martin and Bill Hoffman


As the creators of CMake, Ken Martin and Bill Hoffman offer a definitive guide to
the tool in this book. It goes into the inner workings of CMake, covering not just
how to use the tool, but also why it behaves in certain ways and how to customize it
for specific needs.

591
592

• Key Topics: CMake internals, writing custom CMake modules, advanced topics
in toolchain file configurations, and understanding the build process at a low
level.
• Why It’s Useful: This is an excellent resource for developers who need to write
highly customized CMake files or troubleshoot complex build systems.

3. ”Modern CMake for C++” by Rafal Swidzinski


This book focuses on best practices for using CMake with modern C++ projects,
covering everything from simple applications to large-scale projects. It emphasizes
the use of modern CMake commands and strategies to create highly maintainable
and scalable C++ codebases.

• Key Topics: Modern CMake syntax, integration with external tools like CI/CD
pipelines, writing clean and reusable CMake code, and utilizing new CMake
features.
• Why It’s Useful: It offers a practical, example-driven approach to modern
CMake, which is great for developers looking to move away from legacy
CMake practices.

Online Documentation and Official Resources

1. CMake Official Documentation


The official documentation for CMake is the authoritative source for everything
related to CMake syntax, commands, modules, and configuration options. It is
constantly updated and covers all versions of CMake. The documentation is rich
with examples, command references, and explanations of CMake's internal workings.

• Key Features: Comprehensive command reference, tutorials for beginners,


detailed module documentation, and platform-specific guides.
593

• Why It’s Useful: For any question or issue related to CMake, the official
documentation is the go-to resource. It will help you understand the tool's core
functionality and how to use it in various scenarios.
2. CMake GitLab Repository
This repository contains the source code of CMake itself. It’s a valuable resource if
you need to see how CMake is implemented or want to report a bug, contribute to the
development of CMake, or explore the change history.
• Key Features: CMake source code, bug tracking, feature requests, and
contributions.
• Why It’s Useful: Developers who want to go beyond using CMake and
contribute to its development or learn about its internal mechanics will find this
a great resource.
3. CMake Wiki
The CMake Wiki is a community-driven resource that supplements the official
documentation with additional guides, how-tos, and solutions to common problems.
• Key Features: Community-contributed tutorials, tips and tricks, case studies,
and frequently asked questions.
• Why It’s Useful: The Wiki often contains solutions to real-world CMake
problems, contributed by experienced users and experts from the community.
It’s a great place to find practical solutions.
4. CMake Discourse
CMake’s official forum is an excellent place to discuss issues, ask for help, and find
discussions around CMake features. It’s a valuable resource for troubleshooting and
learning from others' experiences.
• Key Features: Discussion threads on CMake topics, feature requests, bug
reports, and user-provided examples.
594

• Why It’s Useful: The CMake Discourse forum allows you to connect with the
community, ask specific questions, and read about other users' experiences and
solutions to problems.

Websites and Blogs

1. Modern CMake
This website is dedicated to modern CMake best practices and provides an excellent
summary of the most current CMake features, syntax, and patterns.

• Key Features: Quick reference, guides on how to use CMake in modern C++
development, and examples of best practices.
• Why It’s Useful: This website is a great starting point for developers who want
to learn modern CMake practices in a structured and concise way.

2. Kitware Blog
Kitware, the company behind CMake, regularly publishes blog posts on new
features, best practices, case studies, and tutorials. The blog provides insights into
how CMake is used in various industries, including scientific computing, game
development, and more.

• Key Features: Updates on CMake features, case studies, tutorials, and advice
from CMake experts.
• Why It’s Useful: For developers looking to stay up-to-date with new features in
CMake or explore case studies of CMake used in complex environments, the
Kitware blog is a valuable resource.

3. CMake Best Practices


This open-source GitHub repository compiles a collection of CMake best practices,
595

tips, and tricks from the community. It’s an excellent place to find clean,
maintainable examples of how to structure and organize CMake files.
• Key Features: Best practices for writing reusable CMake code, guidelines for
organizing large projects, and solutions to common problems.
• Why It’s Useful: This repository is a useful guide for writing clean and efficient
CMake code. It contains advice on how to structure your build system to make
your projects more scalable and maintainable.
4. CMake Examples
This GitHub repository provides a collection of CMake examples covering various
use cases, from simple projects to more complex configurations.
• Key Features: Practical examples, explanations of CMake techniques, and
diverse use cases.
• Why It’s Useful: If you are learning CMake through hands-on examples, this
repository is a goldmine for realistic configurations. You can find examples on
advanced topics like cross-compiling, multi-platform support, and complex
dependency management.

Open-Source Projects and Repositories

1. LLVM/Clang
LLVM is a highly modular project that uses CMake as its build system. It
demonstrates complex, cross-platform CMake configurations in a large-scale
codebase.
• Why It’s Useful: By studying LLVM’s CMake setup, you can learn how to
handle large codebases, manage external dependencies, and optimize build
processes using CMake.
596

2. Boost Libraries
Boost is one of the most widely-used C++ libraries, and its CMake setup provides
examples of managing complex dependencies, creating libraries, and structuring
large C++ projects.

• Why It’s Useful: Boost’s CMake files provide a clear example of handling
external libraries, setting up multi-platform builds, and supporting various build
configurations.

3. OpenCV
OpenCV is a popular open-source computer vision library that uses CMake. The
OpenCV project is a great example of how to manage cross-platform builds, link to
external libraries, and organize large projects with CMake.

• Why It’s Useful: Studying OpenCV’s CMake configuration will give you
practical examples of using CMake with external dependencies and handling
complex C++ codebases.

Final Thoughts on References


These references will provide you with the necessary tools and information to continue
expanding your knowledge of CMake. Whether you're troubleshooting specific issues,
looking for advanced techniques, or seeking hands-on examples, the resources listed here
will help you develop a deeper understanding of CMake and its role in managing and
building C++ projects.

You might also like