Setting Up a Modern C++ Project with CMake
In this blog post, I'll walk you through setting up a new C++ project using CMake. We'll explore folder structure, dependency management, and how to share your project with others. I will use my little game project as an example.
Project Structure
The folder structure for the project is organized as follows:
project-root/
├── etc/
│ └── cmake
│ └── toolchain.cmake
├── src/
│ ├── engine
│ │ ├── CMakeLists.txt
│ │ ├── video.cpp
│ │ ├── video.h
│ │ └── ...
│ └── game
│ ├── CMakeLists.txt
│ ├── main.cpp
│ └── ...
├── test/
│ ├── CMakeLists.txt
│ ├── engine_tests.cpp
│ └── ...
├── .clang-format
├── .gitignore
├── CMakeLists.txt
├── CMakePresets.json
├── Makefile
└── vcpkg.json
Root CMakeLists.txt
The root CMakeLists.txt
declares the project and manages dependencies:
cmake_minimum_required(VERSION 3.28.3)
project(MyLittleGame
DESCRIPTION "Just a little game"
VERSION 1.0
LANGUAGES CXX
)
find_package(assimp 5.4 CONFIG REQUIRED)
find_package(Bullet 3 CONFIG REQUIRED)
find_package(GLEW 2.2 CONFIG REQUIRED)
find_package(glm 1.0 CONFIG REQUIRED)
find_package(SDL3 3.2 CONFIG REQUIRED)
add_subdirectory(src/engine)
add_subdirectory(src/game)
include(CTest)
if(ENABLE_TESTING AND CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
add_subdirectory(test)
endif()
Source Folder
The src
folder contains the game library and executable. Here's an example CMakeLists.txt
:
add_library(engine)
target_compile_features(engine
PUBLIC
cxx_std_20
)
target_sources(engine
PRIVATE
video.cpp
PUBLIC
FILE_SET HEADERS
BASE_DIRS ..
FILES
video.h
)
target_link_libraries(engine
PRIVATE
assimp::assimp
LinearMath
Bullet3Common
BulletInverseDynamics
BulletCollision
BulletDynamics
BulletSoftBody
GLEW::GLEW
glm::glm
SDL3::SDL3
)
And another example of consuming that library
add_executable(game)
target_compile_features(game
PUBLIC
cxx_std_20
)
target_sources(game
PRIVATE
main.cpp
)
target_link_libraries(game
PRIVATE
engine
)
Test Folder
The test
folder is only included if testing is enabled. Example CMakeLists.txt
:
add_executable(UnitTests)
target_sources(UnitTests
PRIVATE
engine_tests.cpp
)
target_link_libraries(UnitTests
PRIVATE
engine
)
Dependency Management
Installing Vcpkg
Vcpkg is installed either in a global folder like $HOME/vcpkg
or as a git submodule git submodule add https://github.com/microsoft/vcpkg.git
. Use the VCPKG_ROOT
environment variable to point to the global vcpkg folder.
Using Vcpkg in Manifest Mode
Dependencies are declared using a vcpkg.json
manifest file:
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
"dependencies": [
{
"name": "assimp"
},
{
"name": "bullet3"
},
{
"name": "glew"
},
{
"name": "glm"
},
{
"name": "sdl3"
}
]
}
To simplify detection if the global or local option is used, a custom toolchain file checks for VCPKG_ROOT
:
if(DEFINED ENV{VCPKG_ROOT})
include("$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake")
elseif(EXISTS "${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake")
include("${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake")
else()
message(FATAL_ERROR "Vcpkg not found")
endif()
I wonder if FetchContent_Populate
could be used here as well?
Extras
Use CMake presets to simplify setup
Defaults are usually fine but if you need to build with different compilers or set different options, create a CMakePresets.json
file.
{
"$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json",
"version": 8,
"configurePresets": [
{
"name": "base",
"hidden": true,
"binaryDir": "${sourceDir}/out/${presetName}",
"toolchainFile": "${sourceDir}/etc/cmake/toolchain.cmake",
"cacheVariables": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "YES"
}
},
{
"name": "default",
"inherits": "base"
},
{
"name": "ninja",
"hidden": true,
"generator": "Ninja Multi-Config"
},
{
"name": "ninja-gcc14",
"inherits": [
"default",
"ninja"
],
"environment": {
"CXX": "g++-14"
}
},
{
"name": "ninja-clang19-osx",
"inherits": [
"default",
"ninja"
],
"environment": {
"CXX": "/opt/homebrew/opt/llvm@17/bin/clang++"
}
}
]
}
Makefile
Make is not needed but I like it to reduce typing on common tasks. This template is my goto for just about any project irregardless of language.
ifeq ($(OS),Windows_NT)
SHELL := pwsh.exe
else
SHELL := pwsh
endif
.SHELLFLAGS := -NoProfile -Command
ifneq (,$(wildcard .env))
include .env
export
endif
OUT ?= out
PRESET ?= default
CONFIG ?= Release
.PHONY: configure build test install clean
$(OUT)/$(PRESET)/CMakeCache.txt: CMakeLists.txt
@cmake \
-B $(OUT)/$(PRESET) \
-S . \
--preset $(PRESET)
configure: $(OUT)/$(PRESET)/CMakeCache.txt
build: configure
@cmake \
--build $(OUT)/$(PRESET) \
--config $(CONFIG)
test: build
@ctest \
--test-dir $(OUT)/$(PRESET) \
--build-config $(CONFIG) \
--output-on-failure
clean:
@-Remove-Item -Recurse -Force $(OUT)
Warning
Makefiles must be indented with tabs, not spaces.
I have choosen powershell
as shell which probably isn't that common. Powershell
has great crossplatform support so give it a try. Check out here for installation instructions but if you are using the dotnet
eco-system you can install it with:
dotnet tool install --global PowerShell
Mixing Vcpkg with FetchContent
You can combine Vcpkg and FetchContent to manage dependencies. Using FetchContent
:
include(FetchContent)
FetchContent_Declare(
MyDependency
GIT_REPOSITORY https://github.com/example/mydependency.git
GIT_TAG main
)
FetchContent_MakeAvailable(MyDependency)
find_package(MyDependency CONFIG REQUIRED)
Ensure Vcpkg is prioritized using FIND_PACKAGE_ARGS
:
FetchContent_Declare(
AnotherDependency
GIT_REPOSITORY https://github.com/example/anotherdependency.git
GIT_TAG main
FIND_PACKAGE_ARGS CONFIG REQUIRED
)
FetchContent_MakeAvailable(AnotherDependency)
Consuming Libraries from GitHub
To consume the library from GitHub using FetchContent:
FetchContent_Declare(
MyLibrary
GIT_REPOSITORY https://github.com/example/mylibrary.git
GIT_TAG main
)
FetchContent_MakeAvailable(MyLibrary)
Or with Vcpkg:
vcpkg install mylibrary
Conclusion
This setup combines the power of modern C++20, CMake, and dependency management tools like Vcpkg and FetchContent. By mixing these approaches, you can create a flexible and efficient build system. Experiment with these methods to find what works best for your project.