C++:包括目录,但只允许一个文件夹可见(对于 Eigen)

C++: Include directory, but only allow one folder to be visible (for Eigen)

假设有人想要将一个大型 open-source 项目作为子模块包含在存储库 (myrepo) 中。对于此示例,我们以 Eigen 为例。没问题,我可以

git submodule add https://gitlab.com/libeigen/eigen.git

这会创建一个 eigen 子目录,其中包含很多子文件夹:

myrepo/
    eigen/
        bench
        blas
        ci
        cmake
        debug
        demos
        doc
        Eigen
        failtest
        lapack
        scripts
        test
        ...

然而,为了使用 Eigen 库,真正需要的只是文件夹 myrepo/eigen/Eigen 的内容。所以我们只希望该文件夹对 compiler/linker 可见。但是,为了清楚起见,我更希望包含该文件夹内的文件,例如 myrepo/eigen/Eigen/Dense ,如

#include <Eigen/Dense>

两个明显的次优解是

  1. 添加myrepo/eigen作为包含目录,包含

    等文件
    #include <Eigen/Dense>
    
  2. myrepo/eigen/Eigen添加为包含目录,并包含

    等文件
    #include <Dense>
    

这两种方法都有明显的缺点。具体来说,

  1. 使用 myrepo/eigen 作为包含目录会将所有其他文件暴露给 compiler/linker。由于 repo 的大小以及其中包含的所有其他文件,我觉得这是命名空间冲突或类似问题的定时炸弹。例如,现在编译器看到有一个 test/ 子文件夹,其内容现在是免费游戏。

  2. 包括 headers 而没有 Eigen 序言是我认为代码清晰度的灾难。

    #include <Dense> // ¯\_(ツ)_/¯ Where did this come from??? 
    

我知道的唯一其他选择是分叉或复制原始存储库并删除所有我想排除的东西。

我主要关心的是将 Eigen 添加为子模块。因此,如果有 best-practice 将其作为子模块包含在内的建议,我也很感兴趣。我知道有一个与此主题相关的未解决问题:https://gitlab.com/libeigen/eigen/-/issues/1133

虽然编译器只会看到明确提供给他的文件,但 CMake 会从子模块文件夹中获取 CMakeLists.txt 并执行可能很混乱的构建步骤。

CMake 是一个非常强大的构建系统,因此它可以让您以优雅的方式处理这些问题。 CMake 能够将外部项目作为库包含在内。这与简单的 add_subdirectory() 不同。对于外部项目,CMake 实际上会为该项目创建另一个构建实例,并会使用构建步骤来下载、配置、构建和安装项目,然后构建才能开始在主要目标上工作。

有了外部构建,您可以构建库并将其安装在一个临时文件夹中(相对于您的源,或相对于您的构建文件夹)。这样,您就可以只使用来自外部项目的构建过程的产品。

这里值得一提的是,外部项目可以使用不同的构建系统,而不是必须的 CMake,这使得它更加强大。

您可以使用我为 Eigen 库准备的这个小演示作为外部项目将其添加到您的构建中。

该项目有 eigen 作为由 git submodule add https://gitlab.com/libeigen/eigen.git

初始化的项目根目录中的子模块

CMakeLists.txt

cmake_minimum_required(VERSION 3.17)
project(EigenTest)

include(ExternalProject)

set(CMAKE_CXX_STANDARD 17)
set(EIGEN_INCLUDE eigen_install)

ExternalProject_Add(eigen
        SOURCE_DIR ${CMAKE_SOURCE_DIR}/eigen
        CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR} -DINCLUDE_INSTALL_DIR=${EIGEN_INCLUDE}
        )

ExternalProject_Get_Property(eigen install_dir)

add_executable(EigenTest main.cpp)
add_dependencies(EigenTest eigen)
target_include_directories(EigenTest PRIVATE ${install_dir})
target_include_directories(EigenTest PRIVATE ${CMAKE_BINARY_DIR}/${EIGEN_INCLUDE})

main.cpp

#include <iostream>
#include <Eigen/Dense>

using Eigen::MatrixXd;

int main() {
    MatrixXd m(2,2);
    m(0,0) = 3;
    m(1,0) = 2.5;
    m(0,1) = -1;
    m(1,1) = m(1,0) + m(0,1);
    std::cout << m << std::endl;

    return 0;
}

这会在构建目标中创建一个名为 eigen_install 的文件夹,其中将包含构建库的最终产品。请注意,在我的简单示例中,您应该避免使用 eigen 作为目标,因为在构建库期间,CMake 在构建目标中将创建一个 eigen 文件夹。您可以通过 ExternalProject 的额外配置来控制它,但这超出了这个问题的范围。

CMake documents

中详细了解 ExternalProject 模块的附加功能

