使用 CMake 只构建一次外部库

Posted

技术标签:

【中文标题】使用 CMake 只构建一次外部库【英文标题】:Build external library only once with CMake 【发布时间】:2016-11-02 14:22:23 【问题描述】:

我的 C++ 项目包含第三方库的源代码(目前为 git 子模块)。

这个库是我们的主CMakelists通过add_subdirectory添加到项目中的,然后这个库与主目标链接。

这是我当前 Cmake 文件的简化版本:

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)

这个库需要很长时间才能构建,而且由于我不更改它的代码,我只需要构建一次(每个构建配置)。但是当我清理和重建我的代码时,它也会清理库文件并重新编译它们。

我尝试在库目录中设置属性CLEAN_NO_CUSTOM,但根据文档,它仅适用于自定义命令目标。

CMake 中是否有一种机制可以指定这个库目标只需要生成一次,或者不需要由make clean 清理?

【问题讨论】:

由于您不使用第三方库的internal 目标,因此使用ExternalProject_Add 的方法似乎比add_subdirectory 更好。由于ExternalProject_Add 没有指定清理规则,CMake 不会尝试清理库。 【参考方案1】:

正如@Tsyvarev 所说,在你的情况下ExternalProject_Addadd_subdirectory 好。当您希望项目成为构建系统的重要组成部分时,add_subdirectory 很好,因为它创建的目标可以在target_link_libraries() 命令的右侧使用,而ExternalProject_Add 创建的目标不能。

这是我在一个项目中使用的方法。您尝试找到所需的库并仅在未找到时才构建它。我使用 INTERFACE 库将 FOO_EXTERNAL 转换为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_PREFIXfoo$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】:

基于@Hikke 的出色回答,我编写了两个宏来简化外部项目的使用。

代码

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)

说明

第一个宏创建外部项目,它完成整个外部构建步骤,而第二个步骤设置必要的依赖关系并定义接口。将两者分开很重要,因为大多数项目都有不止一个接口/库。

示例

假设我的项目中有GoogleTest 作为子模块,位于googletest 子文件夹中。我可以使用以下接口来定义gtestgtest_main 宏,非常类似于Googletest 本身的做法。

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)

然后我可以像以前一样将我的目标链接到 googletest:

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
)

这应该提供足够的样板来简化实际的外部构建过程,即使是 CMake 驱动的项目。

【讨论】:

以上是关于使用 CMake 只构建一次外部库的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 CMake 构建和使用外部库

使用外部附加头文件构建 cmake 对象库

如何构建使用 CMake FetchContent 下载的外部库?

注册。使用 CMake 将外部库链接到项目

CMake:如何构建外部项目并包含其目标

CMake 链接到外部库