2020-08-28

CMake Tutorial

  • step by step, various topics in one example
  • document & example → /Help/guide/tutorial in source code
    • result of this step is in the next step's subfolder

01: Starting Point

  • minimum project requires only 3 lines in CMakeLists.txt
    • cmake_minimum_required(VERSION 3.10)
    • project(Tutorial)
    • add_executable(Tutorial tutorial.cxx)
  • lower case commands 💬 let's follow official preference, please

Version Number

  • why? more flexible in CMakeLists.txt than in source code
  • how?
    1. project(Tutorial VERSION 1.0)
      specify version number like this
    2. configure_file(TutorialConfig.h.in TutorialConfig.h)
      config file will under binary tree
    3. target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}")
      so as to include config file
      💬 when to quote? something may contains spaces
    4. create TutorialConfig.h.in with
      • @Tutorial_VERSION_MAJOR@
      • @Tutorial_VERSION_MINOR@
      • 💬 also PATCH, TWEAK
    5. access version number by including config file

C++ Standard

  1. set(CMAKE_CXX_STANDARD 11)
  2. set(CMAKE_CXX_STANDARD_REQUIRED True) 💬 CXX_STANDARD_REQUIRED

Build & Test

  1. cmake ..
  2. cmake --build .
  3. find and run your program

02: Add Library

  • subdirectory
    • create CMakeLists.txt under MathFunctions folder
      • add_library(MathFunctions mysqrt.cxx)
    • in top-level CMakeLists.txt
      • add_subdirectory(MathFunctions)
      • target_link_libraries(Tutorial PUBLIC MathFunctions)
      • target_include_directories(… "${PROJECT_SOURCE_DIR}/MathFunctions")
  • option
    • option(USE_MYMATH "…" ON)
    • if(USE_MYMATH)
      • add_subdirectory(MathFunctions)
      • list(APPEND EXTRA_LIBS MathFunctions)
      • list(APPEND EXTRA_INCS "${PROJECT_SOURCE_DIR}/MathFunctions")
    • for Tutorial target
      • target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})
      • target_include_directories(… "${EXTRA_INCS}")
    • EXTRA_LIBS/EXTRA_INCS are classic approach → modern approach next step
    • in source code
      • use #ifdef USE_MYMATH where in need
      • add #cmakedefine USE_MYMATH in TutorialConfig.h.in
      • why not config before option?

03: Usage Requirement

  • ?? 💬 INTERFACE tell consumers what should be configed to use/link the target
    target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
    💬 use quote here?
  • 💬 keyword
    • INTERFACE: for consumer
    • PUBLIC: INTERFACE + PRIVATE
    • PRIVATE: for producer
  • no extra variables required for Tutorial now

04: Install & Test

Install

  • for the library (in MathFunctions/CMakeLists.txt)
    • install(TARGETS MathFunctions DESTINATION lib)
    • install(FILES MathFunctions.h DESTINATION include)
  • for the application (in top-level CMakeLists.txt)
    • install(TARGETS Tutorial DESTINATION bin)
    • install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h" DESTINATION include)
  • cmake --install (≧ v3.15)
    • < v3.15: cmake install
    • or build INSTALL target from IDE
  • CMAKE_INSTALL_PREFIX: where to install files
    • --prefix to customize
  • 💬 why/when do we install? locally install

Test

  • enable_testing()
  • test cases
    • run without crash or error code?
    • is output match the regular expression?
    • custom function to simplify things
  • run ctest -N (dry run), ctest -VV (extra verbose) under binary tree 💬 options
    • for multi-config (e.g. Visual Studio)
      • ctest -C Debug -VV
      • or build RUN_TESTS target from IDE

05: System Introspection

  • why? assume log/exp are not common (require m library instead)
  • how?
    1. using the module
      include(CheckSymbolExists)
    2. check symbols
      check_symbol_exists(log "math.h" HAVE_LOG)
      check_symbol_exists(exp "math.h" HAVE_EXP)
    3. ensure variables are set before config
      #cmakedefine HAVE_LOG
      #cmakedefine HAVE_EXP
    4. use HAVE_LOG/HAVE_EXP where in need
    5. remember to include TutorialConfig.h
      target_include_directories(MathFunctions … PRIVATE ${CMAKE_BINARY_DIR})

