在 Conan 1.4 中,我们引入了两个新的 Conan 生成器,允许您在不更改 CMakeList.txt 文件中任何与 Conan 相关的行的情况下链接您的依赖项。

首先,让我们回顾一下 Conan 生成器是什么,并展示经典生成器和新生成器的优缺点。

以下示例中的所有代码都位于此 GitHub 存储库中:https://github.com/lasote/transparent_cmake_examples.git

经典 CMake 集成:“cmake” 生成器

如果您在项目中使用 Conan 包,您通常会声明一个 conanfile.txt 文件,其中包含一些依赖项。在本例中,我们正在构建一个应用程序,该应用程序使用 libcurl 检查 Conan 存储库中的 Github 星标。

(存储库中的 classic_approach 文件夹)

conanfile.txt


[requires]
libcurl/7.52.1@bincrafters/stable

[generators]
cmake

[options]
libcurl:with_openssl=True


CMakeLists.txt

project(myapp)
cmake_minimum_required(VERSION 3.1)

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup(TARGETS)

add_executable(myapp main.cpp)
target_link_libraries(myapp CONAN_PKG::libcurl)

main.cpp


#include <stdio.h>
#include <curl/curl.h>

int main(void)
{
  CURL *curl;
  CURLcode res;

  curl_global_init(CURL_GLOBAL_DEFAULT);

  curl = curl_easy_init();
  if(curl) {
    struct curl_slist *list = NULL;
    list = curl_slist_append(list, "user-agent: libcurl");
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
    curl_easy_setopt(curl, CURLOPT_URL, "https://api.github.com/repos/conan-io/conan/stargazers");
    res = curl_easy_perform(curl);
    if(res != CURLE_OK)
        fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
    curl_easy_cleanup(curl);
  }

  curl_global_cleanup();
  return 0;
}

创建一个“build”文件夹并调用“conan install”


$ mkdir build && cd build
$ conan install ..

Conan 生成了一个 conanbuildinfo.cmake 文件(对应于“cmake”生成器),其中包含有关 libcurl 依赖项和所有传递依赖项(在本例中为 OpenSSL 和 ZLib)的所有信息,以及一些我们可以调用的宏来简化与依赖项链接的任务。

在我们的 CMakeLists.txt(如上所示)中,我们包含了该文件并调用了 conan_basic_setup()

此宏将为我们执行以下操作

  • 检查 conan install 中指定的编译器是否与 CMake 检测到的编译器匹配。
  • 根据 conan install 命令中指定的设置(本例中的默认配置文件)调整输出目录、rpaths 配置、标准库、运行时(仅限 Visual Studio)、fPIC 标志等。
  • 准备传递目标(现代 CMake)和所需变量(全局方法)以链接依赖项。

可以 单独调用 这些自动调整中的任何一个,而不是调用 conan_basic_setup(),这样我们就可以精确控制我们希望 Conan 为我们执行的操作。

现在我们可以只调用 CMake 来构建应用程序

Linux/Mac

$ cmake .. -DCMAKE_BUILD_TYPE=Release
$ cmake --build .
$ ./bin/myapp
# a JSON will be outputted here

Windows

$ cmake .. -G “Visual Studio 15 2017 Win64”
$ cmake --build . --config Release
$ ./bin/myapp
# a JSON will be outputted here

这种方法非常有用且使用非常简单,但您需要更改 CMakelists.txt 文件以包含 conanbuildinfo.cmake 文件。一些用户更喜欢依赖 CMake find_package() 功能来解耦构建系统和包管理器。

透明 CMake 集成:“cmake_paths” 生成器

(存储库中的 cmake_paths 文件夹)

我们可以调整我们的项目以使用与 CMake 的透明集成

conanfile.txt 将生成器更改为 cmake_paths

[requires]
libcurl/7.52.1@bincrafters/stable

[generators]
cmake_paths

