前言

偷懒了好久没有写分享了,最近的时间也是花费了很多时间大量优化了之前游戏服务器框架和组件的很多细节。其中,相对独立且同时也被其他的项目使用的一个工具则是基于 cmakegit 且兼容 vcpkg 的构建系统 cmake-toolset 。之所以要写这么个构建工具主要是要提供比 vcpkg 更宽容的兼容性(没办法我们公司的编译环境比较古老),并且提供更进一步的定制化能力(包含但不限于功能开关和下载源,这些东西 vcpkg 也是很后期才有了个初步的支持)。那么先来记录一下构建系统适配过程中的一些问题吧。

增加 boringssl 支持,升级 openssl 到 3.0.0

之前 gRPC 的依赖使用的是 openssl 。其实官方默认的SSL库用的是 boringsslboringssl 裁剪掉了很多老的算法,阉割掉了很多低级接口。并且跨平台和跨编译器适配其实没有 openssl 做得好。不过所幸它也支持 cmake ,所以打个类似这样的patch就行了。

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 893bca753..56c90c637 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -117,7 +117,7 @@ endif()
 if(CMAKE_COMPILER_IS_GNUCXX OR CLANG)
   # Note clang-cl is odd and sets both CLANG and MSVC. We base our configuration
   # primarily on our normal Clang one.
-  set(C_CXX_FLAGS "-Werror -Wformat=2 -Wsign-compare -Wmissing-field-initializers -Wwrite-strings -Wvla")
+  set(C_CXX_FLAGS "-Wformat=2 -Wsign-compare -Wmissing-field-initializers -Wwrite-strings -Wvla")
   if(MSVC)
     # clang-cl sets different default warnings than clang. It also treats -Wall
     # as -Weverything, to match MSVC. Instead -W3 is the alias for -Wall.
@@ -238,8 +238,8 @@ elseif(MSVC)
                             ${MSVC_DISABLED_WARNINGS_LIST})
   string(REPLACE "C" " -w4" MSVC_LEVEL4_WARNINGS_STR
                             ${MSVC_LEVEL4_WARNINGS_LIST})
-  set(CMAKE_C_FLAGS   "-utf-8 -Wall -WX ${MSVC_DISABLED_WARNINGS_STR} ${MSVC_LEVEL4_WARNINGS_STR}")
-  set(CMAKE_CXX_FLAGS "-utf-8 -Wall -WX ${MSVC_DISABLED_WARNINGS_STR} ${MSVC_LEVEL4_WARNINGS_STR}")
+  set(CMAKE_C_FLAGS   "-utf-8 -Wall ${MSVC_DISABLED_WARNINGS_STR} ${MSVC_LEVEL4_WARNINGS_STR}")
+  set(CMAKE_CXX_FLAGS "-utf-8 -Wall ${MSVC_DISABLED_WARNINGS_STR} ${MSVC_LEVEL4_WARNINGS_STR}")
 endif()
 
 if(WIN32)
@@ -562,7 +562,7 @@ endif()
 
 # Add minimal googletest targets. The provided one has many side-effects, and
 # googletest has a very straightforward build.
-add_library(boringssl_gtest third_party/googletest/src/gtest-all.cc)
+add_library(boringssl_gtest STATIC third_party/googletest/src/gtest-all.cc)
 target_include_directories(boringssl_gtest PRIVATE third_party/googletest)
 
 include_directories(third_party/googletest/include)
@@ -649,3 +649,12 @@ add_custom_target(
     WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
     DEPENDS all_tests bssl_shim handshaker fips_specific_tests_if_any
     USES_TERMINAL)
