Qt6 projects are built with CMake, and CMake plays a crucial role in managing and structuring C++ projects.
What is CMake?
CMake is a build system manager; it tells tools like Make, Ninja, or Visual Studio how to compile our C++ code, what libraries to use, where to find headers, or how to organize the output.
Note: CMake is not a compiler. It is just a build system generator. It creates the actual build files (like Makefile, Ninja files, Visual Studio solutions) that tell the compiler like g++, clang++, etc., how to build our C++ project.
One important term that keeps coming up when working with CMake is "target".
What is a target?
A target is the thing you're building.
What it Builds:
-
add_executable()
: A complete program with main() -
add_library()
: A reusable module (static/shared) - Imported target: From system or library (like Qt6::Core)
Example:
add_executable(MyApp main.cpp)
Here:
-
MyApp
is a target name. - It's an executable target.
- It will build into something like
MyApp.exe
or./MyApp
.
Why Targets Have Different Names in Folders?
Because each folder has its own role, so its target is named after what it's doing.
Example:
project/
├── src/
│ ├── Core/
│ ├── UI/
│ └── Network/
Each has:
# src/Core/CMakeLists.txt
add_library(CoreLib STATIC CoreEngine.cpp)
# src/UI/CMakeLists.txt
add_library(UILib STATIC MainWindow.cpp)
Then in root:
add_executable(MyApp main.cpp)
target_link_libraries(MyApp CoreLib UILib)
So you get:
-
CoreLib
– reusable backend logic -
UILib
– reusable UI components -
MyApp
– the final app that uses them
Why So Many target_* Commands?
Because you don't want to change things globally.
Instead, you do everything per-target, so your build is modular and clean.
Example:
target_include_directories(CoreLib PRIVATE include/)
target_compile_definitions(UILib PUBLIC USE_QT)
What Happens When You Run CMake?
Step 1:
cmake -S . -B build/
Step 2:
cmake --build build/
What this does:
-
CMakeLists.txt
is read. - It finds what to build (targets).
- It finds what libraries and dependencies are needed.
- It generates a Makefile or
build.ninja
or.vcxproj
. - You then build using make or ninja or an IDE.
CMake Vocabulary You Must Know
cmake_minimum_required(VERSION <min_version>)
Sets the minimum version of CMake required to build the project.
cmake_minimum_required(VERSION 3.16)
project(<project_name> VERSION <version> LANGUAGES <lang1> <lang2>)
Defines the project name, version, and language.
project(myApp VERSION 1.0 LANGUAGES CXX)
set(<VAR_NAME> <value>)
Assigns a value to a variable.
set(CMAKE_CXX_STANDARD 20)
set(SOURCES main.cpp utils.cpp)
You can use the variable as ${CMAKE_CXX_STANDARD}
and ${SOURCES}
add_executable(<target_name> <source_files...>)
Declares an executable to build.
add_executable(myApp main.cpp utils.cpp)
add_library(<target_name> STATIC|SHARED <source_files...>)
Creates a library (static or shared).
Note:
-
add_executable
→ builds.exe
or.app
(for apps withmain()
) -
add_library
→ builds.a
or.so
(for reusable code or shared modules)
target_link_libraries(<target_name> <libraries…>)
Links one target to other targets, frameworks, or libraries.
target_link_libraries(myApp Qt6::Core Qt6::Quick customLib)
target_include_directories(<target> PUBLIC|PRIVATE <dirs...>)
Adds header paths for a target.
target_include_directories(customLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
find_package(<PackageName> [REQUIRED] [COMPONENTS ...])
Finds system-installed packages like Qt, Boost, or OpenCV.
find_package(Qt6 REQUIRED COMPONENTS Core Quick Widgets)
find_path(<VAR> <header> [PATHS path1 path2 ...])
Finds a directory containing a header.
find_path("path here" customLib.h)
find_library(<VAR> <Library_name> [PATHS path1 path2 ...])
Finds the path to a compiled library (.a, .so, .lib)
find_library(MYLIB_LIBRARY myLib)
add_subdirectory(<folder>)
Adds a subfolder that contains its own CMakeLists.txt
.
add_subdirectory(src)
add_subdirectory(tests)
install(TARGETS <target> DESTINATION <folder>)
install(FILES <file> DESTINATION <folder>)
Specifies install paths for targets or files.
install(TARGETS myApp DESTINATION bin)
set_target_properties(<target> PROPERTIES <key> <value> ...)
Sets advanced properties like bundle info or output name.
set_target_properties(myApp PROPERTIES MACOSX_BUNDLE TRUE)
message
Prints custom messages to the console during CMake configuration.
message(STATUS "Hello CMAKE")
Modular Build System (Separation of Concerns)
What it means:
Split your project into independent parts (modules). Each part has its own job.
Example:
Imagine a project with:
-
UI/
— Handles how things look (buttons, windows) -
Network/
— Handles internet communication -
Utils/
— Handles helper functions
Each folder has its own CMakeLists.txt
, so they can be built separately.
project/
├── UI/
│ ├── Window.qml
│ └── CMakeLists.txt
├── Network/
│ ├── NetManager.cpp
│ └── CMakeLists.txt
├── main.cpp
└── CMakeLists.txt
Why it helps:
- Makes each module easy to understand and update
- You can focus on one part at a time
- Faster Builds with Better Parallelism
Faster Builds with Better Parallelism
What it means:
When you change a file, only its module is rebuilt — not the whole project.
Example:
You change UI/Button.qml
. Only the UI part needs to rebuild. The rest (like Network) is untouched.
# In UI/CMakeLists.txt
add_library(UI STATIC
Button.qml
)
Why it helps:
- Saves time when compiling
- Especially important in big projects with 100+ files
Clean Project Structure
What it means:
Keeps your project organized.
Example:
Bad:
project/
├── main.cpp
├── NetManager.cpp
├── Window.qml
├── AppUtils.cpp
Good:
project/
├── UI/
├── Network/
├── Utils/
Why it helps:
- Easy to find things
- Looks professional
- Encourages teamwork (different people work on different folders)
Scoped Dependencies and Linking
What it means:
Only include what's necessary in each module.
Example:
The Network module needs Qt's network support. The UI module needs QtQuick.
# Network/CMakeLists.txt
target_link_libraries(Network Qt6::Network)
# UI/CMakeLists.txt
target_link_libraries(UI Qt6::Quick)
Why it helps:
- Smaller binary
- Faster to compile
- You avoid bloating the entire app with unused stuff
Testing and CI/CD Ease
What it means:
You can write and run tests for each module.
Example:
Utils/
├── MathHelper.cpp
├── CMakeLists.txt
└── tests/
├── test_MathHelper.cpp
└── CMakeLists.txt
Why it helps:
- Test only what you need
- Catch bugs early
- Works great with GitHub Actions or GitLab CI
Supports Plugins and Scalability
What it means:
Big apps like QGroundControl support plugins. Each plugin is a subdirectory.
Example:
Plugins/
├── GPSPlugin/
│ ├── GPS.cpp
│ └── CMakeLists.txt
In the main CMakeLists.txt
:
add_subdirectory(Plugins/GPSPlugin)
Why it helps:
- Add or remove features easily
- Users can build only what they need
Project structure example:
MyApp/
├── CMakeLists.txt # Root
├── main.cpp
├── UI/
│ └── CMakeLists.txt
├── Network/
│ └── CMakeLists.txt
└── Utils/
└── CMakeLists.txt
Root CMakeLists.txt
:
cmake_minimum_required(VERSION 3.16)
project(MyApp)
add_subdirectory(UI)
add_subdirectory(Network)
add_subdirectory(Utils)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp
UI
Network
Utils
Qt6::Core
)
UI/CMakeLists.txt:
add_library(UI STATIC
MainWindow.qml
)
target_link_libraries(UI Qt6::Quick)
Structuring your C++ or Qt project with CMake properly from the beginning will save you hours in debugging, rebuilding, and scaling. With clear module boundaries, proper target scopes, and clean linking, your project becomes professional, maintainable, and ready for collaboration or production.