Jun 19, 2022

[CMake] Modern CMake in style

Reference:
Modern CMake Modules - Bret Brown - CppCon 2021
CppCon 2017: Mathieu Ropert “Using Modern CMake Patterns to Enforce a Good Modular Design”


$ clang++ main.cpp -o vsdmars -Wreturn-type
is same as:
target_compile_options(
    vsdmars PRIVATE
    $<$<COMPILE_LANG_AND_ID:CXX,Clang,GNU>:-Werror=return-type>
)

Find Modules

Loaded by the find_package()
Find<PackageName>.cmake // can have find_library(...) command


Use Modern CMake

  1. declare module with ADD_LIBRARY or ADD_EXECUTABLE
  2. declare build flags with TARGET_xxx()
  3. declare dependencies with TARGET_LINK_LIBRARIES
  4. Specify what is PUBLIC and what is PRIVATE
  5. Consider people are using your library through CMake


Don't

  1. breaking cmake target model
  2. breaking cmake features
    dev workflows
    functions
    standard modules
  3. making changes in source directories
  4. avoid version control operations
  5. toolchain details
    linkers
    compiler
  6. build requires magic flags like -DMAGIC_VAR
  7. Hijacking CMake variables
  8. Requiring special build targets
  9. Expecting certain environments
$ cmake && cmake --build && ctest && cmake --install


Write a module

name the file

vsdmars.cmake // include(vsdmars)
FindVsdmars.cmake // find_package(vsdmars REQUIRED)
vsdmarsConfig.cmake // find_package(vsdmars REQUIRED) <--- Most powerful

e.g.
vsdmarsConfig.cmake
function(target_needs_news)
    cmake_parse_arguments(parsed
        ""             #   options
        "TARGET"       # one-value keywords
        ""             # multi-value keywords
        ${ARGN}        # strings to parse
    )
    # TODO: defensive argument parsing
    set(target ${parsed_TARGET})
    message(VERBOSE "done, ${target}")
endfunction()


Defensive argument parsing

if(parsed_UNPARSED_ARGUMENTS)
    message(FATAL_ERROR
        "BAD ARGUMENT: ${parsed_UNPARSED_ARGUMENTS}"
    )
endif()

if(parsed_KEYWORDS_MISSING_VALUES)
    message(...)
endif()


Essential for CMake modules

  1. API documentation
    README
  2. Testing the cmake module we just wrote
    https://crascit.com/2016/10/18/test-fixtures-with-cmake-ctest/
  3. message() with verbosity
$ cmake --log-level=verbose


Installing a CMake modules

  1. share/cmake/vsdmars/vsdmarsConfig.cmake
  2. ship version files as well if needed


CMakeLists.txt for CMake modules

cmake_minimum_required(VERSION 3.21)
project(vsdmars LANGUAGES NONE) # if module is lang agnostic
enable_testing() # every CMakeLists.txt should have this

install(
    FILES vsdmarsConfig.cmake
    DESTINATION share/cmake/vsdmars
    COMPONENT vsdmars
)


Development Setup

CMAKE_GENERATOR=Ninja
CMAKE_TOOLCHAIN_FILE=path/to/ToolChain.cmake
CMAKE_BUILD_TYPE=Debug
DESTDIR=your/staging/dir


Development Workflow

$ git clone ...
$ cmake -B build -S vsdmars
$ cmake --build build
$ ctest --test-dir build
$ cmake --instal build \
    --prefix /opt/...
    --component vsdmars



Integration Workflow

$ git clone ...
$ cmake -B cxx-build \
    -S cxx-project \
    -DVSDMARS_DIR=./vsdmars/
$ cmake --build cxx-build
$ ctest --ctest-dir cxx-build
$ cmake --install cxx-build \
    --prefix /opt/vsdmars \
    --component cxx-project


Build flags don't scale

  • every change in public flags has to be propagated upwards
  • Most people give up and put every include director in a common/root build file.
  • Correct way of doing so is to put all 3rd party
    my/include/namespace
    into
    ./include/my/include/namespace


Modern build systems

  • Forbid/report circular and hidden dependencies
  • Help developer reason at module level
  • Do more than build as you are told


How?

  • Define your module build flags
  • Define your module dependencies
  • Keep out of other modules internals
  • Each module has a set of private flags
  • Each module has a set of public flags(required to build against its interface)
  • Build interfaces are transitive
  • Private means .cpp includes other headers
  • Public means .h includes other headers
  • Build flags are not gone but encapsulated
  • Can still with CPPFLAGS, CXXFLAGS and LDFLAGS, and external flags are not our concern anymore
  • Declare module with ADD_LIBRARY or ADD_EXECUTABLE
  • Declare build flags with TARGET_xxx()
  • Declare dependencies with TARGET_LINK_LIBRARIES
  • Specify what is PUBLIC and PRIVATE


Global setup

cmake_minimum_required(VERSION 3)
add_compile_options(-Wall -Werror etc.) // including flags effect ABI
add_library(vsdmarsLib src/xxx.cpp)



Declare flags

target_include_directories(vsdmarsLib PUBLIC include)
target_include_directories(vsdmarsLib PRIVATE src)

if (SOME_SETTING)
    target_compile_definitions(vsdmarsLIB
        PUBLIC WITH_SOME_SETTING
endif()


Declare dependencies

target_link_libraries(vsdmarsLib PUBLIC abc)
target_link_libraries(vsdmarsLib PRIVATE xyz)


Header only libraries

# nothing to build actually, just export headers during install/being pulled by other modules
add_library(vsdmarsLibHeaderOnly INTERFACE)
target_include_directories(vsdmarsLibHeaderOnly INTERFACE include)
target_link_libraries(vsdmarsLibHeaderOnly INTERFACE Boost::Boost)



Antipatterns



3rd party

cmake_minimum_required(VERSION 3.5)
find_package(GTest)
add_executable(foo ...)
target_link_libraries(foo GTest::GTest GTest::Main)
find_library(XXX_LIB xxx HINTS ${XXX_DIR}/lib)
add_library(xxx SHARED IMPORTED LOCATION ${XXX_LIB})
target_include_directories(xxx INTERFACE ${XXX_DIR}/include)
target_link_libraries(xxx INTERFACE Boost::boost)

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.