美文网首页
The Ultimate Guide to Modern CMa

The Ultimate Guide to Modern CMa

作者: XBruce | 来源:发表于2021-04-27 14:54 被阅读0次

    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:

    • I want to define nothing more than modules, and dependencies between modules. CMake should automatically figure out transitive dependencies and set up the include paths and linker paths correctly. If I’m not using Boost, but my dependency is, and I have to think about that, I’ve lost.
    • When I define my module, without extra effort I want it to:
      • Package correctly using CPack.
      • Install correctly into /usr/local.
      • Install 64-bit libraries into the correct directories on all platforms (be it lib64 or gnu-linux-x86_64).
      • Be transitively linkable with nothing more than target_link_libraries() in the SAME CMake build.
      • Be transitively linkable with nothing more than and import command and target_link_libraries() when imported from an external CMake file.
    • I don’t want too many CMakeLists files; I’ll lose track of them, and having a zillion open is just annoying. I’ll prefer to have one CMakeLists file per component, not necessarily per directory.
    • A library’s public header files should be namespaced into a directory (i.e., include <mylib/header.h>–it’s the only way to stay sane.

    To achieve all this, we’ll need to:

    • Organize our source tree into a directory per module, with a CMakeLists per module. We’ll use a parent CMakeLists file to tie all modules together, but this top-file should be OPTIONAL and only be taking care of bringing all required modules/dependencies into scope.
    • Make a distinction between public and private headers for a library. We’ll put the public headers in a directory called include/mylib such that we can just add that include directory to our search path to be able to include the headers as <mylib/header.h>.
    • We’re going to be using imported targets for libraries that don’t have a CMake file. That means, no longer are we going to find_package a dependency and poke the include paths and library names directly into our own project.

    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:

    • We want to rebuild the generation tool on-demand.
    • Generated files go in the build tree, not the source tree.

    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:

    相关文章

      网友评论

          本文标题:The Ultimate Guide to Modern CMa

          本文链接:https://www.haomeiwen.com/subject/edaurltx.html