CMake:如何设置源、库和 CMakeLists.txt 依赖项?

2021-12-26 00:00:00 cmake c++

我有几个项目(全部使用来自相同源代码树结构的 CMake 构建)都使用他们自己的数十个支持库的组合.

I have several projects (all building with CMake from the same source tree structure) all using their own mix out of dozens of supporting libraries.

所以我提出了如何在 CMake 中正确设置的问题.到目前为止,我只找到了 CMake 如何正确创建依赖关系目标,但我仍然在设置具有全局依赖项(项目级别确实知道一切)或本地依赖项(每个子级别目标仅处理自己的依赖项)的所有内容之间挣扎.

So I came about the question how to set up this correctly in CMake. So far I have only found CMake how to correctly create dependencies between targets, but I'm still struggling between setting up everything with global dependencies (the project level does know it all) or with local dependencies (each sub-level target only handles its own dependencies).

这是我的目录结构的简化示例以及我目前使用 CMake 和本地依赖项想到的内容(该示例仅显示一个可执行项目 App1,但实际上还有更多,App2App3 等):

Here is a reduced example of my directory structure and what I currently came up with using CMake and local dependencies (the example shows only one executable project, App1, but there are actually more, App2, App3, etc.):

Lib
+-- LibA
    +-- Inc
        +-- a.h
    +-- Src
        +-- a.cc
    +-- CMakeLists.txt
+-- LibB
    +-- Inc
        +-- b.h
    +-- Src
        +-- b.cc
    +-- CMakeLists.txt
+-- LibC
    +-- Inc
        +-- c.h
    +-- Src
        +-- c.cc
    +-- CMakeLists.txt
App1
+-- Src
    +-- main.cc
+-- CMakeLists.txt

Lib/LibA/CMakeLists.txt

include_directories(Inc ../LibC/Inc)
add_subdirectory(../LibC LibC)
add_library(LibA Src/a.cc Inc/a.h)
target_link_libraries(LibA LibC)

Lib/LibB/CMakeLists.txt

include_directories(Inc)
add_library(LibB Src/b.cc Inc/b.h)

Lib/LibC/CMakeLists.txt

include_directories(Inc ../LibB/Inc)
add_subdirectory(../LibB LibB)
add_library(LibC Src/c.cc Inc/c.h)
target_link_libraries(LibC LibB)

App1/CMakeLists.txt(为了便于复制,我在此生成源/头文件)

App1/CMakeLists.txt (for the ease of reproducing it I generate the source/header files here)

cmake_minimum_required(VERSION 2.8)

project(App1 CXX)

