CMake:如何构建外部项目并包含它们的目标

143
我可以帮助您进行翻译。这段内容是关于一个名为"A项目"的工程,它将一个静态库作为目标进行导出。
install(TARGETS alib DESTINATION lib EXPORT project_a-targets)
install(EXPORT project_a-targets DESTINATION lib/alib)

现在我想将项目A作为外部项目从项目B中使用,并包含其构建目标:
ExternalProject_Add(project_a
  URL ...project_a.tar.gz
  PREFIX ${CMAKE_CURRENT_BINARY_DIR}/project_a
  CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
)

include(${CMAKE_CURRENT_BINARY_DIR}/lib/project_a/project_a-targets.cmake)

问题在于当运行项目B的CMakeLists时,包含文件尚不存在。
是否有一种方法可以使包含依赖于外部项目的构建?
更新:我根据这个和其他常见问题写了一个基于CMake by Example教程的简短教程。
7个回答

82
我认为您在这里混淆了两种不同的范例。
正如您所指出的,高度灵活的 ExternalProject 模块在构建时运行其命令,因此您无法直接使用 Project A 的导入文件,因为它只有在安装 Project A 后才会被创建。
如果您想要 include Project A 的导入文件,您必须在调用 Project B 的 CMakeLists.txt 之前手动安装 Project A - 就像通过这种方式或通过 find_file / find_library / find_package 添加的任何其他第三方依赖项一样。
如果您想要使用 ExternalProject_Add,您需要向您的 CMakeLists.txt 添加类似以下内容:
ExternalProject_Add(project_a
  URL ...project_a.tar.gz
  PREFIX ${CMAKE_CURRENT_BINARY_DIR}/project_a
  CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
)

<strike>include(${CMAKE_CURRENT_BINARY_DIR}/lib/project_a/project_a-targets.cmake)</strike>

ExternalProject_Get_Property(project_a install_dir)
include_directories(${install_dir}/include)

add_dependencies(project_b_exe project_a)
target_link_libraries(project_b_exe ${install_dir}/lib/alib.lib)

2
谢谢你的回答。你所建议的与我之前的想法相似。 我希望找到一种方法来利用导出的目标,因为它似乎比手动指定库路径更好用。 - mirkokiefer
7
我想避免在我的源代码树中包含外部项目的源代码。如果 ExternalProject_Add 的行为就像 add_subdirectory 并公开所有目标,那将是很好的。你上面描述的解决方案可能仍然是最干净的。 - mirkokiefer
2
考虑将它们都作为ExternalProject进行构建,然后让B依赖于A,接着项目B的CMakeLists文件会包含来自项目A的目标文件,但是您的“Super Build” CMakeLists将只构建A和B,两者都作为ExternalProjects... - DLRdave
3
@DLRdave - 我看到有几次推荐使用Super Build解决方案,但我不确定它相对于仅通过ExternalProject包含一些外部项目提供了哪些优势。是一致性更好,还是更加规范,或者其他原因?我肯定我在这里缺少一些基本的东西。 - Fraser
9
这个解决方案的问题之一是我们刚刚硬编码了库名(alib.lib),这使得构建系统不跨平台,因为不同的操作系统使用不同的共享库命名规则,适应这些不同的命名规则是CMake的一个特点。 - nsg
显示剩余5条评论

30
这篇文章有一个合理的答案:

CMakeLists.txt.in

cmake_minimum_required(VERSION 2.8.2)

project(googletest-download NONE)

include(ExternalProject)
ExternalProject_Add(googletest
  GIT_REPOSITORY    https://github.com/google/googletest.git
  GIT_TAG           master
  SOURCE_DIR        "${CMAKE_BINARY_DIR}/googletest-src"
  BINARY_DIR        "${CMAKE_BINARY_DIR}/googletest-build"
  CONFIGURE_COMMAND ""
  BUILD_COMMAND     ""
  INSTALL_COMMAND   ""
  TEST_COMMAND      ""
)

CMakeLists.txt:

的翻译是:

CMakeLists.txt:

# Download and unpack googletest at configure time
configure_file(CMakeLists.txt.in
               googletest-download/CMakeLists.txt)
execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" .
  WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download )
execute_process(COMMAND ${CMAKE_COMMAND} --build .
  WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download )

