Lesson 28 · Reference
Best practices
The whole course distilled into a checklist. Fifteen rules, each with the lesson that proves it.
Everything you learned, compressed into one checklist. Each rule links the lesson that explains why.
The checklist
- Start with the right minimum version.
cmake_minimum_required(VERSION 4.4), and never below 3.5, since CMake 4.x errors on older policy versions. Lesson 6 - Describe requirements on targets, never globally.
target_include_directories,target_compile_options,target_link_libraries. Notinclude_directories,add_definitions,link_libraries. Lesson 7 - List sources explicitly. No
file(GLOB)for source files. The docs warn against it. Lesson 19 - Choose the narrowest scope. PRIVATE over PUBLIC over INTERFACE. Ask “does this appear in my public headers?”. No, use
PRIVATE. Lessons 9 and 10 - Pick the C++ standard per target.
target_compile_features(app PRIVATE cxx_std_20), not a globalCMAKE_CXX_STANDARD. Lesson 12 - Enable warnings per target, per compiler. And do not turn
-Werroron by default. Lesson 11 - Structure by directory. A top-level
CMakeLists.txtplus one per subdirectory, andinclude/<name>/for public headers. Lessons 5 and 15 - Use imported targets when available.
Threads::Threads,fmt::fmt, not${FOO_LIBRARIES}. Lesson 14 find_packagefor system dependencies,FetchContentfor pinned self-contained ones. Always hash-pinGIT_TAG. Lessons 17 and 18- Ship presets.
CMakePresets.jsoncommitted,CMakeUserPresets.jsonignored. One command for everyone, including CI. Lesson 20 - Build out of source.
cmake -S . -B build, andbuild/in.gitignore. Lesson 4 - Make it installable.
install(TARGETS ... EXPORT ...),GNUInstallDirs, public headers installed. Lesson 22 - Ship a config package. So consumers write
find_package(calc)and linkcalc::calc. Lesson 23 - Test with CTest.
enable_testing(),add_test(NAME ... COMMAND ...),ctest --output-on-failurein CI. Lesson 25 - Know the foot-guns and avoid them.
file(GLOB), global variables, deprecated flags. Lesson 30 is the full list. What not to use
The short version
Everything is a target. Every requirement lives on the target that owns it, with the narrowest scope. Sources are explicit. Versions are pinned. Presets are committed. Tests run with ctest.
A template that follows every rule
myproj/
├── CMakePresets.json
├── .gitignore # build/
├── CMakeLists.txt
├── src/
│ ├── CMakeLists.txt
│ └── main.cpp
├── libs/
│ └── calc/
│ ├── CMakeLists.txt
│ ├── calc.cpp
│ └── include/calc/calc.h
└── tests/
├── CMakeLists.txt
└── test_calc.cpp
CMakeLists.txtcmake_minimum_required(VERSION 4.4) project(myproj VERSION 1.0.0 LANGUAGES CXX) include(CTest) if(BUILD_TESTING) add_subdirectory(tests) endif() add_subdirectory(src) add_subdirectory(libs/calc)
libs/calc/CMakeLists.txtadd_library(calc STATIC calc.cpp) target_include_directories(calc PUBLIC include) target_compile_features(calc PUBLIC cxx_std_20) # Rules 5 and 6. The standard and the warnings live with the target. add_library(calc_warnings INTERFACE) if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT MATCHES "MSVC") target_compile_options(calc_warnings INTERFACE /W4) else() target_compile_options(calc_warnings INTERFACE -Wall -Wextra) endif() target_link_libraries(calc PRIVATE calc_warnings) # Rules 12 and 13. Installable and find_package-able. include(GNUInstallDirs) install(TARGETS calc EXPORT calcTargets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) install(FILES include/calc/calc.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/calc) install(EXPORT calcTargets FILE calcConfig.cmake NAMESPACE calc:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/calc )
Tip
Do not apply all fifteen at once to an existing project. That is a rewrite, not an improvement. Do them in order of pain. First explicit sources, then target-scoped include dirs, then presets. Each step is independently valuable.
Read the docs
The official CMake tutorial covers the same ground, step by step, if you want the primary source.