[options]
libcurl:with_openssl=True

CMakeLists.txt 在这里我们不包含 conanbuildinfo.cmake 文件,只调用 find_package

project(myapp)
cmake_minimum_required(VERSION 3.1)

add_executable(myapp main.cpp)
find_package(CURL)
include_directories(${CURL_INCLUDE_DIRS})
target_link_libraries(myapp ${CURL_LIBRARIES})

清理“build”目录并重新安装,它将生成 conan_paths.cmake 文件

$ rm -rf build && mkdir build && cd build
$ conan install ..

现在调用 CMake,将 conan_paths.cmake 作为工具链包含在内

Linux/Mac:

$ cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=./conan_paths.cmake
$ cmake --build .
$ ./bin/myapp
# a JSON will be outputted here

Windows:

$ cmake .. -G “Visual Studio 15 2017 Win64” -DCMAKE_TOOLCHAIN_FILE=./conan_paths.cmake
$ cmake --build . --config Release
$ ./release/myapp
# a JSON will be outputted here

不幸的是,它不起作用,将发生许多链接器错误

  ...
  "_sk_value", referenced from:
      _ossl_connect_common in libcurl.a(libcurl_la-openssl.o)
  "_zlibVersion", referenced from:
      _Curl_version_init in libcurl.a(libcurl_la-version.o)
      _curl_version in libcurl.a(libcurl_la-version.o)
      _curl_version_info in libcurl.a(libcurl_la-version.o)
      _Curl_unencode_gzip_write in libcurl.a(libcurl_la-content_encoding.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

为什么?因为即使使用 find_package(CURL),我们也需要管理传递依赖项 OpenSSL 和 Zlib

(存储库中的 cmake_paths_attempt2 文件夹)

PROJECT(myapp)
cmake_minimum_required(VERSION 3.1)

ADD_EXECUTABLE(myapp main.cpp)

find_package(ZLIB)
find_package(OpenSSL)
find_package(CURL)

include_directories(${ZLIB_INCLUDE_DIRS} ${OPENSSL_INCLUDE_DIRS} ${CURL_INCLUDE_DIRS})
target_link_libraries(myapp ${CURL_LIBRARIES} ${OPENSSL_LIBRARIES} ${ZLIB_LIBRARIES})

同样重要的是,我们需要以正确的顺序进行 target_link_libraries。因此,在这种情况下,透明集成远非理想,我们丢失了来自包管理器的宝贵信息,例如传递性和链接顺序。

如果您未使用 Windows,则前面的示例可能会起作用。否则,您仍然会看到由于 findCURL.cmake(由 CMake 提供)导致的链接器错误。

  • 它没有链接到 Ws2_32:请记住,CMake findXXX 模块不是传递的,因此您必须在 CMakeLists.txt 文件中声明所有依赖项树。

  • 它没有传播正确链接静态库所需的定义 CURL_STATICLIB

查看存储库中 cmake_paths_attempt3_windows 文件夹中的代码。

我们可以看到,使用 CMake 提供的 findXXX 模块远非理想,因为包管理器已经知道的大量信息完全丢失了:传递依赖项和定义都在 libcurl 配方的 package_info 方法中声明,但是如果您使用 CMake 提供的 findXXX 模块,则永远不会应用它们。

如何改进这一点?

透明 CMake 集成 (2):“cmake_find_package” 生成器

cmake_find_package 是一种不同的方法。它将为 Conan 关于依赖项树的信息中的每个依赖项生成一个 find<package_name>.cmake。我们可以将其与现代 CMake 目标方法一起使用。由于每个目标都是传递的,因此 libcurl 目标也将包含 OpenSSL 和 zlib 信息。

(存储库中的 cmake_find_package 文件夹)

conanfile.txt 将生成器更改为 cmake_find_package

[requires]
libcurl/7.52.1@bincrafters/stable

[generators]
cmake_find_package

[options]
libcurl:with_openssl=True
libcurl:darwin_ssl=False # Force use openssl in OSX too

CMakeLists.txt Conan 将生成 libcurl::libcurl 目标,我们只需要修改 CMAKE_MODULE_PATH 以让 CMake 找到我们的自定义脚本

project(myapp)
cmake_minimum_required(VERSION 3.1)
set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR})
message(${CMAKE_BINARY_DIR})