# Prevent GoogleTest from overriding our compiler/linker options
# when building with Visual Studio
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)

# Add googletest directly to our build. This adds
# the following targets: gtest, gtest_main, gmock
# and gmock_main
add_subdirectory(${CMAKE_BINARY_DIR}/googletest-src
                 ${CMAKE_BINARY_DIR}/googletest-build)

# The gtest/gmock targets carry header search path
# dependencies automatically when using CMake 2.8.11 or
# later. Otherwise we have to add them here ourselves.
if (CMAKE_VERSION VERSION_LESS 2.8.11)
  include_directories("${gtest_SOURCE_DIR}/include"
                      "${gmock_SOURCE_DIR}/include")
endif()

# Now simply link your own targets against gtest, gmock,
# etc. as appropriate

不过这种方法似乎很不专业。我想提出另一种替代方案——使用Git子模块。

cd MyProject/dependencies/gtest
git submodule add https://github.com/google/googletest.git
cd googletest
git checkout release-1.8.0
cd ../../..
git add *
git commit -m "Add googletest"

然后在MyProject/dependencies/gtest/CMakeList.txt中,您可以进行以下操作:

cmake_minimum_required(VERSION 3.3)

if(TARGET gtest) # To avoid diamond dependencies; may not be necessary depending on you project.
    return()
endif()

add_subdirectory("googletest")

我还没有进行过深入的尝试,但它似乎更加简洁。

编辑:这种方法存在一个缺点:子目录可能会运行你不想要的install()命令。这篇帖子提供了一种禁用它们的方法,但它存在错误,并且对我没有起作用。

编辑2:如果使用add_subdirectory("googletest" EXCLUDE_FROM_ALL),则似乎默认情况下不会使用子目录中的install()命令。


这可能只是我过于谨慎了,因为这只是一个示例,gtest可能非常稳定,但我强烈建议在克隆时始终使用特定的GIT_TAG,否则您可能会失去构建可重复性,因为两年后运行构建脚本的人将得到与您不同的版本。 CMake的文档也建议这样做。 - jrh
为什么include(ExternalProject)不是CMake默认的关键字之一? - Sandburg

12

编辑:CMake现在内置了此功能的支持。请参见使用FetchContent新答案

您还可以在辅助构建过程中强制构建所依赖的目标。

有关类似主题的内容,请参见我的回答


5

我正在寻找类似的解决方案。这里的回复和上面的教程很有信息量。我学习了这里提到的帖子/博客来构建我自己的成功。我发布了完整的 CMakeLists.txt,对我非常有用。我想这将作为初学者的基本模板非常有帮助。

"CMakeLists.txt"

cmake_minimum_required(VERSION 3.10.2)

# Target Project
project (ClientProgram)

# Begin: Including Sources and Headers
include_directories(include)
file (GLOB SOURCES "src/*.c")
# End: Including Sources and Headers


# Begin: Generate executables
add_executable (ClientProgram ${SOURCES})
# End: Generate executables


# This Project Depends on External Project(s) 
include (ExternalProject)

# Begin: External Third Party Library
set (libTLS ThirdPartyTlsLibrary)
ExternalProject_Add (${libTLS}
PREFIX          ${CMAKE_CURRENT_BINARY_DIR}/${libTLS}
# Begin: Download Archive from Web Server
URL             http://myproject.com/MyLibrary.tgz
URL_HASH        SHA1=<expected_sha1sum_of_above_tgz_file>
DOWNLOAD_NO_PROGRESS ON
# End: Download Archive from Web Server

# Begin: Download Source from GIT Repository
#    GIT_REPOSITORY  https://github.com/<project>.git
#    GIT_TAG         <Refer github.com releases -> Tags>
#    GIT_SHALLOW     ON
# End: Download Source from GIT Repository

# Begin: CMAKE Comamnd Argiments
CMAKE_ARGS      -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_CURRENT_BINARY_DIR}/${libTLS}
CMAKE_ARGS      -DUSE_SHARED_LIBRARY:BOOL=ON
# End: CMAKE Comamnd Argiments    
)

# The above ExternalProject_Add(...) construct wil take care of \
# 1. Downloading sources
# 2. Building Object files
# 3. Install under DCMAKE_INSTALL_PREFIX Directory

