推荐的 target_include_directories 用法和项目结构以及模块化、相互依赖的静态库
背景
我是 CMake 的新手,虽然我已经阅读了大量文档,并且感觉我在真空中理解了许多概念(至少在基本层面上),但当它出现时我仍然感到非常困惑将其中的几个与更复杂的项目结合使用,其中之一是 target_include_directories()。
当前设置
我目前有一个已重组的库,并将其移植到 CMake,这是一个具有多个组件库/模块的顶级项目,使用 add_subdirectory() 进行处理。希望每个子库都可以用作 CMake 包组件,值得注意的是,某些子库依赖于其他子库。
现在该项目的结构如下:
MyLib/
├── CMakeLists.txt
└── src/
└── mylib/
├── sublibA/
│ ├── CMakeLists.txt
│ ├── classA1.h
│ ├── classA1.cpp
│ ├── classA1_p.h
│ ├── classA1_p.cpp
│ ├── classA2.h
│ └── ...
├── sublibB/
│ ├── (Similar to Above)
│ └── ...
├── sublibC/
│ └── ...
└── ...
在我的顶级 CMakeLists.txt 中,我
include_directories("${CMAKE_SOURCE_DIR}/src")
这样做 因此,当一个子库需要包含来自另一个子库的标头时,我可以简单地执行此操作
// Arbitrary SubLibB source file
#include "mylib/sublibA/classA1.h"
,然后使用尴尬的 PUBLIC_HEADER
属性来提供每个模块的公共 API 的一部分的标头列表(因此没有 *_p.h 文件),以及用于处理的 install(TARGETS ... PUBLIC_HEADER DESTINATION ...)
.lib 和公共标头结构安装。接下来是一些 INSTALL(CODE ...)
,它利用 configure_file()
在同一安装结构中生成方便的组件级别包含(即 sublibA.h、sublibB .h 等),以便用户可以简单地
#include "mylib/sublibA.h"
包含该组件的所有标头。
这对于构建/安装效果很好,并生成一组可以按原样使用的文件;然而,当涉及到打包时,这已经成为一个噩梦,因为我想为这个库创建一个很好的包,可以轻松地与其他 CMake 项目一起使用。
目标
- 让用户能够通过诸如
find_package(MyLib COMPONENTS SubLibA ...)
之类的方式 - 访问库 拥有统一的公共标头结构,以便用户通过(并且只能通过)执行 '#include "mylib/sublibX/classY.h"' 无论它是哪个子库,受限于已链接到的子库(即如果用户未执行“target_link_libraries(user_target PUBLIC/PRIVATE MyLib::SubLibB)”,则找不到“mylib/sublibB/classZ.h”
- 与上面相同,将其保留在我的构建树中,这样我的子库之一-如果未将其指定为依赖项/库链接,则库无法找到另一个子库标头,以便我知道我的依赖项是直接的(现在是顶层include_directories() 只是绕过了这一要求,稍后我可以更改我的源代码,以便一个子库包含来自新子库的标头,忘记更新我的 CMake 脚本,但构建仍然会完成,就好像一切正常一样
- 使用传播的依赖项来避免不必要的用户指定的链接/包含目录。例如,如果 SubLibB 依赖于 SubLibA,但该库的用户仅在 find_package() 中请求 SubLibB 组件,并且也仅链接到它,则自动处理 SubLibA 的链接和标头包含,以便在其末尾不会引发
- 错误能够包含类的直接头文件,就好像它位于同一目录中一样(即 classA1.cpp 中的“#include“classA1.h””)
- 显然没有将私有头文件放入 lib 的 install/package
中 可以
经过大量研究后我了解到,我真的想使用 target_include_directories() 而不是顶级 include_directories(),主要是为了填充每个组件的 INTERFACE_INCLUDE_DIRECTORIES 变量,并且依赖项 起初,我确定如何执行此操作,因为只能指定一个目录,在我的情况下,这会错误地包含私有头文件,但在查看了更简单的示例之后,我认为我已经确定了一种可能有效的通用方法:
新结构:
MyLib/
├── CMakeLists.txt
└── src/
└── mylib/
├── SubLibA/
│ ├── CMakeLists.txt
│ ├── include/
│ │ └── mylib/
│ │ └── sublibA/
│ │ ├── classA1.h
│ │ ├── classA2.h
│ │ └── ...
│ └── src/
│ ├── classA1.cpp
│ ├── classA1_p.h
│ ├── classA1_p.cpp
│ ├── classA2.cpp
│ └── ...
├── SubLibB/
│ ├── (Similar to Above)
│ └── ...
├── SubLibC/
│ └── ...
└── ...
然后对于给定的模块/组件,可以说 SubLibA (不依赖于其他子库)我这样做:
# Allow for including public headers in a given classes .cpp file as if they were in the same directory
target_include_directories(SubLibA PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/mylib/sublibA)
# Propagate include directory to dependents properly
target_include_directories(SubLibA INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
最后对于使用其他子库中的标头的组件(即 SubLibB )做:
target_link_libraries(SubLibB PUBLIC SubLibA)
如果我理解正确,这应该使得:
- 每个模块“通告”其接口标头,以便在链接到
- CMake 时自动包含它们知道 SubLibB 需要链接到 SubLibA,并且需要其公共/接口 头
- 由于我的新结构和预处理器搜索包含目录的方式,用户使用其 CMake 脚本链接到的任何/所有组件都将通过“#include“mylib/SubLibX/[filename.h]”在用户代码中访问其标 ',就好像“mylib”变成了一个大的虚拟目录。
我认为 target_include_directories() 的这种用法也正确设置了要在其目标导出时打包的 SubLib 组件的公共标头,尽管我认为获取包配置文件来处理它们可能需要多一点(另外我如何将生成的标头合并到包中?)。我对这一点特别执着。
因此,除了包装设置中需要采取的步骤之外,我相信所有这些都会实现我的目标。
备注
这些更改是否符合 target_include_directories() 的预期用途以及正确的方向?
为了实现这些目标我还应该做什么/以不同的方式做?
我的 scipts 中确实有基本的 EXPORT 和 cmake 配置生成,但由于这个问题,我暂时停止了对它们的处理。对于如何设置这些的非常基本的想法,这就是顶级 MyLib-config.cmake 的样子:
include(CMakeFindDependencyMacro)
find_dependency(Qt6 6.2)
file(GLOB AVAILABLE_COMPONENT_CONFIGS
RELATIVE "${CMAKE_CURRENT_LIST_DIR}"
"${CMAKE_CURRENT_LIST_DIR}/MyLib-*-config.cmake"
)
foreach(component ${MyLib_FIND_COMPONENTS})
set(component_config MyLib-${component}-config.cmake)
if (";${AVAILABLE_COMPONENT_CONFIGS};" MATCHES ";${component_config};")
include("${CMAKE_CURRENT_LIST_DIR}/${component_config}")
elseif(MyLib_FIND_REQUIRED_${component}})
set(MyLib_FOUND False)
set(MyLib_NOT_FOUND_MESSAGE "Unsupported component: ${component}")
endif()
endforeach()
如果有必要,我可以托管我的源结构的存储库,仅包含 CMake 相关部分以供参考。
编辑: 目前它非常丑陋,仅供个人使用,但无论如何(只是没有必要批评项目本身:))。这是所提到的当前设置的相关库: https://github.com/obliviocth/Qx/tree/2183a1f7c64be090ebf1cf804a8ecf811d847658
Background
I'm new to CMake and while I've read through a lot of the documentation and feel like I understand many of the concepts (at least at a basic level) in a vacuum, I'm still left pretty confused when it comes to using several of them in tandem with a more complex project, one of which being target_include_directories().
Current Setup
I currently have a library that I restructured and am porting to CMake that is a top level project with several component libraries/modules, handled using add_subdirectory(). The hope is that each sub-library will be usable as a CMake package component, and it is worth noting that some of the sub-libraries depend on others.
Right now the project is structured like so:
MyLib/
├── CMakeLists.txt
└── src/
└── mylib/
├── sublibA/
│ ├── CMakeLists.txt
│ ├── classA1.h
│ ├── classA1.cpp
│ ├── classA1_p.h
│ ├── classA1_p.cpp
│ ├── classA2.h
│ └── ...
├── sublibB/
│ ├── (Similar to Above)
│ └── ...
├── sublibC/
│ └── ...
└── ...
In my top level CMakeLists.txt I do
include_directories("${CMAKE_SOURCE_DIR}/src")
So that when one sub lib needs to include headers from another it can simply do
// Arbitrary SubLibB source file
#include "mylib/sublibA/classA1.h"
I then use the awkward PUBLIC_HEADER
property to provide a list of just the headers that are part of the public API (so no *_p.h files) for each module, along with install(TARGETS ... PUBLIC_HEADER DESTINATION ...)
to handle the .lib and public header structure installation. This is followed by some INSTALL(CODE ...)
, which utilizes configure_file()
to generate convenience component level includes in that same install structure (i.e. sublibA.h, sublibB.h, etc.) so that users can simply do
#include "mylib/sublibA.h"
to include all headers from that component.
This works fine for building/installing and results in a set of files that could be used as is; however, this has become a bit of a nightmare when it comes to packaging, as I would like to create a nice package of this lib that can easily be used with other CMake projects.
Goals
- Have user's be able to access the library via something like
find_package(MyLib COMPONENTS SubLibA ...)
- Have a unified public header structure so that a user accesses a class/component in code by (and only by) doing '#include "mylib/sublibX/classY.h"' regardless of which sublib it is, limited by which sublibs have been linked to (i.e. so "mylib/sublibB/classZ.h" is not found if the user hasn't done 'target_link_libraries(user_target PUBLIC/PRIVATE MyLib::SubLibB)'
- Same as in the above, have it so that in my build tree one of my sub-libraries can't find another sub-libraries headers if it isn't specified as a dependency/library link so that I know I have my dependencies straight (right now the top-level include_directories() just bypasses this requirement and later I could change my source so that one sublib includes the header from a new sublib, forget to update my CMake scripts and yet the build would still complete as if everything was OK
- Use propagated dependencies to avoid unnecessary linking/include directory specification by the user. For example if SubLibB depends on SubLibA, but a user of this library only requests the SubLibB component in find_package() and only links to it as well, have the linking and header inclusion of SubLibA handled automatically so no error is thrown on their end
- Be able to include a class's direct header file as if it was in the same directory (i.e. '#include "classA1.h"' inside classA1.cpp
- Obviously have no private headers placed into the lib's install/package
Proposed Changes
From what I understand after doing a lot of research is that I really want to be using target_include_directories() instead of the top level include_directories(), primarily so that each component's INTERFACE_INCLUDE_DIRECTORIES
variable is populated and dependencies can be propagated. At first I was insure how to do this since only a directory can be specified and in my case this would errantly include private header files, but after looking over simpler examples, I think I've determined a general approach that might work:
New Structure:
MyLib/
├── CMakeLists.txt
└── src/
└── mylib/
├── SubLibA/
│ ├── CMakeLists.txt
│ ├── include/
│ │ └── mylib/
│ │ └── sublibA/
│ │ ├── classA1.h
│ │ ├── classA2.h
│ │ └── ...
│ └── src/
│ ├── classA1.cpp
│ ├── classA1_p.h
│ ├── classA1_p.cpp
│ ├── classA2.cpp
│ └── ...
├── SubLibB/
│ ├── (Similar to Above)
│ └── ...
├── SubLibC/
│ └── ...
└── ...
Then for a given module/component, lets say SubLibA (which doesn't depend on other sublibs) I do:
# Allow for including public headers in a given classes .cpp file as if they were in the same directory
target_include_directories(SubLibA PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/mylib/sublibA)
# Propagate include directory to dependents properly
target_include_directories(SubLibA INTERFACE
lt;BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
lt;INSTALL_INTERFACE:include>
)
Finally for components that use headers from other sublibs (i.e. SubLibB) do:
target_link_libraries(SubLibB PUBLIC SubLibA)
If I understand correctly, this should make it so that:
- Each module "advertises" its interface headers so that they're automatically included when linked to
- CMake knows that SubLibB needs to link to SubLibA, and needs its public/interface headers
- Because of my new structure and the way that preprocessors search include directories, any/all components a user links to with their CMake scripts will have their headers accessible in user code via '#include "mylib/SubLibX/[filename.h]"', as if "mylib" became one big virtual directory.
I think this usage of target_include_directories() also correctly sets up the public headers of a SubLib component to be packaged when its target is EXPORTed, though I think a little bit more than that may be required to get the package config files to handle them (also how would I incorporate my generated headers into the package?). I'm a bit stuck on this specifically.
So other than the steps that need to be taken in packaging setup, I believe all of this will achieve my goals.
Remarks
Are these change in-line with the intended use of target_include_directories() and the right direction to go in?
What else/differently should I do in order to achieve these goals?
I do have basic EXPORTs and cmake config generation present in my scipts, but I stopped working on them momentarily because of this issue. For very basic idea of how I set those up, this is what the top-level MyLib-config.cmake looks like:
include(CMakeFindDependencyMacro)
find_dependency(Qt6 6.2)
file(GLOB AVAILABLE_COMPONENT_CONFIGS
RELATIVE "${CMAKE_CURRENT_LIST_DIR}"
"${CMAKE_CURRENT_LIST_DIR}/MyLib-*-config.cmake"
)
foreach(component ${MyLib_FIND_COMPONENTS})
set(component_config MyLib-${component}-config.cmake)
if (";${AVAILABLE_COMPONENT_CONFIGS};" MATCHES ";${component_config};")
include("${CMAKE_CURRENT_LIST_DIR}/${component_config}")
elseif(MyLib_FIND_REQUIRED_${component}})
set(MyLib_FOUND False)
set(MyLib_NOT_FOUND_MESSAGE "Unsupported component: ${component}")
endif()
endforeach()
If necessary, I can host a repository of my source structure with just the CMake related portions for reference.
EDIT:
It's pretty ugly at the moment and only intended for personal use, but whatever (just no need to criticize the project itself :) ). Here is the library in question with the current setup mentioned: https://github.com/oblivioncth/Qx/tree/2183a1f7c64be090ebf1cf804a8ecf811d847658
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论