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

  1. 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
  2. Describe requirements on targets, never globally. target_include_directories, target_compile_options, target_link_libraries. Not include_directories, add_definitions, link_libraries. Lesson 7
  3. List sources explicitly. No file(GLOB) for source files. The docs warn against it. Lesson 19
  4. Choose the narrowest scope. PRIVATE over PUBLIC over INTERFACE. Ask “does this appear in my public headers?”. No, use PRIVATE. Lessons 9 and 10
  5. Pick the C++ standard per target. target_compile_features(app PRIVATE cxx_std_20), not a global CMAKE_CXX_STANDARD. Lesson 12
  6. Enable warnings per target, per compiler. And do not turn -Werror on by default. Lesson 11
  7. Structure by directory. A top-level CMakeLists.txt plus one per subdirectory, and include/<name>/ for public headers. Lessons 5 and 15
  8. Use imported targets when available. Threads::Threads, fmt::fmt, not ${FOO_LIBRARIES}. Lesson 14
  9. find_package for system dependencies, FetchContent for pinned self-contained ones. Always hash-pin GIT_TAG. Lessons 17 and 18
  10. Ship presets. CMakePresets.json committed, CMakeUserPresets.json ignored. One command for everyone, including CI. Lesson 20
  11. Build out of source. cmake -S . -B build, and build/ in .gitignore. Lesson 4
  12. Make it installable. install(TARGETS ... EXPORT ...), GNUInstallDirs, public headers installed. Lesson 22
  13. Ship a config package. So consumers write find_package(calc) and link calc::calc. Lesson 23
  14. Test with CTest. enable_testing(), add_test(NAME ... COMMAND ...), ctest --output-on-failure in CI. Lesson 25
  15. 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.txt
cmake_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.txt
add_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.