# Acquire Installation Directory of 
ExternalProject_Get_Property (${libTLS} install_dir)

# Begin: Importing Headers & Library of Third Party built using ExternalProject_Add(...)
# Include PATH that has headers required by Target Project
include_directories (${install_dir}/include)

# Import librarues from External Project required by Target Project
add_library (lmytls SHARED IMPORTED)
set_target_properties (lmytls PROPERTIES IMPORTED_LOCATION ${install_dir}/lib/libmytls.so)
add_library (lmyxdot509 SHARED IMPORTED)
set_target_properties(lmyxdot509 PROPERTIES IMPORTED_LOCATION ${install_dir}/lib/libmyxdot509.so)

# End: Importing Headers & Library of Third Party built using ExternalProject_Add(...)
# End: External Third Party Library

# Begin: Target Project depends on Third Party Component
add_dependencies(ClientProgram ${libTLS})
# End: Target Project depends on Third Party Component

# Refer libraries added above used by Target Project
target_link_libraries (ClientProgram lmytls lmyxdot509)

2
我建议首先使用FetchContent:
include(FetchContent)
FetchContent_Declare(glog
    GIT_REPOSITORY    https://github.com/google/glog.git
    SOURCE_DIR        ${CMAKE_CURRENT_SOURCE_DIR}/third_party/glog
    SUBBUILD_DIR      third_party/glog/subbuild
    BINARY_DIR        third_party/glog/build
)
option(WITH_GFLAGS "" OFF)
option(WITH_GTEST "" OFF)
option(WITH_GMOCK "" OFF)
option(WITH_UNWIND "" OFF)
option(BUILD_SHARED_LIBS "" OFF)
option(BUILD_TESTING "" OFF)
FetchContent_MakeAvailable(glog)

....

add_library(libsomething STATIC)
target_link_libraries(libsomething PUBLIC glog::glog)


请注意,所有的CMake魔法都按预期工作:您不需要指定glog的包含目录或libsomething的构建产物。

1

cmake的ExternalProject_Add确实可以使用,但我不喜欢它的一点是在构建期间执行一些操作、持续轮询等等。我更喜欢在构建阶段仅构建项目,没有其他操作。我尝试过多次覆盖ExternalProject_Add,但很遗憾都没有成功。

然后我还尝试添加git子模块,但这会拉取整个git仓库,而在某些情况下我只需要部分git仓库。我已经检查过了 - 确实可以执行稀疏的git checkout,但这需要单独的函数,我在下面写了一个。

#-----------------------------------------------------------------------------
#
# Performs sparse (partial) git checkout
#
#   into ${checkoutDir} from ${url} of ${branch}
#
# List of folders and files to pull can be specified after that.
#-----------------------------------------------------------------------------
function (SparseGitCheckout checkoutDir url branch)
    if(EXISTS ${checkoutDir})
        return()
    endif()

    message("-------------------------------------------------------------------")
    message("sparse git checkout to ${checkoutDir}...")
    message("-------------------------------------------------------------------")

    file(MAKE_DIRECTORY ${checkoutDir})

    set(cmds "git init")
    set(cmds ${cmds} "git remote add -f origin --no-tags -t master ${url}")
    set(cmds ${cmds} "git config core.sparseCheckout true")

    # This command is executed via file WRITE
    # echo <file or folder> >> .git/info/sparse-checkout")

    set(cmds ${cmds} "git pull --depth=1 origin ${branch}")

    # message("In directory: ${checkoutDir}")

    foreach( cmd ${cmds})
        message("- ${cmd}")
        string(REPLACE " " ";" cmdList ${cmd})

        #message("Outfile: ${outFile}")
        #message("Final command: ${cmdList}")

        if(pull IN_LIST cmdList)
            string (REPLACE ";" "\n" FILES "${ARGN}")
            file(WRITE ${checkoutDir}/.git/info/sparse-checkout ${FILES} )
        endif()

        execute_process(
            COMMAND ${cmdList}
            WORKING_DIRECTORY ${checkoutDir}
            RESULT_VARIABLE ret
        )

        if(NOT ret EQUAL "0")
            message("error: previous command failed, see explanation above")
            file(REMOVE_RECURSE ${checkoutDir})
            break()
        endif()
    endforeach()

endfunction()