Compile Definition

  • why? define HAVE_LOG/HAVE_EXP without TutorialConfig.h
  • how?
    1. remove defines from TutorialConfig.h
      • no more inclusion and include path as well
    2. check symbols directly in MathFunctions/CMakeLists.txt
    3. if(HAVE_LOG AND HAVE_EXP), specify private definitions
      target_compile_definitions(MathFunctions PRIVATE "HAVE_LOG" "HAVE_EXP")
      💬 is quote required?

06: Custom Command & Generated File

  • why? look up the precomputed table that generated while compile time
  • how?
    1. not to check for log/exp anymore, and remove HAVE_LOG/HAVE_EXP
      • also remove #include <cmath>
    2. build the generator
      add_executable(MakeTable MakeTable.cxx)
    3. generate the table
      add_custom_command(OUTPUT …Table.h COMMAND MakeTable …Table.h DEPENDS MakeTable)
    4. specify that mysqrt.cxx depends on Table.h
      add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h)
    5. ensure Table.h can be included
      target_include_directories(MathFunctions … PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
    6. use the generated table
    7. see how cmake build this project

07: Installer

  • why? distribute our project to others
    • provide binary and source for different platforms
    • differ from step 04: package management
  • how?
    1. include(InstallRequiredSystemLibraries) to include required runtime libraries
    2. set the variables (license file, version) then include(CPack)
    3. build the project as usual
    4. distribute binary: cpack
      • -G: generator
      • -c: configuration
    5. distribute source: cpack --config CPackSourceConfig.cmake
    6. alternatively,
      • make package
      • or build Package target from IDE

08: Dashboard

  • why? for something like this
  • how?
    1. take include(CTest) instead of enable_testing()
    2. create CTestConfig.cmake at top-level directory, and specify variables
    3. run ctest -VV -D Experimental under binary tree

09: Static & Shared

  • why? let BUILD_SHARED_LIBS make add_library() become SHARED (if no type specified)
  • refactor for encapsulation
    1. move USE_MYMATH option to MathFunctions
    2. MathFunctions (static) links to SqrtLibrary if USE_MYMATH
    3. namespace
    4. Tutorial always use MathFunctions (no #include <cmath> required)
  • how?
    1. option(BUILD_SHARED_LIBS "…" ON)
      usually let user decide
    2. define DECLSPEC with target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")
    3. POSITION_INDEPENDENT_CODE 💬 wiki: Position Independent Code
    4. set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")

10: Generator Expression

  • why? do something conditional
  • logical expression
    • $<0:…> → empty string
    • $<1:…> → content of "…"
    • can be nested
  • example: INTERFACE target to add compiler flags
    • $<COMPILE_LANG_AND_ID:language,compiler_ids> different flags for gcc/msvc
    • $<BUILD_INTERFACE:…> not to apply on the consumers after installed

11: Export Config

  • why? make our project relocatable
    • to be used from 1) build directory, 2) local install, or 3) when packaged
  • how?
    1. have CMake file to import all installed targets
      install(TARGETS … EXPORT MathFunctionsTargets)
    2. explicitly install this generated file
      install(EXPORT … FILE MathFunctionsTargets.cmake DESTINATION …)
    3. (try and get an error) have a machine independent path
      target_include_directories(MathFunctions INTERFACE … $<install_interface:include>)
    4. have CMake file for find_package() by CMakePackageConfigHelpers
      • prepare Config.cmake.in
      • include(CMakePackageConfigHelpers)
      • configure_package_config_file(…) for MathFunctionsConfig.cmake
      • write_basic_package_version_file(…) for MathFunctionsConfigVersion.cmake
    5. used from build directory (also export one for ourself)
      export(EXPORT MathFunctionsTargets FILE "${CMAKE_CURRENT_BINSRY_DIR}/MathFunctionsTargets.cmake")

12: Package Debug+Release

  • why? make a package with multiple configs
    • for single-config generators
  • how?
    1. different names for different configs postfix d for debug
      set(CMAKE_DEBUG_POSTFIX d)
    2. apply debug postfix on executable
      set_target_properties(Tutorial PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX})
    3. add version numbers
      set_property(TARGET MathFunctions PROPERTY VERSION "1.0.0")
      set_property(TARGET MathFunctions PROPERTY SOVERSION "1")
    4. create debug and release sub-directories, and build both 2 configs
    5. customize MultiCPackConfig.cmake to pack 2 builds into one
      • include default config file
      • use CPACK_INSTALL_CMAKE_PROJECTS variable
    6. cpack --config MultiCPackConfig.cmake