使用 git submodule 和 CMake 的 ExternalProject 两个强大的概念,您可以完全控制构建过程,只需选择要构建的外部项目的哪个分支以及使用哪种配置参数。同时,您将保持所需的构建分离(源代码分离)。

您可以创建一个单独的空目录 include 并将软 link Eigen 指向 myrepo/eigen/Eigen。然后将 include 添加为搜索目录,而不是 myrepo/eigen.

Git “按原样”跟踪软件 link,将 link 的路径存储在存储库中,并在克隆时重新创建 link。它不会尝试验证 link 是否有效。 只要:

  • link 是作为同一存储库中 file/directory 的相对路径给出的
  • 存储库克隆在能够容纳软 links
  • 的文件系统上

一切都会按预期进行。

如果您无法完成其他答案,例如您的文件系统不支持链接,您可以将“Eigen”目录复制到一个单独的目录

file(COPY
  ${CMAKE_CURRENT_SOURCE_DIR}/lib/eigen/Eigen
  ${CMAKE_BUILD_FILES_DIRECTOR}/lib/Eigen
)
// And then link to that folder

include_directories(${CMAKE_BUILD_FILES_DIRECTOR}/lib/Eigen)

我没有尝试过这段代码,但理论上应该可以。它应该有点健壮,即使它有点 hacky。

cmake documentation

Suppose one wants to include a large open-source project as a submodule

However, for the purpose of using the Eigen library, all one really needs is the contents of the folder

您要么希望将整个项目包含为子模块,要么不希望。子模块专门用于包含整个源代码树。如果您不想逐字包含整个源代码树,请不要使用子模块。

3rd 方库的常用方法是克隆自己的 repo,构建可安装的工件(public headers 和静态 and/or 动态库),然后安装它们在一些第 3 方图书馆区域。然后你编译并 link 你的代码来对抗它们。第 3 方库是您的构建系统引用的外部依赖项,而不是您自己的存储库的一部分。

我想分享一下我在上一个项目中是如何解决类似问题的——希望对你有用。我的要求是相似的——只公开 headers 和 libs 打算公开的第三方项目的开发人员。我的项目本身使用 cmake,但这不是必需的。

我在我的 repo 的根目录下创建了一个 deps 文件夹,并在里面放了一个 CMakeLists.txt。这不是构建主项目,而是拉下我关心的依赖项,构建它们(他们的开发人员打算构建的方式),并安装它们以供我的项目使用。看起来像这样:

myrepo/
    deps/
        CMakeLists.txt

与@jordanvrtanoski 类似,我使用了 ExternalProject 功能,但我写了一个宏,这让我更简洁一些。我最初与 add_subdirectory 作斗争,最终我找到了他的建议来避免 add_subdirectory 声音 :)。与@jordanvrtanoski 不同,我更喜欢对依赖项使用单独的 CMakeLists.txt,即使我的 top-level 项目是基于 cmake 的。否则,我发现 cmake 会做一些检查,确认依赖项已正确安装,并从我的内部开发循环中抢走时间......我还发现,在内部循环之外安装依赖项可以让你可靠地做 find_package 在 cmake 中,不用担心在 top-level 项目中分别指定 include 和 lib 目录。

这是其中的一个片段 CMakeLists.txt

cmake_minimum_required(VERSION 3.17)

project(deps)

include(ExternalProject)

function(install NAME GIT_REPO GIT_TAG)
    ExternalProject_Add(
            ${NAME}
            GIT_REPOSITORY ${GIT_REPO}
            GIT_TAG ${GIT_TAG}
            PREFIX ${CMAKE_SOURCE_DIR}/${CMAKE_BUILD_TYPE}

            ${ARGN}
            CMAKE_ARGS
            --config ${CMAKE_BUILD_TYPE}
            -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
            -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded$<$<CONFIG:Debug>:Debug>
            -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_SOURCE_DIR}/${CMAKE_BUILD_TYPE}/install
    )
endfunction()

install(gflags
        https://github.com/gflags/gflags.git
        v2.2.0

        CMAKE_ARGS
        -DREGISTER_INSTALL_PREFIX=FALSE
        -DGFLAGS_BUILD_TESTING=FALSE)

install(glog
        https://github.com/google/glog.git
        v0.4.0

        DEPENDS gflags

        CMAKE_ARGS
        -DBUILD_TESTING=FALSE)

您可以通过以下方式触发安装:

cmake -DCMAKE_BUILD_TYPE=Debug --log-level=VERBOSE -G "NMake Makefiles" ..
cmake --build . --config Debug

请注意,您可以分别进行调试和发布构建,并且工件最终位于 deps 下的单独文件夹中。

