ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CMake源码编译安装gRPC:彻底搞懂gRPCTargets.cmake的生成与使用

CMake源码编译安装gRPC:彻底搞懂gRPCTargets.cmake的生成与使用 先说我为什么要写这篇东西。过去两年里我在好几个C项目里集成gRPC几乎每一次都会遇到和CMake相关的坑要么是系统包管理器装的gRPC版本太老要么是源码编译完了发现CMake根本找不到gRPC的包配置文件再要么是find_package(gRPC)侥幸过了链接时却报一堆莫名其妙的错误。折腾到最后问题的根源几乎都指向同一个东西——gRPCTargets.cmake有没有被正确生成、正确安装、正确被下游找到。这篇文章就把这条路完整走一遍用CMake从源码编译安装gRPC重点讲清楚gRPCTargets.cmake是怎么来的、里面写了什么、以及你自己的项目该怎么把它用起来。1. 什么情况下你才需要折腾源码编译gRPC1.1 系统包管理器里的gRPC为什么不够用很多人第一反应是直接用系统包管理器装gRPCUbuntu下就是apt install libgrpc-devmacOS下就是brew install grpc。对于快速验证和纯研究场景这确实是最省事的路径但一旦进到正式项目问题会接二连三冒出来。第一个痛点是版本滞后。Ubuntu官方源里的gRPC版本通常比上游落后一大截比如Ubuntu 22.04自带的gRPC是1.30.2而上游早就到1.6x了。如果你的项目需要新版才有的API或者需要修复某些性能问题包管理器这条路直接堵死。第二个痛点是包拆分不一致。不同发行版对gRPC的打包策略不一样有的把grpc_cpp_plugin拆到单独的包有的把CMake配置文件放在非常规路径下。我遇到过最离谱的一次是找gRPCConfig.cmake找半天最后发现它被装在/usr/lib/x86_64-linux-gnu/cmake/grpc/下而CMAKE_PREFIX_PATH默认根本不会去扫这个目录。第三个痛点是静态链接需求。不少服务端项目为了部署方便要求全静态链接但发行版自带的gRPC包通常只有动态库甚至有的一起装好的protobuf还是动态链接的混链起来非常难受。第四个痛点是交叉编译。安卓NDK、嵌入式平台、定制工具链这些场景下发行版包完全不可用只能源码编译。所以当你遇到以上任何一种情况时源码编译安装gRPC就不是一个可选项而是必选项。而一旦走上源码编译这条路就绕不开CMake的那套find_package机制也就绕不开gRPCTargets.cmake。1.2 源码编译要解决的核心问题源码编译gRPC表面上只是把源码变成库文件但对下游C项目来说真正的核心问题只有一个让gRPC把自己登记到CMake的包系统里让其他项目可以通过find_package(gRPC CONFIG REQUIRED)干净地找到它并拿到一组可直接链接的target比如gRPC::grpc。CMake规范的做法是库项目在安装阶段把自己的库目标导出成一份清单文件这就是gRPCTargets.cmake然后再包一层gRPCConfig.cmake作为入口。gRPCConfig.cmake负责处理依赖搜索、版本检查最后include那份gRPCTargets.cmake把目标导入进来。这份机制是CMake原生的install(EXPORT ...)能力gRPC只是按照标准方式使用它。所以你会发现gRPCTargets.cmake不是手工写的而是CMake在安装阶段根据install(TARGETS ... EXPORT ...)指令自动生成的。理解这一点后面所有排错思路都会清晰很多。接下来的内容就围绕怎么让这条生成链路完整跑通来展开。2. gRPC的CMake构建是怎么把自己导出成可用包的2.1 顶层CMakeLists.txt里那些决定成败的开关gRPC的顶层CMakeLists.txt是整场戏的总导演。它把整个项目拆成了几十个库target比如grpc、grpc_unsecure、grpc、grpc_unsecure、gpr、grpc_cpp_plugin等。这些target在构建时是内部目标在安装时则通过install(TARGETS ... EXPORT grpcTargets)统一导出。配置阶段有几个开关需要特别留意它们直接决定了安装出来的包是否完整CMake选项作用推荐值gRPC_INSTALL是否执行安装规则不开这个gRPCTargets.cmake根本不会生成ONgRPC_BUILD_TESTS是否编译测试代码纯浪费编译时间OFFgRPC_BUILD_CSHARP_EXT是否编译C#扩展非C#项目建议关闭OFFgRPC_SSL_PROVIDER选择OpenSSL提供方module用自带源码package用系统库module或package视环境而定gRPC_PROTOBUF_PROVIDERprotobuf提供方module用third_partypackage用已安装的protobuf一般用module省事ABSL_ENABLE_INSTALL是否安装abseil-cpp的targetsgRPC 1.4x以后必须要开ON其中最容易坑到人的就是gRPC_INSTALL。很多人编译时图省事直接跑一个cmake -S . -B build就完事结果build目录里确实生成了库文件但因为没有安装阶段gRPCTargets.cmake根本不存在。后面项目里find_package(gRPC CONFIG)时CMake能找到gRPCConfig.cmake吗能找到因为有些场景下它会在build目录里生成gRPCConfig.cmake但它依赖的gRPCTargets.cmake却不在预期位置于是立刻报错。2.2 gRPCTargets.cmake是怎么被generate出来的要理解gRPCTargets.cmake的生成机制先看CMake的install(EXPORT)指令。GNU Hello World那种单库项目都见过这种写法install(TARGETS mylib EXPORT mylibTargets LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin) install(EXPORT mylibTargets FILE mylibTargets.cmake NAMESPACE mylib:: DESTINATION lib/cmake/mylib)当cmake执行到install(EXPORT)时会读取指定target的构建信息把每个target的路径、编译选项、依赖关系、头文件搜索目录等序列化到mylibTargets.cmake文件中。这个文件在项目构建完成后、执行cmake --install时被写入安装前缀目录。gRPC做的事情完全一样只是把导出名定为grpcTargets最终生成的文件名就是gRPCTargets.cmake。还有一个很多人没注意的细节在build目录里其实也会生成一份grpcTargets.cmake这叫构建树导出build tree export是给add_subdirectory方式使用的。安装目录里那份是安装树导出install tree export两者内容不同——安装树里的路径是相对于CMAKE_INSTALL_PREFIX展开的绝对路径构建树里则指向build目录下的中间产物。所以如果你在安装目录里找不到gRPCTargets.cmake可以先检查build目录如果build目录有而安装目录没有基本可以断定安装这一步没执行或者gRPC_INSTALL没开。3. 从零实操一条命令链路生成gRPCTargets.cmake3.1 环境准备与源码获取先交代我这次实操的环境Ubuntu 22.04CMake 3.25gcc 11.4。Android NDK、MSVC的流程大同小异但个别参数有区别后面会单独指出。拉取gRPC源码时务必带上submodule。gRPC对third_party依赖管理非常重abseil、protobuf、re2、c-ares、zlib等都在submodule里git clone --recurse-submodules -b v1.60.0 https://github.com/grpc/grpc.git-b v1.60.0指定版本。这里要提醒一下不要直接clone默认分支然后构建不同gRPC版本对protobuf、abseil的版本要求是强绑定的用发布tag最稳。如果你只需要最新代码体验新特性那当我没说但正式项目一定要用tag。如果你不想让gRPC用自带的third_party而是想使用系统里已装的protobuf和abseil那么得先把这两个库单独装好然后在配置阶段把gRPC_PROTOBUF_PROVIDER设为package。但我的建议是新手直接保持module默认值让gRPC用自己捆绑的依赖编译这样少一个版本配对的烦恼。不过注意即使依赖用moduleabseil那部分仍然需要设置ABSL_ENABLE_INSTALLON否则gRPC安装时不会把abseil的targets一起导出。3.2 配置阶段逐参数拆解配置命令如下我逐参数解释每一个选项的含义cmake -S . -B build \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_INSTALL_PREFIX/opt/grpc \ -DgRPC_INSTALLON \ -DgRPC_BUILD_TESTSOFF \ -DgRPC_BUILD_CSHARP_EXTOFF \ -DABSL_ENABLE_INSTALLON \ -DCMAKE_CXX_STANDARD17CMAKE_BUILD_TYPEReleasegRPC库本身是性能敏感的RPC框架Debug和Release差距很大。还有一个隐藏原因Release编译的gRPC在链接下游Release项目时更不容易出现标准库不一致的问题。CMAKE_INSTALL_PREFIX/opt/grpc安装前缀。这里特别提醒如果你计划在多个项目里复用这套gRPC最好固定一个前缀不要每次装到不同位置否则日后的CMAKE_PREFIX_PATH维护会很痛苦。gRPC_INSTALLON关键开关决定是否生成安装规则没有它就没有gRPCTargets.cmake。gRPC_BUILD_TESTSOFFgRPC的测试套件非常大开着等于把编译时间拉长好几倍。gRPC_BUILD_CSHARP_EXTOFFC#扩展对自己写的C项目毫无用处。ABSL_ENABLE_INSTALLON这条非常关键。gRPC 1.40以后的版本把abseil作为基础组件如果abseil的targets不安装下游项目链接gRPC::grpc时会出现无法找到absl::xxx的报错。Windows下还需要额外注意如果你用的是Visual Studio生成器CMAKE_BUILD_TYPE在构建时通过--config Release指定配置阶段写CMAKE_BUILD_TYPE不生效。另外MSVC的Debug和Release把库文件放在不同目录Debug/和Release/如果gRPC装的是Release而你的项目用Debug链接时经常出现找不到grpc.lib的情况。个人建议在Windows下统一用x64-Release配置。3.3 编译、安装与结果验证配置完成后就是编译和安装cmake --build build -j$(nproc) cmake --install build这里$(nproc)在Linux下是CPU核数macOS下可以用$(sysctl -n hw.ncpu)。实测下来gRPC全量编译大概需要10到20分钟取决于机器性能这个编译时间算是可以接受的。安装完成后去安装目录确认一下关键文件是否生成ls -la /opt/grpc/lib/cmake/grpc/正常情况下会看到这样的文件列表gRPCConfig.cmake gRPCConfigVersion.cmake gRPCTargets.cmake gRPCTargets-release.cmakegRPCConfig.cmake是入口文件gRPCTargets.cmake是核心目标清单gRPCTargets-release.cmake是Release配置下的具体路径映射。如果你只看到gRPCConfig.cmake而没有gRPCTargets.cmake说明gRPC_INSTALL设置有问题。另外还需要检查abseil是否被安装ls -la /opt/grpc/lib/cmake/absl/abseil的安装路径里应该也有abslConfig.cmake和一堆abslTargets.cmake。这里如果缺失下游项目必然报错后面第6章会专门给出排查方法。4. 拆开gRPCTargets.cmake看看里面到底有什么4.1 文件里的target清单与关键字段gRPCTargets.cmake本质上是一长串add_library(... IMPORTED)调用。每个库target都被标记为IMPORTEDCMake会读取其中的IMPORTED_LOCATION和INTERFACE_INCLUDE_DIRECTORIES等属性来链接和寻找头文件。打开文件你会看到类似这样的结构add_library(gRPC::grpc SHARED IMPORTED) set_target_properties(gRPC::grpc PROPERTIES INTERFACE_INCLUDE_DIRECTORIES /opt/grpc/include INTERFACE_LINK_LIBRARIES gRPC::grpc;gRPC::gpr;absl::abseil_dll;...;protobuf::libprotobuf ) add_library(gRPC::grpc SHARED IMPORTED) set_target_properties(gRPC::grpc PROPERTIES INTERFACE_LINK_LIBRARIES gRPC::gpr;absl::cord;...;OpenSSL::SSL )几个关键信息可以留意一是target命令空间。gRPC::前缀就是在install(EXPORT)时由NAMESPACE选项指定的。你项目里写gRPC::grpc底层其实是被导入的add_library(gRPC::grpc SHARED IMPORTED)。二是传递依赖关系。INTERFACE_LINK_LIBRARIES字段里列出了grpc依赖的grpc、gpr、absl、protobuf、OpenSSL等。链接一个gRPC target时CMake会自动把所有这些依赖传下去这也是为什么推荐用target而不是直接指定库路径——target方式能正确处理传递依赖。三是构建类型对应关系。gRPCTargets-release.cmake里专门存Release模式的IMPORTED_LOCATION_RELEASE指向libgrpc.so.1.60.0这样的具体文件。如果下游项目用的是Debug配置而gRPC没装Debug版本CMake会找不到对应位置的库报错时毫无头绪。具体会导出哪些target你可以用这个命令查一下grep add_library /opt/grpc/lib/cmake/grpc/gRPCTargets.cmake会看到gRPC::grpc、gRPC::grpc_unsecure、gRPC::grpc、gRPC::grpc_unsecure、gRPC::gpr、gRPC::grpc_cpp_plugin、gRPC::grpc_python_plugin等。你项目里真正常用的是gRPC::grpc安全链接和gRPC::grpc_cpp_plugin编译器插件。4.2 为什么不能只拷贝一个gRPCTargets.cmake这个坑我踩得记忆犹新。有一次给同事做环境交付我想当然地认为gRPCTargets.cmake就是一份独立配置拷过去就行了。结果对方项目里find_package(gRPC)立刻报错打开文件一看里面所有的INTERFACE_INCLUDE_DIRECTORIES都是绝对路径/opt/grpc/include。你拷贝文件到别处路径依然指向原机器。这个道理其实和pkg-config里的-I/usr/local/include一样config文件里记录的是构建时的绝对路径。所以gRPC的安装目录不能随便移动移动之后要么重新安装要么给下游项目额外设置gRPC_DIR指向新位置。还有一个更隐蔽的问题gRPCTargets.cmake只是清单真正的库文件在lib/目录里头文件在include/目录里这三个部分必须共同存在。想整体迁移gRPC安装目录正确做法是连同include、lib、lib/cmake三个目录一起拷贝且保持相对布局不变。拷贝后如果路径变化要么手动改gRPCTargets.cmake里的路径要么重新编译安装没有第三条路。另外如果你的机器装了多个gRPC版本比如系统自带一个、/opt下面一个一定要检查CMAKE_PREFIX_PATH的搜索顺序。CMake会按CMAKE_PREFIX_PATH里列出的先后顺序搜索靠前的先命中。排查时可以用cmake --debug-find输出详细搜索过程看它到底命中了哪个目录的gRPCConfig.cmake。5. 在自己项目里让find_package(gRPC)真正生效5.1 项目CMakeLists.txt的推荐写法gRPC装好了接下来就是写自己项目的CMakeLists.txt。先给出一个可以抄作业的最小示例cmake_minimum_required(VERSION 3.20) project(my_grpc_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) list(APPEND CMAKE_PREFIX_PATH /opt/grpc) find_package(gRPC CONFIG REQUIRED) find_package(Protobuf CONFIG REQUIRED) file(GLOB_RECURSE PROTO_FILES ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto) set(PROTO_SRCS) set(PROTO_HDRS) set(GRPC_SRCS) set(GRPC_HDRS) foreach(proto ${PROTO_FILES}) get_filename_component(proto_dir ${proto} DIRECTORY) get_filename_component(proto_name ${proto} NAME_WE) list(APPEND PROTO_SRCS ${CMAKE_CURRENT_BINARY_DIR}/proto/${proto_name}.pb.cc) list(APPEND PROTO_HDRS ${CMAKE_CURRENT_BINARY_DIR}/proto/${proto_name}.pb.h) list(APPEND GRPC_SRCS ${CMAKE_CURRENT_BINARY_DIR}/proto/${proto_name}.grpc.pb.cc) list(APPEND GRPC_HDRS ${CMAKE_CURRENT_BINARY_DIR}/proto/${proto_name}.grpc.pb.h) endforeach() add_custom_command( COMMAND protobuf::protoc ARGS --cpp_out${CMAKE_CURRENT_BINARY_DIR}/proto --grpc_out${CMAKE_CURRENT_BINARY_DIR}/proto --pluginprotoc-gen-grpc$TARGET_FILE:gRPC::grpc_cpp_plugin ${PROTO_FILES} DEPENDS ${PROTO_FILES} OUTPUT ${PROTO_SRCS} ${PROTO_HDRS} ${GRPC_SRCS} ${GRPC_HDRS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT Generating protobuf and gRPC sources ) add_executable(my_grpc_demo main.cpp ${PROTO_SRCS} ${PROTO_HDRS} ${GRPC_SRCS} ${GRPC_HDRS}) target_link_libraries(my_grpc_demo gRPC::grpc gRPC::grpc_reflection protobuf::libprotobuf )这里有几个点值得重点说明。list(APPEND CMAKE_PREFIX_PATH /opt/grpc)一行是灵魂所在。如果gRPC的安装路径不在CMake默认搜索范围里find_package找不到包一切白搭。更好的做法是在配置阶段通过-DCMAKE_PREFIX_PATH/opt/grpc传入这样不污染CMakeLists文件适合team协作。find_package(Protobuf CONFIG REQUIRED)也不可省略。gRPC的gRPCTargets.cmake里INTERFACE_LINK_LIBRARIES会引用protobuf::libprotobuf但CMake只有在执行find_package(Protobuf)后才会知道这个target的定义。顺序上先找Protobuf再找gRPC比较稳妥。add_custom_command里的protobuf::protoc是protobuf的编译器启动器--pluginprotoc-gen-grpc$TARGET_FILE:gRPC::grpc_cpp_plugin是关键它会利用gRPC的target属性自动定位grpc_cpp_plugin的可执行文件路径。这句话能work的前提正是gRPC通过install(EXPORT)把grpc_cpp_plugin这个可执行target也导出了。如果没有gRPCTargets.cmake你只能手工写插件路径换个环境就废了。5.2 代码生成和链接顺序那些事链接顺序是个老生常谈但永远有人踩的坑。虽然现在CMake的target依赖会自动传递但在处理静态库时链接器的解析顺序仍然是单向的被依赖的库必须放在依赖它的库后面。实际项目里如果gRPC::grpc内部是静态库标准链接顺序大致是grpc - grpc - gpr - protobuf - absl - ssl - crypto - zlib不过用了CMake target后这些依赖都是自动传递的你不需要手写这一长串。真正会出问题的是当你绕过target、直接去链接某个具体.a文件时少写一个依赖库就报一堆undefined reference。另一个经验是强烈建议用gRPC::grpc_reflection而不是手动链接libgrpc_reflection.so。reflection库提供gRPC的服务反射协议调试grpcurl这种工具时特别有用target写法能自动带上所有依赖省心不少。还有一点项目里如果同时引入OpenSSL要注意版本冲突的可能性。gRPC自带的module模式OpenSSL是静态编进gRPC的和系统OpenSSL共存一般没问题如果你用package模式链接系统OpenSSL而项目其他部分也依赖OpenSSL那么务必保证两边版本一致否则会出现运行时符号冲突表现得很诡异——比如握手失败或者证书解析错误。6. 我踩过的坑安装与集成阶段的典型报错6.1 下游找不到absl/protobuf的targets这是我在多个项目里复现过的经典场景。gRPC本身编译安装成功gRPCConfig.cmake在gRPCTargets.cmake在但下游项目cmake阶段报错CMake Error at /opt/grpc/lib/cmake/grpc/gRPCTargets.cmake:123 (set_target_properties): The link interface of target gRPC::grpc contains: absl::abseil_dll but the target was not found.这句话的意思很直白gRPC::grpc这个target的依赖项absl::abseil_dll没有被导入。原因就是abseil的targets没有被安装到任何CMake可搜索的目录中。解决方式分两步。第一回到gRPC源码目录确认配置参数里有没有-DABSL_ENABLE_INSTALLON如果没有重新配置编译安装。第二如果abseil已经单独安装到别的目录了比如/usr/local那么需要在项目里把这个路径也追加到CMAKE_PREFIX_PATHcmake -DCMAKE_PREFIX_PATH/opt/grpc;/usr/local ..protobuf::libprotobuf找不到的原因类似大概率是find_package(Protobuf CONFIG REQUIRED)没有执行或者系统里的protobuf版本和gRPC内部依赖版本对不上。解决办法是让系统里只保留一套protobuf或者干脆用gRPC的module模式统一构建。6.2 gRPCTargets.cmake缺失或张冠李戴另一种高频报错长这样CMake Error at .../gRPCConfig.cmake:29 (include): include could not find load file: /opt/grpc/lib/cmake/grpc/gRPCTargets.cmake看到这个报错第一反应是去检查/opt/grpc/lib/cmake/grpc/目录正常情况下gRPCTargets.cmake就在那里。如果目录里只有gRPCConfig.cmake没有gRPCTargets.cmake八成是构建时gRPC_INSTALL没有设为ON虽然build目录里有库但安装规则没生成。还有一个场景你确实安装了gRPC但gRPCConfig.cmake是从另一个路径被找到的。比如系统里的/usr/lib/cmake/grpc/有一个旧版gRPCConfig.cmake而你新装的gRPC在/opt/grpc/lib/cmake/grpc/。CMAKE_PREFIX_PATH搜索顺序靠前的是系统目录CMake先去系统目录找到了旧版的配置文件旧版的config再去加载旧版的gRPCTargets.cmake如果你把旧版移除了就会报target缺失。排查这种问题我习惯用cmake --debug-find它会打印CMake查找每个文件时的完整路径遍历过程cmake --debug-find .. 21 | grep -i grpc看输出里实际命中了哪个路径十有八九问题就清楚了。6.3 静态编译选项不一致引发的link错误最后说一个最隐蔽的问题编译选项不一致。表现是find_package完全正常cmake阶段完美通过但一到build阶段链接器报一堆LNK2038MSVC或者undefined referenceGCC。MSVC场景最常见的是/MT和/MD冲突。gRPC编译时用动态运行库/MD而你的项目用了静态运行库/MT链接时就会报LNK2038提示RuntimeLibrary mismatch。解决办法是保持两边一致要么都动要么都静。GCC/Linux下Debug和Release混链也会出问题。gRPC如果用Release编译而你的项目是Debug链接时往往会报undefined reference to symbol其实就是Debug模式引用了一些Release库没有导出的调试符号。更麻烦的在于gRPCTargets-release.cmake只定义了Release的IMPORTED_LOCATIONDebug构建时CMake找不到对应的Debug库文件。要让CMake自动区分最简单的方式是给gRPC同时编译Debug和Release两种配置Windows下用cmake --install build --config Debug和--config Release各执行一次即可Linux下则是分别用Debug和Release的build目录构建安装一次。在我个人实践中更推荐一条简单粗暴的规则gRPC和你的项目统一用同一个构建类型、同一个编译器、同一个前缀。配好之后写进团队的构建脚本里能在源头上省掉我一整天的排查时间。回顾这条路gRPC_INSTALLON是生成gRPCTargets.cmake的总开关ABSL_ENABLE_INSTALLON保证了依赖target完整CMAKE_PREFIX_PATH指向正确是下游找到它的最后一步。三步缺一不可它们环环相扣只在其中一环下功夫往往解决不了问题。希望这篇能帮你少走我走过的这些弯路。
返回列表