CMake Project Template
This chapter uses a Linux-only CMake project template to explain how to organize, build, install, and integrate third-party libraries in a C++ project.
The goal of this template is not to cover every aspect of CMake syntax all at once, but rather to focus on a real, runnable project to understand the most commonly used set of practices in modern CMake.
- The top-level
CMakeLists.txtonly handles overall project control. CMakePresets.jsonLock in the Debug / Release build configurations.cmake/ProjectOptions.cmakeSave common project compilation options.src/CMakeLists.txtmanages the main program.- Each library directory manages its own source code, headers, installation rules, and third-party dependencies.
- Third-party libraries follow a "whoever uses them, whoever
find_package" approach.
Template Directory Structure
The core directories of the template are as follows:
.
├── CMakeLists.txt
├── CMakePresets.json
├── cmake/
│ └── ProjectOptions.cmake
└── src/
├── CMakeLists.txt
├── main.cpp
├── lib1/
│ ├── CMakeLists.txt
│ ├── inc/
│ │ └── lib1/
│ │ └── eigen3_test.hpp
│ └── src/
│ └── eigen3_test.cpp
└── lib2/
├── CMakeLists.txt
├── inc/
│ └── lib2/
│ └── eigen3_test.hpp
└── src/
└── eigen3_test.cpp
Among them:
| file | Responsibilities |
|---|---|
CMakeLists.txt | Top-level entry point, declares the project, loads common configuration, and enters src. |
CMakePresets.json | Save build presets, such as linux-debug and linux-release. |
cmake/ProjectOptions.cmake | Save C/C++ standards, common warnings, and other configurations. |
src/CMakeLists.txt | Create the final executable file and link lib1, lib2 |
src/lib1/CMakeLists.txt | Create lib1_src_lib to manage lib1's own includes, dependencies, and installation. |
src/lib2/CMakeLists.txt | Create lib2_src_lib to manage lib2's own includes, dependencies, and installations. |
Build Command
This template uses CMakePresets + Ninja.
Debug Build:
cmake --preset linux-debug
cmake --build --preset linux-debug
cmake --install build/linux-debug
./install/linux-debug/bin/cmake_template
Release build:
cmake --preset linux-release
cmake --build --preset linux-release
cmake --install build/linux-release
./install/linux-release/bin/cmake_template
These commands do four things respectively.
| command | effect |
|---|---|
cmake --preset linux-debug | Configure the project and generate Ninja build files. |
cmake --build --preset linux-debug | Compile source code and link objects |
cmake --install build/linux-debug | Install the executables, libraries, and header files to install/linux-debug. |
./install/linux-debug/bin/cmake_template | Run the program after installation |
During routine development, installation isn't necessary every time; after compiling, you can directly run the program from the build directory.
./build/linux-debug/src/cmake_template
install is more suitable for verifying "whether the post-installation directory structure and dynamic library paths are correct."
Configure, build, and install are three distinct stages commonly used in compiling and setting up software from source code. Here’s what each step does:
- Configure:
This step prepares the build environment. It checks system dependencies, verifies required libraries, and generates a customized build configuration (often via aMakefileor similar). It may also allow enabling/disabling specific features via options. - Build:
This stage performs the actual compilation or assembly of the source code, producing executable binaries or libraries based on the configuration set in the previous step. - Install:
After a successful build, this step copies the compiled files (executables, libraries, configuration files, etc.) to standard system directories (e.g.,/usr/local/bin,/usr/lib), making the software ready for use.
In summary:configure → prepares and adapts the build to your system;build → compiles the code;install → places the built software in the correct system locations.
A CMake project typically involves three stages:
configure
cmake --preset linux-debug
At this stage, all CMakeLists.txt and CMakePresets.json are read, compilers, third-party libraries, and variables are checked, and build system files are generated.
When using Ninja, files are generated under build/linux-debug at build.ninja.
build
cmake --build --preset linux-debug
At this stage, call Ninja to compile the .cpp files, generating build artifacts such as .o, .so, and executables.
install
cmake --install build/linux-debug
At this stage, follow the install(...) rule to copy the build artifacts to the installation directory.
In this template, Debug is installed to:
install/linux-debug/
Release installed to:
install/linux-release/
The typical structure after installation is as follows:
install/linux-debug/
├── bin/
│ └── cmake_template
├── include/
│ ├── lib1/
│ │ └── eigen3_test.hpp
│ └── lib2/
│ └── eigen3_test.hpp
└── lib64/
├── liblib1_src_lib.so
└── liblib2_src_lib.so
Some Linux distributions use lib, while others use lib64. This template uses GNUInstallDirs to allow CMake to automatically select the appropriate directory.
Why not handwrite build/install/log scripts
This practice was common in older templates:
- Create your own
builddirectory. - Run
cmake ..inbuild. - Handwritten
make install. - Hand-written script sets up
LD_LIBRARY_PATH. - Maintain the log directory yourself.
After switching the template to use CMakePresets, this content is no longer needed:
| previous method | New Approach |
|---|---|
Manual cd build | CMakePresets.json Unified Settings binaryDir |
| Manually set Debug / Release | set CMAKE_BUILD_TYPE in the preset |
| Manually Set Installation Path | set CMAKE_INSTALL_PREFIX in the preset |
| Manually invoke Makefile | preset specifies Ninja |
| Manual setting of dynamic library paths | Install Target Settings INSTALL_RPATH |
| Hand-writing VSCode Tasks |
To manually create custom tasks in Visual Studio Code (VSCode), follow these steps:
1. Create the tasks.json File
- Open your project folder in VSCode.
- Press Ctrl+Shift+P (or Cmd+Shift+P on macOS) and type “Tasks: Configure Task”.
- Select “Create tasks.json file from template” and choose “Others” (or start with an empty template).
- This will generate a
tasks.jsonfile inside a.vscodefolder in your project root.
2. Basic Structure of tasks.json
A minimal task configuration might look like this:
{
"version": "2.0.0",
"tasks": [
{
"label": "Build Project",
"type": "shell",
"command": "make",
"args": ["build"],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
label: A descriptive name for the task (visible in the task menu).type: Usually"shell"(for command-line tasks) or"process"(for direct executables).command: The command to run (e.g.,make,gcc,python).args: Arguments passed to the command.group: Specifies the task type ("build","test", etc.) and whether it’s the default.
3. Advanced Configuration
- Dependencies: Use
"dependsOn"to run multiple tasks in sequence."dependsOn": ["Task1", "Task2"] - Problem Matchers: Map compiler output to VSCode’s Problems panel.
"problemMatcher": "$gcc" - Environment Variables: Set via
"env"."env": { "PYTHONPATH": "${workspaceFolder}/src" } - Input Variables: Prompt for user input dynamically.
4. Run Tasks
- Press Ctrl+Shift+P and select “Tasks: Run Task”.
- Choose your task from the list.
- To run the default build task, use the shortcut Ctrl+Shift+B.
5. Examples for Common Scenarios
- Compile C++ with g++:
{ "command": "g++", "args": ["-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}"] } - Run a Python script:
{ "command": "python", "args": ["${file}"] } - Execute a shell script:
{ "command": "./scripts/build.sh", "type": "shell" }
6. Tips
- Use variables like
${workspaceFolder}or${file}for dynamic paths. - Configure tasks per workspace (
.vscode/tasks.json) or globally (user settings). - For complex builds, consider using compound tasks or integrating with build tools like CMake or Maven.
For full documentation, refer to VSCode’s official guide: Tasks in Visual Studio Code.|VSCode CMake Tools directly reads from presets.| |Handwriting complex debugging scripts|Directly run or debug the current CMake target using VSCode CMake Tools.|
Running and Debugging with VSCode CMake Tools
This template doesn't require extra VSCode debug configuration. After installing CMake Tools, VSCode can directly recognize executable targets in CMake and run or debug the current target.
Graphical processes can be understood as the button-based version of command-line processes:
| command line | VSCode CMake Tools |
|---|---|
cmake --preset linux-debug | After selecting the Linux Debug preset, configure |
cmake --build --preset linux-debug | Click Build |
./build/linux-debug/src/cmake_template | After selecting the cmake_template target, click the run button. |
| Debugging build artifacts with GDB | After selecting the cmake_template target, click the Debug button. |
CMake Tools typically runs or debugs the executable files located in the build directory:
build/linux-debug/src/cmake_template
Therefore, daily development does not require installation each time. install is primarily used for verifying the installation layout, header file installation, dynamic library RPATH, and similar aspects.
Recommended workflow:
- Install the VSCode CMake Tools extension and Microsoft C/C++ extension.
- Select
linux-debugconfigure preset. - Configure。
- Build。
- Select
cmake_templateas the run/debug target. - Click the Run or Debug button provided by CMake Tools.
For more detailed graphical interface operation steps, see CMakePresets and Build and Installation.
Recommended reading order for this chapter
- CMakePresets and Build Installation: First, understand the preset workflow.
- Top-level CMake and Public Compilation Options: Understanding the top-level entry point and public configuration.
- CMake In-Depth Guide for src and lib Modules: Understanding Executables, Libraries, Include, and Install.
- Third-party library dependency writing: Learn how to introduce libraries such as Eigen, OpenCV, Boost, and PCL.
After completing this tutorial, you should be able to:
- Create a new C++ project and build it using presets.
- Add new library directory.
- Add new executable files.
- Correctly manage the header file include paths.
- Import third-party libraries within the module.
- Once installed, run the program directly without needing to rely on hand-written scripts.