file(WRITE "Src/main.cc" "#include "a.h"
#include "b.h"
int main()
{
a();
b();
return 0;
}")
file(WRITE "../Lib/LibA/Inc/a.h" "void a();")
file(WRITE "../Lib/LibA/Src/a.cc" "#include "c.h"
void a()
{
c();
}")
file(WRITE "../Lib/LibB/Inc/b.h" "void b();")
file(WRITE "../Lib/LibB/Src/b.cc" "void b() {}")
file(WRITE "../Lib/LibC/Inc/c.h" "void c();")
file(WRITE "../Lib/LibC/Src/c.cc" "#include "b.h"
void c()
{
b();
}")

include_directories(
    ../Lib/LibA/Inc
    ../Lib/LibB/Inc
)

add_subdirectory(../Lib/LibA LibA)
add_subdirectory(../Lib/LibB LibB)

add_executable(App1 Src/main.cc)

target_link_libraries(App1 LibA LibB)

上面例子中的库依赖看起来像这样:

The library dependencies in the above example do look like this:

App1 -> LibA -> LibC -> LibB
App1 -> LibB

目前我更喜欢本地依赖项变体,因为它更易于使用.我只是在源级别使用 include_directories() 提供依赖项,在链接级别使用 target_link_libraries() 和在 CMake 级别使用 add_subdirectory().

At the moment I prefer the local dependencies variant, because it's easier to use. I just give the dependencies at the source level with include_directories(), at the link level with target_link_libraries() and at the CMake level with add_subdirectory().

有了这个,您不需要知道支持库之间的依赖关系,并且 - 使用 CMake 级别包含" - 您最终只会得到您真正使用的目标.果然,您可以让所有包含目录和目标在全球范围内都知道,并让编译器/链接器整理其余部分.但这对我来说似乎是一种膨胀.

With this you don't need to know the dependencies between the supporting libraries and - with the CMake level "includes" - you will only end-up with the targets you really use. Sure enough you could just make all include directories and targets be known globally and let the compiler/linker sort out the rest. But this seems like a kind of bloating to me.

我也尝试使用 Lib/CMakeLists.txt 来处理 Lib 目录树中的所有依赖项,但我最终拥有了很多 if ("${PROJECT_NAME}" STREQUAL ...) 检查以及我无法在不提供至少一个源文件的情况下创建对目标进行分组的中间库的问题.

I also tried to have a Lib/CMakeLists.txt to handle all the dependencies in the Lib directory tree, but I ended up having a lot of if ("${PROJECT_NAME}" STREQUAL ...) checks and the problem that I can't create intermediate libraries grouping targets without giving at least one source file.

所以上面的例子是到目前为止很好",但它抛出以下错误,因为你应该/不能添加 CMakeLists.txt 两次:

So the above example is "so far so good", but it throws the following error because you should/can not add a CMakeLists.txt twice:

CMake Error at Lib/LibB/CMakeLists.txt:2 (add_library):
  add_library cannot create target "LibB" because another target with the
  same name already exists.  The existing target is a static library created
  in source directory "Lib/LibB".
  See documentation for policy CMP0002 for more details.

目前我看到了两种解决方案,但我认为我的方法太复杂了.

At the moment I see two solutions for this, but I think I got this way too complicated.

1. 覆盖add_subdirectory() 以防止重复

function(add_subdirectory _dir)
    get_filename_component(_fullpath ${_dir} REALPATH)
    if (EXISTS ${_fullpath} AND EXISTS ${_fullpath}/CMakeLists.txt)
        get_property(_included_dirs GLOBAL PROPERTY GlobalAddSubdirectoryOnceIncluded)
        list(FIND _included_dirs "${_fullpath}" _used_index)
        if (${_used_index} EQUAL -1)
            set_property(GLOBAL APPEND PROPERTY GlobalAddSubdirectoryOnceIncluded "${_fullpath}")
            _add_subdirectory(${_dir} ${ARGN})
        endif()
    else()
        message(WARNING "add_subdirectory: Can't find ${_fullpath}/CMakeLists.txt")
    endif()
endfunction(add_subdirectory _dir)

2. 向所有子级 CMakeLists.txt 添加包含保护",例如:

2. Adding an "include guard" to all sub-level CMakeLists.txts, like:

if (NOT TARGET LibA)
    ...
endif()


我一直在测试 tamas.kenez 和 ms 有一些有希望的结果.摘要可以在我的以下答案中找到:


I've been testing the concepts suggested by tamas.kenez and m.s. with some promising results. The summaries can be found in my following answers:

  • 首选cmake项目结构
  • 具有多个可执行文件的 CMake 共享库
  • 使其他 cmake 包自动访问 cmake 库

推荐答案

多次添加同一个子目录是不可能的,这不是 CMake 的工作方式.有两种主要的替代方法可以以干净的方式做到这一点:

Adding the same subdirectory multiple times is out of question, it's not how CMake is intended to work. There are two main alternatives to do it in a clean way:

  1. 在与您的应用程序相同的项目中构建您的库.对于您正在积极处理的库(在您处理应用程序时),更喜欢使用此选项,因此它们可能会经常被编辑和重建.它们也会出现在同一个 IDE 项目中.

  1. Build your libraries in the same project as your app. Prefer this option for libraries you're actively working on (while you're working on the app) so they are likely to be frequently edited and rebuilt. They will also show up in the same IDE project.

在外部项目中构建您的库(我不是指 ExternalProject).对于仅由您的应用程序使用但您不使用它们的库,更喜欢使用此选项.大多数第三方库都是这种情况.它们也不会弄乱您的 IDE 工作区.

Build your libraries in an external project (and I don't mean ExternalProject). Prefer this option for libraries that are just used by your app but you're not working on them. This is the case for most third-party libraries. They will not clutter your IDE workspace, either.

方法#1

  • 您的应用程序的 CMakeLists.txt 添加了库的子目录(而您的库的 CMakeLists.txt 没有)
  • 您的应用程序的 CMakeLists.txt 负责添加所有直接和传递依赖项,并以正确的顺序添加它们
  • 它假定为 libx 添加子目录将创建一些可以与 target_link_libraries 一起使用的目标(比如 libx)
  • Method #1

    • your app's CMakeLists.txt adds the subdirectories of the libraries (and your libs' CMakeLists.txt's don't)
    • your app's CMakeLists.txt is responsible to add all immediate and transitive dependencies and to add them in the proper order
    • it assumes that adding the subdirectory for libx will create some target (say libx) that can be readily used with target_link_libraries
    • 作为旁注:对于库来说,创建一个功能齐全的库目标是一个很好的做法,即包含使用库所需的所有信息的目标:

      As a sidenote: for the libraries it's a good practice to create a full-featured library target, that is, one that contains all the information needed to use the library:

      add_library(LibB Src/b.cc Inc/b.h)
      target_include_directories(LibB PUBLIC
          $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/Inc>)
      

      因此库的包含目录的位置可以保留为库的内部事务.你只需要这样做;

      So the location of include directories of the library can remain an internal affair of the lib. You will only have to do this;

      target_link_libraries(LibC LibB)
      

      那么LibB的include目录也会被加入到LibC的编译中.如果 LibB 没有被 LibC 的公共头文件使用,请使用 PRIVATE 修饰符:

      then the include dirs of LibB will also be added to the compilation of LibC. Use the PRIVATE modifier if LibB is not used by the public headers of LibC:

      target_link_libraries(LibC PRIVATE LibB)
      

      方法#2

      在单独的 CMake 项目中构建和安装您的库.您的库将安装一个所谓的 config-module,它描述头文件和库文件的位置以及编译标志.您的应用程序的 CMakeList.txt 假定库已经构建并安装,并且可以通过 find_package 命令找到配置模块.这是另一个故事,所以我不会在这里详细介绍.

      Method #2

      Build and install your libraries in seperate CMake projects. Your libraries will install a so-called config-module which describes the locations of the headers and library files and also compile flags. Your app's CMakeList.txt assumes the libraries has already been built and installed and the config-modules can be found by the find_package command. This is a whole another story so I won't go into details here.

      一些注意事项:

      • 您可以混合使用 #1 和 #2,因为在大多数情况下,您将同时拥有不变的第三方库和正在开发的自己的库.
      • #1 和 #2 之间的妥协是使用 ExternalProject 模块,受到很多人的青睐.这就像将库的外部项目(在它们自己的构建树中构建)包含到应用程序的项目中.在某种程度上,它结合了两种方法的缺点:您不能将库用作目标(因为它们在不同的项目中)并且不能调用 find_package(因为未安装库您的应用的 CMakeLists 正在配置的时间).
      • #2 的一个变体是在外部项目中构建库,但不是安装工件,而是从它们的源/构建位置使用它们.有关更多信息,请参阅 export() 命令.
      • You can mix #1 and #2 as in most cases you will have both unchanging, third-party libs and your own libraries under development.
      • A compromise between #1 and #2 is using the ExternalProject module, preferred by many. It's like including the external projects of your libraries (built in their own build tree) into your app's project. In way it combines the disadvantages of both approaches: you can't use your libraries as targets (because they're in a different project) and you can't call find_package (because the libs are not installed the time your app's CMakeLists is configuring).
      • A variant of #2 is to build the library in an external project but instead of installing the artifacts use them from their source/build locations. For more about this see the export() command.

相关文章