Build external library only once with CMake

2019-02-17 07:23发布

问题:

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 the use of add_subdirectory, and then the library is linked with the main target.

Here is a reduced version of my current Cmake file :

add_subdirectory(foo)
set(FOO_LIBRARY ${CMAKE_CURRENT_SOURCE_DIR}/libfoo/libfoo.so)

add_executable(target main.cpp)
add_dependencies(target foo)
target_link_libraries(target ${FOO_LIBRARY})

This library takes a long time to build and, since I don't change its code I need it built only once (per build configuration). But when I clean and rebuild my code it also cleans the library files and recompile them.

I have tried to set the property CLEAN_NO_CUSTOM in the library's directory, but according to the documentation it only works for custom command targets.

Is there a mechanism in CMake through which it is possible to specify that this library target needs to be generated only once, or alternatively not cleaned by make clean ?

回答1:

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})


回答2:

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.



标签: c++ build cmake