SparseGitCheckout(${CMAKE_BINARY_DIR}/catch_197 https://github.com/catchorg/Catch2.git v1.9.7 single_include)
SparseGitCheckout(${CMAKE_BINARY_DIR}/catch_master https://github.com/catchorg/Catch2.git master single_include)

我在下面添加了两个函数调用,只是为了说明如何使用该函数。
有些人可能不喜欢检出主分支/主干,因为它可能会出现问题 - 那么就可以始终指定特定的标签。
检出将仅执行一次,直到您清除缓存文件夹。

0

我还推荐对于简单项目使用FetchContent。 然而,有时需要更多对下载的库进行控制。 这时候ExternalProject很有用,因为它有许多定制选项。 关键在于理解你要下载的库。阅读它的CMakeLists.txt文件以了解如何修改安装目录,或者在构建或安装之前��要哪些配置。

以下示例将下载ZeroMQ(也称为zmq)。 该库需要在库内部运行一个bash脚本来正确配置事务,并且它还有一些选项可用于自定义安装。

我个人不喜欢标准的文件保存位置,也不需要将库安装到我的系统中。所以我想将其安装在我的项目中。因此,您可以按照自己的方式指定目录。

在我的项目中,我覆盖了CMAKE_INSTALL_PREFIX和CMAKE_INCLUDE_PREFIX以指向我希望文件存在的文件夹。

set(ZMQ_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}")
set(ZMQ_DIR "${CMAKE_CURRENT_BINARY_DIR}/ZeroMQ")
set(ZMQ_SRC_DIR "${ZMQ_DIR}/src")
set(ZMQ_INSTALL_INCLUDEDIR "${CMAKE_INCLUDE_PREFIX}/ZeroMQ")

include(ExternalProject)
ExternalProject_Add(ZeroMQ
  URL https://github.com/zeromq/libzmq/releases/download/v4.3.4/zeromq-4.3.4.tar.gz
  PREFIX "${ZMQ_DIR}/prefix"
  TMP_DIR "${ZMQ_DIR}/tmp"
  STAMP_DIR "${ZMQ_DIR}/stamp"
  DOWNLOAD_DIR "${ZMQ_DIR}"
  SOURCE_DIR "${ZMQ_SRC_DIR}"
  BINARY_DIR "${ZMQ_DIR}/build"
  CONFIGURE_COMMAND cd ${ZMQ_SRC_DIR} && ./configure --with-pic --prefix=${ZMQ_INSTALL_DIR}
  BUILD_COMMAND ""
  INSTALL_COMMAND cd ${ZMQ_SRC_DIR} && $(MAKE) install
)

请注意,ZeroMQ并不是实际的库目标,它只是我为ExternalProject目标选择的一个名称。库目标应该是zmq。因此,库文件应该是libzmq.so。
在构建或安装之后,您的主目标需要显示它依赖于外部库,以便按正确的顺序构建事物。 因此,在您的目标所在的CMakeLists.txt中(或任何父级CMakeLists.txt中),您需要添加以下内容:
include_directories("${CMAKE_INCLUDE_PREFIX}/ZeroMQ")
add_dependencies(your_target ZeroMQ)     # Depends on the ExternalProject
target_link_libraries(your_target zmq)   # Link the library target

如果你需要下载另一个依赖于zmq的外部库,你可以在ExternalProject_Add中添加一个DEPENDS参数。
ExternalProject_Add(
    cppzmqDownload
    URL https://github.com/zeromq/cppzmq/archive/refs/tags/v4.10.0.tar.gz
    DEPENDS ZeroMQ
    CMAKE_ARGS
      -DCMAKE_MODULE_PATH:PATH=${CMAKE_INSTALL_PREFIX}
      -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_INSTALL_PREFIX}
      -DCMAKE_INSTALL_INCLUDEDIR:PATH=${CMAKE_INCLUDE_PREFIX}/cppzmq
      -DCPPZMQ_BUILD_TESTS=OFF
      -DPC_LIBZMQ_INCLUDE_DIRS=${ZMQ_INSTALL_INCLUDEDIR}
      -DPC_LIBZMQ_LIBRARY_DIRS=${CMAKE_LIB_PREFIX}
)

我希望这能帮助到某人,尽管这是一个旧帖子。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接