从 CMake 语法到 libstdc++ ABI 不兼容:迁移总是艰难的
这是关于 Conan 0.7.X 版本发布的简短故事,其中我们不得不发布多个小版本来修复与 CMake 和 libstdc++ ABI 不兼容相关的一些意外问题,尽管从中获得的经验教训可能对任何 C 和 C++ 开发人员都很有用,所以让我们尝试在这篇文章中总结一下。
从一开始,Conan 就试图尽可能地兼容非前沿系统,因为我们知道在许多企业环境中存在一些限制,并且倾向于使用旧的发行版和工具。因此,Conan 为其 CMake 生成器(它支持 Visual 或 XCode 等其他生成器)使用了典型的 CMake 2.8 语法来进行条件判断。
macro(CONAN_CHECK_COMPILER)
if( ("${CONAN_COMPILER}" STREQUAL "Visual Studio" AND NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") OR
在旧版本的 CMake 中,未定义的变量会被评估为空字符串,因此上述代码可以正常工作。但是,一旦我们的用户开始使用更新版本的 CMake,CMake>3.1 的 CMake#CMP0054 策略就规定字符串内的变量不会被解引用,这会影响到上述大多数字符串,甚至包括作为 CMake 变量的 MSVC 字符串。
问题在于,如果使用新的方法,当这样的变量未定义时,由于参数数量不正确,CMake 命令会失败,因此必须提前检查变量是否存在。
macro(CONAN_CHECK_COMPILER)
if(NOT DEFINED CONAN_COMPILER)
message(STATUS "WARN: CONAN_COMPILER variable not set, please make sure yourself that "
"your compiler and version matches your declared settings")
return()
endif()
if( (CONAN_COMPILER STREQUAL "Visual Studio" AND NOT CMAKE_CXX_COMPILER_ID MATCHES MSVC) OR
在这里,我们犯了另一个错误,你能在上面的代码中找到它吗?是的,来自 CMake 宏的 return()
的行为与函数内部的 return()
不同,它从调用者返回,考虑到宏不是被调用而是内联扩展。解决方案显然是使用函数而不是宏。
此外,C 项目(可以在 CMake 项目声明中定义为 project(MyProject C)
)是另一个我们最初没有考虑到的特殊情况,这导致上述代码失效,因为 CMAKE_CXX_COMPILER_ID 也未定义,而应该检查 CMAKE_C_COMPILER_ID
。此外,还需要为 CMAKE_CXX_FLAGS_RELEASE
和其他类似变量引入一些检查。
但为什么要进行所有这些编译器检查呢?
一些读者可能想知道为什么我们需要所有这些编译器检查,因为这通常是用户管理的事情,例如在 CMake 中指定生成器时。
cmake -G “Visual Studio 14 Win64”
或者通过定义 CC、CXX 环境变量,或者只是在他们的系统中安装特定版本的 gcc 编译器。
问题在于不同编译器版本之间的 ABI 不兼容,这意味着用 Visual Studio 14 编译的库可能无法与 Visual Studio 12 链接,或者用 gcc 4.8 编译的库可能无法与 gcc 5.2 链接。因此,在 Conan 中,用户指定他们的设置(操作系统、编译器、编译器版本、架构、构建类型等),以便检索其依赖项的兼容预构建二进制文件(如果可能)。同样,包创建者也以类似的方式指定他们的设置并构建和上传他们的包。
但是,如果用户指定了某个编译器版本的用法,但实际安装和使用的编译器版本不同会发生什么?对于包使用者来说,这通常会导致链接错误,但对于包创建者来说,这意味着他们可能会影响许多人,导致构建不兼容。
因此,在构建系统中引入一个检查,将用于安装依赖项的设置与实际的构建设置进行比较,似乎是明智之举。
并且检查证明是有用的
这些类型的检查通常是无用的,就像代码中的断言一样,直到它们被触发。我们意识到,在某些上下文中,特别是当用户使用自己的工具进行构建(而不是使用 Conan 构建命令)时,Conan 使用的编译器未定义,并且没有任何检查。
这很快在像 Travis CI 这样的 CI 环境中变得臭名昭著,它使用相当旧的 Ubuntu 12.04 机器,默认编译器为 gcc 4.6。通常情况下,Travis 用户会在设置过程中安装更新的编译器,但根据情况,Conan 可能只会自动检测系统 gcc 4.6,而不是最近安装的编译器。
因此,我们发现一些项目**使用 gcc 5.2 构建可执行文件,链接到用 gcc 4.6 编译的 Boost 库。并且它可以正常工作和运行!**但我们知道这可能不是用户的本意,实际上用 gcc 5.2 构建的 Boost 包更有可能被首选。因此,编译器检查可以轻松地发现这种编译器版本不兼容,并修复设置以正确指向正确的版本。
但是然后……惊喜!这是链接到用 gcc 5.2 构建的 Boost 时的一个典型输出。
/home/travis/.conan/data/Boost/1.60.0/lasote/stable/package/ebdc9c0c0164b54c29125127c75297f6607946c5/lib/libboost_log.so: undefined reference to `std::invalid_argument::invalid_argument(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)@GLIBCXX_3.4.21'
/home/travis/.conan/data/Boost/1.60.0/lasote/stable/package/ebdc9c0c0164b54c29125127c75297f6607946c5/lib/libboost_log.so: undefined reference to `std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::find(char const*, unsigned long, unsigned long) const@GLIBCXX_3.4.21'
/home/travis/.conan/data/Boost/1.60.0/lasote/stable/package/ebdc9c0c0164b54c29125127c75297f6607946c5/lib/libboost_log.so: undefined reference to `std::runtime_error::runtime_error(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)@GLIBCXX_3.4.21'
/home/travis/.conan/data/Boost/1.60.0/lasote/stable/package/ebdc9c0c0164b54c29125127c75297f6607946c5/lib/libboost_filesystem.so: undefined reference to `std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::rfind(char, unsigned long) const@GLIBCXX_3.4.21'
什么?!我们能够使用 gcc 5.2 链接和运行针对用 gcc 4.6 构建的 Boost,但我们却在链接针对用 gcc 5.2 构建的 Boost 时遇到错误?
Libstdc++ ABI 不兼容
是的,我们已经知道了,从 gcc 5.1 开始,有两个不同的 libstdc++ 实现,据称默认实现是最新的实现,即 C++11 实现(我们将其称为 libstdc++11
)。这是真的吗?**答案:并非总是如此,它取决于你的机器和操作系统。**
事实证明,为了在 Travis CI 中构建 Boost,我们使用了 conan-package-tools,它使用 Docker 来轻松管理不同的编译器版本。这些 Docker 镜像基于现代 Ubuntu 发行版,如 Xenial Xerus (15.04),默认使用 gcc 5.2 编译器和 libstdc++11。因此,这些包使用了现代的 libstdc++ ABI。
但是大多数 Travis CI 用户不会使用 Docker 来构建他们的项目。他们只会将他们的编译器升级到 gcc 5.2。但是 Travis CI 机器是旧的 Ubuntu 12.04 发行版,因此将它们升级到 gcc 5.2 并不会默认升级 libstdc++。事实上,这样的升级非常困难,因为许多程序依赖于 libstdc++,因此系统 libstdc++ 无法在没有主要系统升级的情况下进行升级。理论上,可以下载一个单独的现代 libstdc++ 副本并链接到它,但这似乎有点复杂,并且不是 Travis CI 用户正在做的事情。
Libstc++11 及其新的 ABI 将成为现代发行版中 gcc>5.1 的默认设置,但对于旧的发行版来说并非如此,即使升级了 gcc 编译器。
我们的解决方案
那么可以做什么呢?首先,我们为 gcc 和 clang 编译器系列添加了一个新的设置,它可以取多个值。
os: [Windows, Linux, Macos, Android, iOS]
arch: [x86, x86_64, armv6, armv7, armv7hf, armv8]
compiler:
gcc:
version: ["4.4", "4.5", "4.6", "4.7", "4.8", "4.9", "5.1", "5.2", "5.3"]
libcxx: [libstdc++, libstdc++11]
Visual Studio:
runtime: [MD, MT, MTd, MDd]
version: ["8", "9", "10", "11", "12", "14"]
clang:
version: ["3.3", "3.4", "3.5", "3.6", "3.7"]
libcxx: [libstdc++, libstdc++11, libc++]
apple-clang:
version: ["5.0", "5.1", "6.0", "6.1", "7.0"]
libcxx: [libstdc++, libc++]
build_type: [None, Debug, Release]
然后,该设置与其他设置一样进行管理,因此用户可以编写
conan install -s compiler=gcc -s compiler.version=5.3 -s compiler.libcxx=libstdc++11
它将针对与新的 ABI libstdc++ 链接的二进制包。
在 CMake 的情况下,它被转换为一个变量(只是一个摘录),该变量被检查以定义编译器标志 _GLIBCXX_USE_CXX11_ABI
if(CONAN_LIBCXX STREQUAL "libstdc++11")
add_definitions(-D_GLIBCXX_USE_CXX11_ABI=1)
elseif(CONAN_LIBCXX STREQUAL "libstdc++")
add_definitions(-D_GLIBCXX_USE_CXX11_ABI=0)
endif()
可以看出,Visual Studio 没有此设置,而是使用运行时,从本质上解决了相同的问题。有人可能会争辩说,C 项目不会链接到 libcxx,这是真的,因此我们决定,对于 C 项目来说,最符合概念的方法是删除该设置,因此它们不依赖于它,这可以通过以下方式完成
def config(self):
del self.settings.compiler.libcxx
迁移是困难的。将 CMake 支持从 CMake 2.8 扩展到更新的 3.4,以及处理现代 gcc>5.1 的不兼容性,意味着在最新的 0.8 Conan 版本中做了大量工作,但我们相信 Conan 将继续改进,主要原因是:上述所有问题都已被活跃用户发现并报告,他们使用 Conan 构建项目,并且我们已经收到了大量帮助、反馈和贡献来解决这些问题。
特别感谢 @mcraveiro、@tyroxx、@Manu343726、@nathanaeljones,当然还有所有 Conan 贡献者!(提示,克隆仓库并执行“git shortlog -sne”;) )