Skip to content

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.