Project Structure
Overview
Teaching: 10 min
Exercises: 10 minQuestions
What should my project look like?
Objectives
Understand best practices for project structure
For this section, we will be looking at the project in code/03-structure
.
code/03-structure/
├── CMakeLists.txt
├── README.md
├── apps
│ ├── CMakeLists.txt
│ └── app.cpp
├── cmake
│ └── FindSomeLib.cmake
├── docs
│ ├── CMakeLists.txt
│ └── mainpage.md
├── include
│ └── modern
│ └── lib.hpp
├── src
│ ├── CMakeLists.txt
│ └── lib.cpp
└── tests
├── CMakeLists.txt
└── testlib.cpp
First, take a look at the main CMakeLists.txt
file. This is an example of a nice project file
in CMake 3.14, so enjoy it for a minute. Now let’s look at specifics!
Click to see CMakeLists.txt
# Works with 3.14 and tested through 3.18
cmake_minimum_required(VERSION 3.14...3.18)
# Project name and a few useful settings. Other commands can pick up the results
project(
ModernCMakeExample
VERSION 0.1
DESCRIPTION "An example project with CMake"
LANGUAGES CXX)
# Only do these if this is the main project, and not if it is included through
# add_subdirectory
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
# Optionally set things like CMAKE_CXX_STANDARD,
# CMAKE_POSITION_INDEPENDENT_CODE here
# Let's ensure -std=c++xx instead of -std=g++xx
set(CMAKE_CXX_EXTENSIONS OFF)
# Let's nicely support folders in IDE's
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
# Testing only available if this is the main app. Note this needs to be done
# in the main CMakeLists since it calls enable_testing, which must be in the
# main CMakeLists.
include(CTest)
# Docs only available if this is the main app
find_package(Doxygen)
if(Doxygen_FOUND)
add_subdirectory(docs)
else()
message(STATUS "Doxygen not found, not building docs")
endif()
endif()
# FetchContent added in CMake 3.11, downloads during the configure step
# FetchContent_MakeAvailable was not added until CMake 3.14
include(FetchContent)
# Accumulator library This is header only, so could be replaced with git
# submodules or FetchContent
find_package(Boost REQUIRED)
# Adds Boost::boost / Boost::headers (newer FindBoost / BoostConfig 3.15 name)
# Formatting library, adds fmt::fmt
FetchContent_Declare(
fmtlib
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 7.0.2)
FetchContent_MakeAvailable(fmtlib)
# The compiled library code is here
add_subdirectory(src)
# The executable code is here
add_subdirectory(apps)
# Testing only available if this is the main app
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
Protect project code
The parts of the project that only make sense if we are building this as the main project are
protected; this allows the project to be included in a larger master project with
add_subdirectory
.
Testing handled in the main CMakeLists
We have to do a little setup for testing in the main CMakeLists, because you can’t run
enable_testing
from a subdirectory (and thereby include(CTest)
). Also, notice that
BUILD_TESTING
does not turn on testing unless this is the main project.
Finding packages
We find packages in our main CMakeLists, then use them in subdirectories. We could have also put
them in a file that was inlude
d, such as cmake/find_pakages.cmake
. If your CMake is new enough,
you can even add a subdirectory with the find packages commands, but you have to set
IMPORTED_GLOBAL
on the targets you want to make available if you do that. For small to mid-size
projects, the first option is most common, and large projects use the second option (currently).
All the find packages here provide imported targets. If you do not have an imported target, make
one! Never use the raw variables past the lines immediately following the find_package
command.
There are several easy mistakes to make if you do not make imported targets, including forgetting to
add SYSTEM
, and the search order is better (especially before CMake 3.12).
In this project, I use the new FetchContent to download several dependencies; although normally I
prefer git submodules in /extern
.
Source
Now follow the add_subdirectory
command to see the src folder, where a library is created.
Click to see src/CMakeLists.txt
# Note that headers are optional, and do not affect add_library, but they will
# not show up in IDEs unless they are listed in add_library.
# Optionally glob, but only for CMake 3.12 or later: file(GLOB HEADER_LIST
# CONFIGURE_DEPENDS "${ModernCMakeExample_SOURCE_DIR}/include/modern/*.hpp")
set(HEADER_LIST "${ModernCMakeExample_SOURCE_DIR}/include/modern/lib.hpp")
# Make an automatic library - will be static or dynamic based on user setting
add_library(modern_library lib.cpp ${HEADER_LIST})
# We need this directory, and users of our library will need it too
target_include_directories(modern_library PUBLIC ../include)
# This depends on (header only) boost
target_link_libraries(modern_library PRIVATE Boost::boost)
# All users of this library will need at least C++11
target_compile_features(modern_library PUBLIC cxx_std_11)
# IDEs should put the headers in a nice place
source_group(
TREE "${PROJECT_SOURCE_DIR}/include"
PREFIX "Header Files"
FILES ${HEADER_LIST})
The headers are listed along with the sources in the add_library
command. This would have been
another way to do it in CMake 3.11+:
add_library(modern_library)
target_sources(modern_library
PRIVATE
lib.cpp
PUBLIC
${HEADER_LIST}
)
Notice that we have to use target_include_directories
; just adding a header to the sources does
not tell CMake what the correct include directory for it should be.
We also set up the target_link_libraries
with the appropriate targets.
App
Now take a look at apps/CMakeLists.txt
. This one is pretty simple, since all the leg work for
using our library was done on the library target, as it should be.
Click to see apps/CMakeLists.txt
add_executable(app app.cpp)
target_compile_features(app PRIVATE cxx_std_17)
target_link_libraries(app PRIVATE modern_library fmt::fmt)
Docs and Tests
Feel free to look at docs
and tests
for their CMakeLists.txt
.
Click to see docs/CMakeLists.txt
set(DOXYGEN_EXTRACT_ALL YES)
set(DOXYGEN_BUILTIN_STL_SUPPORT YES)
doxygen_add_docs(docs modern/lib.hpp "${CMAKE_CURRENT_SOURCE_DIR}/mainpage.md"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/include")
Click to see tests/CMakeLists.txt
# Testing library
FetchContent_Declare(
catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v2.9.1)
FetchContent_MakeAvailable(catch2)
# Adds Catch2::Catch2
# Tests need to be added as executables first
add_executable(testlib testlib.cpp)
# I'm using C++17 in the test
target_compile_features(testlib PRIVATE cxx_std_17)
# Should be linked to the main library, as well as the Catch2 testing library
target_link_libraries(testlib PRIVATE modern_library Catch2::Catch2)
# If you register a test, then ctest and make test will run it. You can also run
# examples and check the output, as well.
add_test(NAME testlibtest COMMAND testlib) # Command can be a target
More reading
- Based on Modern CMake basics/structure
Key Points
Projects should have a standard structure