Build external library only once with CMake

前端 未结 2 1790
庸人自扰
庸人自扰 2020-12-10 05:38

My C++ projects includes the source code of a third-party library (currently as a git submodule).

This library is added to the project by our main CMakelists through

相关标签:
2条回答
  • 2020-12-10 05:53

    Based on the excellent answer by @Hikke, I wrote two macros to simplify using external projects.

    Code

    include(ExternalProject)
    
    #
    #   Add external project.
    #
    #   \param name             Name of external project
    #   \param path             Path to source directory
    #   \param external         Name of the external target
    #
    macro(add_external_project name path)
        # Create external project
        set(${name}_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${path})
        set(${name}_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/${path})
        ExternalProject_Add(${name}
            SOURCE_DIR "${${name}_SOURCE_DIR}"
            BINARY_DIR "${${name}_BINARY_DIR}"
            CMAKE_ARGS "-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}"
                       "-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}"
                       "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}"
                       # These are only useful if you're cross-compiling.
                       # They, however, will not hurt regardless.
                       "-DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}"
                       "-DCMAKE_SYSTEM_PROCESSOR=${CMAKE_SYSTEM_PROCESSOR}"
                       "-DCMAKE_AR=${CMAKE_AR}"
                       "-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}"
                       "-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}"
                       "-DCMAKE_RC_COMPILER=${CMAKE_RC_COMPILER}"
                       "-DCMAKE_COMPILER_PREFIX=${CMAKE_COMPILER_PREFIX}"
                       "-DCMAKE_FIND_ROOT_PATH=${CMAKE_FIND_ROOT_PATH}"
           INSTALL_COMMAND ""
        )
    
    endmacro(add_external_project)
    
    #
    #   Add external target to external project.
    #
    #   \param name             Name of external project
    #   \param includedir       Path to include directory
    #   \param libdir           Path to library directory
    #   \param build_type       Build type {STATIC, SHARED}
    #   \param external         Name of the external target
    #
    macro(add_external_target name includedir libdir build_type external)
        # Configurations
        set(${name}_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/${libdir})
    
        # Create external library
        add_library(${name} ${build_type} IMPORTED)
        set(${name}_LIBRARY "${${name}_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${CMAKE_${build_type}_LIBRARY_PREFIX}${name}${CMAKE_${build_type}_LIBRARY_SUFFIX}")
    
        # Find paths and set dependencies
        add_dependencies(${name} ${external})
        set(${name}_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/${includedir}")
    
        # Set interface properties
        set_target_properties(${name} PROPERTIES IMPORTED_LOCATION ${${name}_LIBRARY})
        set_target_properties(${name} PROPERTIES INCLUDE_DIRECTORIES ${${name}_INCLUDE_DIR})
    endmacro(add_external_target)
    

    Explanation

    The first macro creates the external project, which does the entire external build step, while the second step sets the necessary dependencies and defines the interface. Separating the two is important, because most projects have more than one interface/library.

    Example

    Say I have GoogleTest as a submodule in my project, located in the googletest subfolder. I can use the following interface to define the gtest and gtest_main macros, very similar to how Googletest itself does it.

    add_external_project(googletest_external googletest)
    add_external_target(gtest googletest/googletest/include googletest/googlemock/gtest STATIC googletest_external)
    add_external_target(gtest_main googletest/googletest/include googletest/googlemock/gtest STATIC googletest_external)
    

    I can then link my target to googletest much like before:

    target_link_libraries(target_tests
        gtest
        gtest_main
        # The CMAKE_THREAD_LIBS_INIT can be found from `find_package(Threads)`
        # and is required for all but MinGW builds.
        ${CMAKE_THREAD_LIBS_INIT}
    )
    

    This should provide sufficient boilerplate to simplify the actual external build process, even with CMake-driven projects.

    0 讨论(0)
  • 2020-12-10 06:15

    As @Tsyvarev said, in your case ExternalProject_Add is better than add_subdirectory. add_subdirectory is good when you want project to be essential part of your build system because target it creates can be used in in the right-hand-side of the target_link_libraries() command while target created by ExternalProject_Add cannot.

    Here is the approach I used in one of my projects. You try to find required library and build it only if it was not found. I use INTERFACE library to turn FOO_EXTERNAL into a target acceptable by target_link_libraries().

    add_library(foo INTERFACE)
    find_package(foo ${FOO_VER})
    if(NOT foo_FOUND)
        include(ExternalProject)
        include(GNUInstallDirs)
        ExternalProject_Add(FOO_EXTERNAL
                        SOURCE_DIR "${FOO_SOURCE_DIR}"
                        BINARY_DIR "${FOO_BINARY_DIR}"
                        INSTALL_DIR "${FOO_INSTALL_DIR}"
                        CMAKE_ARGS "-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}"
                                   "-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}"
                                   "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}"
    
                                   "-DCMAKE_INSTALL_PREFIX=${FOO_INSTALL_DIR}"
                        )
    
        add_dependencies(foo FOO_EXTERNAL)
        set(foo_LIBRARY
                "${FOO_INSTALL_DIR}/${CMAKE_INSTALL_LIBDIR}/${CMAKE_STATIC_LIBRARY_PREFIX}foo${CMAKE_STATIC_LIBRARY_SUFFIX}")
        set(foo_INCLUDE_DIR "${FOO_INSTALL_DIR}/include")
    endif()
    
    target_link_libraries(foo INTERFACE ${foo_LIBRARY})
    target_include_directories(foo INTERFACE ${foo_INCLUDE_DIR})
    
    0 讨论(0)
提交回复
热议问题