The Ultimate Guide to Modern CMa

2021-04-27  本文已影响0人  XBruce

CMake is a great tool for managing a C++ system’s build. It builds quickly, supports the major use cases, and is quite flexible. The problem is, it’s too flexible, and for people used to writing Makefiles themselves, it’s not always obvious what CMake commands and properties you should be using. If you’re used to just splatting some compiler flags into the CXX_FLAGS environment variable, you might just do that in CMake as well, even though it supports better ways to manage your build.

It occurred to me recently that I need a gold standard reference for laying out a CMake project. There are a lot of “CMake tutorial” style articles out there, but they’re of the form add_executable, add_include_directories, bam done. However, those projects are not easily portable, and their libraries are not easily reused in different contexts.

Instead, I want CMake to do proper dependency management for me. I don’t want to be managing include directories (especially transitive include directories!), linker command lines and all that jazz. And I want to be able to import my libraries So I made this page to refer myself and others to in the future, so we can have a single point of truth and reference for these docs.

Most of this is gleaned from Daniel Pfeifer’s presentation and the CMake docs.

Requirements

I have a number of requirements on my build:

To achieve all this, we’ll need to:

Directory layout

Obviously we’re going to be wanting an out-of-source build, so I’ll start with a top-level src directory. That way I can make a build directory next to it.

/
    build/                      <-- out-of-source build
    src/
        CMakeLists.txt
        mylibrary/
            CMakeLists.txt
            include/
                mylibrary/
                    mylibrary.h
            src/
                lib.cpp
                frob.cpp
            test/
                testlib.cpp
        myapp/
            CMakeLists.txt
            src/
                myapp.cpp
                quux.cpp
        libs/
            (buildable 3rd party libs that you want to vend for
                                                    convenience)
            libfoo/
                CMakeLists.txt
                ...

Top-level CMakeLists.txt

The top-level CMake file is there to bring all modules into scope. That means, adding the subdirectories for all CMake projects in this tree, and finding external libraries and turning them into imported targets.

# At LEAST 2.8 but newer is better
cmake_minimum_required(VERSION 3.2 FATAL_ERROR)
project(myproject VERSION 0.1 LANGUAGES CXX)

# Must use GNUInstallDirs to install libraries into correct
# locations on all platforms.
include(GNUInstallDirs)

# Include Boost as an imported target
find_package(Boost REQUIRED)
add_library(boost INTERFACE IMPORTED)
set_property(TARGET boost PROPERTY
    INTERFACE_INCLUDE_DIRECTORIES ${Boost_INCLUDE_DIR})

# Some other library that we import that was also built using CMake
# and has an exported target.
find_package(MyOtherLibrary REQUIRED)

# Targets that we develop here
enable_testing()
add_subdirectory(liblib)
add_subdirectory(app)

Library

This file has the most going on, because it needs to be the most flexible.

# Define library. Only source files here!
project(liblib VERSION 0.1 LANGUAGES CXX)

add_library(lib
    src/lib.cpp
    src/frob.cpp)

# Define headers for this library. PUBLIC headers are used for
# compiling the library, and will be added to consumers' build
# paths.
target_include_directories(lib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
    PRIVATE src)

# If we have compiler requirements for this library, list them
# here
target_compile_features(lib
    PUBLIC cxx_auto_type
    PRIVATE cxx_variadic_templates)

# Depend on a library that we defined in the top-level file
target_link_libraries(lib
    boost
    MyOtherLibrary)

# 'make install' to the correct locations (provided by GNUInstallDirs).
install(TARGETS lib EXPORT MyLibraryConfig
    ARCHIVE  DESTINATION ${CMAKE_INSTALL_LIBDIR}
    LIBRARY  DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME  DESTINATION ${CMAKE_INSTALL_BINDIR})  # This is for Windows
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

# This makes the project importable from the install directory
# Put config file in per-project dir (name MUST match), can also
# just go into 'cmake'.
install(EXPORT MyLibraryConfig DESTINATION share/MyLibrary/cmake)

# This makes the project importable from the build directory
export(TARGETS lib FILE MyLibraryConfig.cmake)

# Every library has unit tests, of course
add_executable(testlib
    test/testlib.cpp)

target_link_libraries(testlib
    lib)

add_test(testlib testlib)

Program

# Define an executable
add_executable(app
    src/app.cpp
    src/quux.cpp)

# Define the libraries this project depends upon
target_link_libraries(app
    lib)

Commands to run

cd build

# Building
cmake ../src && make

# Testing
make test
ctest --output-on-failure

# Building to a different root directory
cmake -DCMAKE_INSTALL_PREFIX=/opt/mypackage ../src

Importing external libraries

If the library you’re importing was not built with CMake, you’ll define an imported target somewhere in your top-level CMake file (or perhaps in an included file if you have a lot of them). For example, this is how you import the Boost header library and a compiled Boost library:

find_package(Boost REQUIRED iostreams)

add_library(boost INTERFACE IMPORTED)
set_property(TARGET boost PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${Boost_INCLUDE_DIRS})

add_library(boost-iostreams SHARED IMPORTED)
set_property(TARGET boost-iostreams PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${Boost_INCLUDE_DIRS})
set_property(TARGET boost-iostreams PROPERTY IMPORTED_LOCATION ${Boost_IOSTREAMS_LIBRARY})

If, on the other hand, the external library you need was built with CMake and it was following this guide (i.e., cleanly defined its own headers and sources, and it was exported), then you can simply do this:

# CMakeLists
find_package(MyLibrary)

# Build like this:
cmake ../src -DMyLibrary_DIR=/path/to/mylibrary/build

# Passing the MyLibrary_DIR is not necessary if you
# 'make install'ed the project.

In case of an external target, obviously CMake can’t track the binary to its sources so won’t automatically rebuild your external project.

A short note on transitive dependencies: if the library you EXPORT depends on any targets, those targets will be recorded in the MyLibraryConfig.cmake file by name. This means that if you import this target into a separate project file, that project must have targets with the same name. So when importing an external target, you’ll need to have find_package()d its dependencies already.

I currently don’t know of any way to hide these transitive dependencies from consumers. At work, we integrate CMake into a bigger build/dependency management system, and we post-process the generated xxxConfig.cmake files to insert additonal find_package() commands into the config files themselves. This works, but requires a postprocessing step that may not be easily achievable if you don’t have a bigger build orchestration framework to hook into. Also, it requires that the target name and the config file name are exactly the same (whereas in my example up there the target name was lib but the config file name was MyLibrary). On the other hand, that seems like good practice anyway.

Integrating code generators

Goals:

Leading to something like this:

add_custom_command(
    OUTPUT file.output
    COMMAND tool --options "${CMAKE_CURRENT_SOURCE_DIR}/file.gen"
    MAIN_DEPENDENCY file.gen
    DEPENDS tool)

# Both for source files and header files
add_library(target
    ...
    file.output)

# For headers
target_include_directories(target
    ...
    PRIVATE ... ${CMAKE_CURRENT_BINARY_DIR})

The tool’s cwd is CMAKE_CURRENT_BINARY_DIR so source file paths must be qualified with ${CMAKE_CURRENT_SOURCE_DIR} in the command.

If you don’t have an existing target to add the generated files to, create a custom target just for adding the dependency:

add_custom_target(flow-diagram ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/file.output)

Packaging

I won’t need to learn deeply about this part for my job (just yet), so I don’t have a lot of tips here. Instead, I’ll link to guides other people have written:

上一篇 下一篇

猜你喜欢

热点阅读