+
+include(GNUInstallDirs)
+install(TARGETS bssl crypto ssl
+  RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
+  LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+  ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}")
+
+install(DIRECTORY include/ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
+
diff --git a/crypto/pkcs8/pkcs12_test.cc b/crypto/pkcs8/pkcs12_test.cc
index e67630d62..1641a6c6d 100644
--- a/crypto/pkcs8/pkcs12_test.cc
+++ b/crypto/pkcs8/pkcs12_test.cc
@@ -34,7 +34,7 @@ std::string GetTestData(const char *path);
 static const char kPassword[] = "foo";
 
 // kUnicodePassword is the password for unicode_password.p12
-static const char kUnicodePassword[] = u8"Hello, 世界";
+static const char* kUnicodePassword = reinterpret_cast<const char*>(u8"Hello, 世界");
 
 static bssl::Span<const uint8_t> StringToBytes(const std::string &str) {
   return bssl::MakeConstSpan(reinterpret_cast<const uint8_t *>(str.data()),
@@ -391,7 +391,7 @@ TEST(PKCS12Test, RoundTrip) {
                 {bssl::Span<const uint8_t>(kTestCert2)}, 0, 0, 0, 0);
 
   // Test some Unicode.
-  TestRoundTrip(kPassword, u8"Hello, 世界!",
+  TestRoundTrip(kPassword, reinterpret_cast<const char*>(u8"Hello, 世界!"),
                 bssl::Span<const uint8_t>(kTestKey),
                 bssl::Span<const uint8_t>(kTestCert),
                 {bssl::Span<const uint8_t>(kTestCert2)}, 0, 0, 0, 0);
diff --git a/crypto/x509v3/internal.h b/crypto/x509v3/internal.h
index e510b4092..bd004847f 100644
--- a/crypto/x509v3/internal.h
+++ b/crypto/x509v3/internal.h
@@ -58,7 +58,7 @@ int x509v3_cache_extensions(X509 *x);
 // it decodes an IPv4 address, it writes the result to the first four bytes of
 // |ipout| and returns four. If it decodes an IPv6 address, it writes the result
 // to all 16 bytes of |ipout| and returns 16. Otherwise, it returns zero.
-int x509v3_a2i_ipadd(unsigned char ipout[16], const char *ipasc);
+int x509v3_a2i_ipadd(unsigned char* ipout, const char *ipasc);
 
 
 #if defined(__cplusplus)
diff --git a/decrepit/ripemd/ripemd_test.cc b/decrepit/ripemd/ripemd_test.cc
index 0700baee0..2eeb38210 100644
--- a/decrepit/ripemd/ripemd_test.cc
+++ b/decrepit/ripemd/ripemd_test.cc
@@ -109,7 +109,7 @@ TEST(RIPEMDTest, RunTest) {
       0x37, 0xf9, 0x7f, 0x68, 0xf0, 0x83, 0x25, 0xdc, 0x15, 0x28};
 
   if (OPENSSL_memcmp(digest, kMillionADigest, sizeof(digest)) != 0) {
-    fprintf(stderr, u8"Digest incorrect for “million a's” test: ");
+    fprintf(stderr, "%s", reinterpret_cast<const char*>(u8"Digest incorrect for “million a's” test: "));
     hexdump(stderr, "", digest, sizeof(digest));
     ok = 0;
   }

当然 boringssl 的支持和升级 openssl 还涉及我们开发框架的基础库 atframe_utils 的适配。后面 专门写一篇总结吧。

protobufstd::to_string 的使用和交叉编译适配

protobuf 从 v3.14.0 版本开始依赖 C++11,直接使用了。其中有个比较特别的地方是使用了 std::to_string 这个API。为什么说这个API特别呢?是因为Clang从3.3版本开始就宣传支持C++11的全部特性了(详见: https://clang.llvm.org/cxx_status.html),但是其实它带的 libc++ 对C++11库的支持还不完整。在我的测试中 Clang 6.0 版本(对应AppleClang 版本10.0)带的 libc++ 才开始能够正常使用这个接口。所以在这些老的编译器下需要降级到 v3.13.0 。

另一个问题是现在的 protobuf 已经支持了 cmake 的config模式的导出库。然后install完以后,会有一个 protobuf-module.cmake 文件用于兼容老的模式提供的函数和变量,可以通过 find_package(Protobuf) 设置 set(protobuf_MODULE_COMPATIBLE TRUE) 选项来开启。在交叉编译时,我们其实是不需要编译 protoc 的,但是这个模块没有考虑到我们可能关闭某些模块的情况。所以需要打一些Patch。

diff --git a/cmake/protobuf-module.cmake.in b/cmake/protobuf-module.cmake.in
index 09b9d29..8787d65 100644
--- a/cmake/protobuf-module.cmake.in
+++ b/cmake/protobuf-module.cmake.in
@@ -134,6 +134,7 @@ get_target_property(Protobuf_INCLUDE_DIRS protobuf::libprotobuf
   INTERFACE_INCLUDE_DIRECTORIES)
 
 # Set the protoc Executable
+if (NOT Protobuf_PROTOC_EXECUTABLE AND TARGET protobuf::protoc)
 get_target_property(Protobuf_PROTOC_EXECUTABLE protobuf::protoc
   IMPORTED_LOCATION_RELEASE)
 if(NOT EXISTS "${Protobuf_PROTOC_EXECUTABLE}")
@@ -152,6 +153,7 @@ if(NOT EXISTS "${Protobuf_PROTOC_EXECUTABLE}")
   get_target_property(Protobuf_PROTOC_EXECUTABLE protobuf::protoc
     IMPORTED_LOCATION_NOCONFIG)
 endif()
+endif()
 
 # Version info variable
 set(Protobuf_VERSION "@[email protected]")

另外,因为我们需要兼容到 GCC 4.8。所使用的 protobuf v3.13.0 里对关闭 protoc 和单元测试以后,开启 libprotoc 也没有支持,所以也需要Patch一下。

diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt
index 9ca31ac..a789e29 100644
--- a/cmake/CMakeLists.txt
+++ b/cmake/CMakeLists.txt
@@ -44,6 +44,7 @@ option(protobuf_BUILD_TESTS "Build tests" ON)
 option(protobuf_BUILD_CONFORMANCE "Build conformance tests" OFF)
 option(protobuf_BUILD_EXAMPLES "Build examples" OFF)
 option(protobuf_BUILD_PROTOC_BINARIES "Build libprotoc and protoc compiler" ON)
+option(protobuf_BUILD_LIBPROTOC "Build libprotoc" OFF)
 if (BUILD_SHARED_LIBS)
   set(protobuf_BUILD_SHARED_LIBS_DEFAULT ON)
 else (BUILD_SHARED_LIBS)
@@ -64,8 +65,6 @@ include(protobuf-options.cmake)
 # Overrides for option dependencies
 if (protobuf_BUILD_PROTOC_BINARIES OR protobuf_BUILD_TESTS)
   set(protobuf_BUILD_LIBPROTOC ON)
-else()
-  set(protobuf_BUILD_LIBPROTOC OFF)
 endif ()
 # Path to main configure script
 set(protobuf_CONFIGURE_SCRIPT "../configure.ac")

交叉编译 gRPC 和对 abseil-cpp 的适配

abseil-cpp 是 Google 的STL扩展库,用来提前体验一些新版本的STL的功能。我们的基础框架倒是不依赖它,但是 gRPCopentelemetry-cpp 都依赖它。 abseil-cpp 的问题仍然是在一些编译器适配上,特别是它其实对编译器版本要求比较高。而且比较坑的是 gRPC 申明支持的编译器版本比 abseil-cpp 声明支持的版本要老,然而 abseil-cpp 却是 gRPC 的依赖项之一,所以我们也得按实际的测试来看支持性。

新增对 gRPC 交叉编译的支持并不是说原来的版本不支持。而是原来的版本里 cmake-toolset 在交叉编译的场景下只编译库,不编译宿主平台的 gRPC 代码生成插件。这主要是因为 gRPC 的依赖特别多。所以这次大规模重构了编译依赖库的变量继承部分:把很多原先默认继承的选项分离成了默认继承的 CMAKE_XXX 和默认不继承的 CMAKE_HOST_XXX ;并且交叉编译的可执行程序搜索目录加入了host平台的二进制目录,并且共享交叉编译时host平台和非交叉编译时target平台的默认输出目录。这样可以最大限度地共享已有的编译缓存;另外优化了一些重置cmake会用到的环境变量的脚本。因为有些构建流程会通过设置环境变量来影响构建选项。

gRPC 对新版本的编译器适配也有一些问题。比如 gRPC 里使用了 std::string_view::string_view(nullptr) 。而这个接口在C++23里被移除了(详见: https://en.cppreference.com/w/cpp/string/basic_string_view/basic_string_view )。 那么在一些新式的编译器开启 C++23的时候,比如 (Visual Studio 2022里带的MSVC v14.30)就会编译不过,这里也是打个Patch就好了。

GCC/Clang STL BUG

在适配 gRPC 的过程中,我还碰到了一个GCC和Clang的BUG。我只在GCC上碰到了,但是按 stackoverflow 上的相关问题的说法Clang也是有问题的。我本地的Clang版本都比较高,在高版本的Clang上是没有这个问题的。相关的链接在此: https://stackoverflow.com/questions/53408962/try-to-understand-compiler-error-message-default-member-initializer-required-be

触发的代码是 gRPC v1.42.0 版本的 src/core/ext/xds/xds_api.h 文件(老版本没有这个问题,因为实现不一样)。相关代码是:

struct Duration {
  int64_t seconds = 0;
  int32_t nanos = 0;
  bool operator==(const Duration& other) const {
    return seconds == other.seconds && nanos == other.nanos;
  }
  std::string ToString() const {
    return absl::StrFormat("Duration seconds: %ld, nanos %d", seconds, nanos);
  }
};

和下面的 absl::variant<UnknownAction, RouteAction, NonForwardingAction> action;

在这种语法中,默认生成的构造函数应该是能够初始化 secondsnanos 的。后面这一行会报 /usr/include/c++/9/type_traits:883:12: error: default member initializer for ‘grpc_core::XdsApi::Duration::seconds’ required before the end of its enclosing class 这个错误。

abseil-cpp 会在自带的STL支持的时候使用STL版本的标准库,否则才是使用 abseil-cpp 内的版本。

虽然这是编译器的问题,但是我们也不得不打Patch绕开他。

支持 Android NDK r23

Android NDK r23 是当前最新的LTS版本,里面的 android.toolchain.cmake 锁提供的信息和NDK的目录结构和之前的版本有比较大的变化,所以这里适配了一下。详情就不列举了,大致上就是一些平台相关的变量和新的NDK没有platform目录了。这个目录以前是放不同版本的的一些基础库文件的。

macOS宿主机编译的适配

这里碰到的问题是默认情况下,macOS使用的是xcode环境(默认编译器是 /Applications/Xcode_12.4.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc/Applications/Xcode_12.4.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ )。其实我们需要命令行工具环境 ( /usr/bin/clang/usr/bin/clang++ )。因为有些包会链接 -framework CoreFoundation 在xcode环境中不设置 SYSROOT 是找不到的。而我们在交叉编译到iOS和iPhoneSimulator的时候其实是会重置掉SYSROOT让编译器从默认默认目录里找,就会编译不过。所以我的解决方案是直接找到并使用命令行工具版本的 clang 。

libuv 和MinGW

最后一个问题是和 libuv 还有MinGW相关。最新版本的MinGW更新了一些头文件,导致即便是当前最新版本的 libuv ( v1.42.0 ) 仍然是编译不过。这个问题在 https://github.com/libuv/libuv/commit/3e90bc76b036124c2a94f9bf006af527755271cf 里修复了,但是还未发布。所以 cmake-toolset 不得不把 libuv 指向打了patch的版本。

另外就是 libuvv1.42.0 版本开始有了cmake config的官方导出支持。但是有点问题,uv_a 未导出。我给提交了一个 PR: https://github.com/libuv/libuv/pull/3373 。不知道是否能够合入,也不知道啥时候能合入。目前在 cmake-toolset 里也有一份这个Patch。

总结

近期对 cmake-toolset 的改造主要就上面这些了。对一些依赖库的升级过程中,碰到的不是特别典型的问题我就不一一列举了。目前CI测试的平台增加了 Android NDK,iOS,iPhoneSimulator。我自己测试过的平台已经扩大到了 Visual Studio 2022 (MSVC v143) ,Android NDK r23,GCC 11,LLVM/Clang 13 这些非常高版本的编译环境,且测试过开启部分 C++23 特性时的兼容性。后面可能陆陆续续也会碰到更多问题,欢迎大家一起交流改进。