我希望这里有一些要拆开的东西,但都是有充分理由的。您可以看到,一旦宏不受影响,安装 gflags 的代码片段就非常少了。它为 cmake 指定了 repo、标签和一些 gflags-specific 选项。安装 glog 的以下步骤同样简单,但我将其作为传递依赖项的示例包含在内...您可以看到它声明了对 gflags 的依赖项,这很重要...因为 glog 可以在有或没有 gflags 支持的情况下安装,我想要后者。

另一个需要注意的更普遍的事情是宏覆盖了依赖项的 CMAKE_INSTALL_PREFIX,因此它被安装在 myrepo/deps/Debug/install/... 之类的地方。安装本身会创建 includelib,有时还会在下面创建 bin 文件夹,具体取决于原始开发人员的意图。

当你编译 myrepo 时,你需要将编译器指向安装目录,具体告诉它在 myrepo/deps/Debug/install/include 中查找 headers 并在 myrepo/deps/Debug/install/libs 中查找库。如何执行此操作取决于您用于主项目的构建系统。如果是 cmake(像我的),这对我有用:

...
set(CMAKE_PREFIX_PATH ${CMAKE_SOURCE_DIR}/deps/${CMAKE_BUILD_TYPE}/install)

find_package(gflags REQUIRED NO_MODULE)
find_package(glog REQUIRED NO_MODULE)

add_binary(mybinary
        ...)
target_link_libraries(mybinary      
        glog::glog)

我希望这对你有用,我真的在寻找解决过这个问题的其他人的反馈。我发现 C/C++ 中的整个依赖基础结构比为 Java(例如 Maven)或 JavaScript(例如 NPM)或 Python(例如 PyPI),考虑到它已经存在了多久,这确实有点令人惊讶。

我想 Eigen 在它的安装脚本中会在 .../deps/Debug/include/Eigen 下创建 Eigen 目录,你的梦想就会成真:)

请注意,此方法不使用 git 子模块,但依赖项的源代码最终会被 ExternalProject 模块提取并包含在 .../deps/Debug/src 下...所以你仍然可以去那里检查它们。如果您选择在 .../deps/CMakeLists.txt 中使用不同的标签或 rev 并如上所述重新运行安装部分,则可以轻松更新依赖项的修订版。

一些离别的想法和花絮,有些重要,但不值得在主要论述中占有一席之地...

  1. 如果你需要安装一些特殊的东西,你可以放弃我的宏,它不基于 cmake,不存储在 git,甚至使用预构建的二进制文件......该宏旨在解决对我来说最常见的快乐路径,但在所有其他情况下你可以直接使用 ExternalProject_Add

有一次我不得不在 Windows 上为 OpenSSL 执行此操作,我在我的 deps/CMakeLists.txt:

中使用了这个片段
ExternalProject_Add(
        openssl
        URL https://github.com/CristiFati/Prebuilt-Binaries/raw/master/OpenSSL/v1.1.1/OpenSSL-1.1.1i-Win-pc064.zip
        PREFIX ${CMAKE_SOURCE_DIR}/${CMAKE_BUILD_TYPE}
        CONFIGURE_COMMAND ""
        BUILD_COMMAND ""
        INSTALL_COMMAND ${CMAKE_COMMAND} -E echo installing from `<SOURCE_DIR>/OpenSSL/1.1.1i` to `${CMAKE_SOURCE_DIR}/${CMAKE_BUILD_TYPE}/install`
        COMMAND ${CMAKE_COMMAND} -E copy_directory <SOURCE_DIR>/OpenSSL/1.1.1i/include ${CMAKE_SOURCE_DIR}/${CMAKE_BUILD_TYPE}/install/include
        COMMAND ${CMAKE_COMMAND} -E copy_directory <SOURCE_DIR>/OpenSSL/1.1.1i/lib ${CMAKE_SOURCE_DIR}/${CMAKE_BUILD_TYPE}/install/lib
        COMMAND ${CMAKE_COMMAND} -E copy_directory <SOURCE_DIR>/OpenSSL/1.1.1i/bin ${CMAKE_SOURCE_DIR}/${CMAKE_BUILD_TYPE}/install/bin
)
  1. 我很幸运 googletest 但它更喜欢静态库,他们的 cmake 脚本做了一些漂亮的 low-level 手术来注入正确的编译器选项来做那些当你有一个大的时候会混淆的事情一组依赖项,所以我必须向它传递一个标志以容忍共享库:
install(googletest
        https://github.com/google/googletest.git
        release-1.10.0

        CMAKE_ARGS
        #FIXME: figure out how to make all of them static!!!
        -Dgtest_force_shared_crt=ON)
  1. 我的待办事项列表中的一件事是尝试将此技术应用于容器并使用标准安装目录,而不是像本例中那样被覆盖的目录。我会在这些日子里完成它。

如果您有任何问题,请告诉我!