add_executable(myapp main.cpp)
find_package(libcurl)
target_link_libraries(myapp libcurl::libcurl)

清理build目录并重新安装后,我们将得到以下文件

  • FindOpenSSL.cmake
  • Findzlib.cmake
  • Findlibcurl.cmake
$ rm -rf build && mkdir build && cd build
$ conan install ..

现在让我们再次调用 CMake

Linux/Mac:

$ cmake .. -DCMAKE_BUILD_TYPE=Release
$ cmake --build .
$ ./bin/myapp
# a JSON will be outputted here

Windows:

$ cmake .. -G “Visual Studio 15 2017 Win64”
$ cmake --build . --config Release
$ ./release/myapp
# a JSON will be outputted here

如果我们想避免 CMAKE_MODULE_PATH 操作,我们还可以使用 cmake_pathscmake_find_package 生成器,并将其用作工具链,就像在前面的示例中一样,它也将调整模块路径以在当前目录中找到我们的 find<package_name>.cmake 脚本。

CMake 将尝试按以下顺序找到 find find<package_name>.cmake 脚本:首先是 packages 文件夹,然后是我们在其中构建项目的目录,最后是 CMake 安装/Modules 目录。

等等……这真的透明吗?

您可能已经注意到,find_package(CURL) 已被 find_package(libcurl) 替换。也许您正在想:好吧,但是 findXXX 文件的名称与我在 CMakelists.txt 中的经典 find_package 调用不对应。我不想更改 CMakeLists.txt 文件,而这正是我阅读这篇博文的唯一原因!

您是对的,但此生成器并非旨在替换原始 CMake 安装 find_package 模块,因为它们的行为方式大不相同。如您所见,使用经典的 find_package 模块,从包管理器传输到构建系统的信息很少,而使用此生成器,目标的所有信息都会自动传播

  • 目标是传递的,因此您只需指定您直接依赖的依赖项。您不需要知道 libcurl 是否依赖于 OpenSSL。实际上,Mac OSX 中的先前示例默认使用内部 Apple SSL 实现。我们的 CMakeLists.txt 将在任何系统中都以完全相同的方式工作。

  • 传播定义:如果没有 CURL_STATICLIB 定义,构建将失败。此定义在 libcurl 包的 package_info 方法中声明。

  • 传播链接器和编译器标志:例如,要链接 Mac OSX 中的 SSL 框架,配方会注入:-framework Security-framework Cocoa,但仅在 OSX 中,并且仅当您不强制它使用 OpenSSL 时。

  • 如果您正在使用包并且对更改 CMakeLists.txt 具有很高的限制,那么 cmake_paths 可能是最佳选择。

  • 如果您正在使用包并且正在寻找一种以非侵入方式连接包管理器和 CMake 构建系统的方法,请选择 cmake_find_package

  • 如果您正在创建 Conan 包,我们强烈建议您在 CMakeLists.txt 文件中包含 conanbuildinfo.cmake(您可以始终从配方中修补您的 CMakeLists.txt!)。经典的 cmake 生成器将来自包管理器的更多信息引入我们的构建脚本:应用的设置和选项、Visual Studio 运行时、rpaths、编译器检查、标准库版本和 fPIC 标志。

您可以在此存储库中找到这篇博文中示例的源代码:https://github.com/lasote/transparent_cmake_examples

您可以在 Conan 文档中找到有关与 CMake 集成的更多信息:https://docs.conan.org.cn/en/latest/integrations/cmake.html#cmake