Grpc.Tools MSBuild 集成机制深度解析:从 .proto 文件到 C 代码生成的完整构建管线
2026/9/11 5:36:43 网站建设 项目流程

Grpc.Tools MSBuild 集成机制深度解析:从 .proto 文件到 C# 代码生成的完整构建管线

【免费下载链接】grpcC++ based gRPC (C++, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpc

导读

Grpc.Tools是 gRPC 仓库中面向 .NET/C# 生态的 NuGet 工具包,它把protoc编译器与grpc_csharp_plugin插件封装进 MSBuild 构建系统,让开发者只需在.csproj中声明<Protobuf>项,即可在每次构建时自动完成.proto编译、生成 C# 代码并将其纳入 CSC 编译。本文以 implementation_notes.md(Grpc.Tools 维护者视角的内部实现笔记)为主体,结合 ProtoCompile.cs、ProtoCompilerOutputs.cs、ProtoReadDependencies.cs、ProtoToolsPlatform.cs 等源码,完整还原包内文件布局、自定义 MSBuild 任务、增量构建判断、设计时构建处理等内部机制,并给出可直接落地的配置参考。读完你将理解"一次<Protobuf>声明如何在幕后驱动 protoc 与增量构建",并能自行排查构建集成问题。


一、NuGet 包内文件布局

Grpc.Tools的本质是一个"构建期工具包":它没有运行时组件,所有内容都是为了让 MSBuild 在编译项目前自动把.proto文件翻译成 C# 源码。整个包的布局在 Grpc.Tools.csproj 中有清晰的资产定义。包内文件可分为四类。

1.1.props.targets文件

NuGet 约定:包内build\目录下的同名.props.targets文件会被自动注入到引用项目的构建中——.props注入到项目文件顶部(最先求值),.targets追加到项目文件底部(最后求值),从而实现对属性(Property)和目标(Target)的定义与挂钩。

Grpc.Tools的入口文件为:

  • build\Grpc.Tools.props,内部再导入:
    • build\_grpc\_Grpc.Tools.props
    • build\_protobuf\Google.Protobuf.Tools.props
  • build\Grpc.Tools.targets,内部再导入:
    • build\_grpc\_Grpc.Tools.targets
    • build\_protobuf\Google.Protobuf.Tools.targets

_grpc_protobuf两套目录分别承载 gRPC 插件侧与 protobuf 编译器侧的构建逻辑,职责分离、便于分别维护。

1.2 Visual Studio 属性页

为了在 Visual Studio 的属性窗口中暴露每个.proto文件的常用选项(如GrpcServices),包内附带两份属性页 XML:

  • build\_protobuf\Protobuf.CSharp.xml(由Google.Protobuf.Tools.targets引入)
  • build\_grpc\Grpc.CSharp.xml(由_Grpc.Tools.targets引入)

这两份 XML 描述了属性页在 IDE 中的呈现方式,用户层面的交互效果与选项说明可参见 BUILD-INTEGRATION.md。

1.3 自定义任务 DLL

包含自定义 MSBuild 任务(ProtoCompileProtoCompilerOutputs等)的程序集名为Protobuf.MSBuild.dll(见 Grpc.Tools.csproj),按目标框架分别打包在:

  • build\_protobuf\netstandard2.0
  • build\_protobuf\net45

对应 Grpc.Tools.csproj 中的<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>,以兼容经典 .NET Framework 项目与 .NET Core/.NET 5+ SDK 项目。

1.4 protoc 与 grpc_csharp_plugin 原生二进制

包内随附protoc(protobuf 编译器)与grpc_csharp_plugin(C# gRPC 代码生成插件)的原生可执行文件,覆盖多操作系统与 CPU 架构。从 Grpc.Tools.csproj 可以精确看到打包矩阵:

平台protoc 资产路径grpc_csharp_plugin 资产路径
Windows x86tools/windows_x86/protoc.exetools/windows_x86/grpc_csharp_plugin.exe
Windows x64tools/windows_x64/protoc.exetools/windows_x64/grpc_csharp_plugin.exe
Linux x86tools/linux_x86/protoctools/linux_x86/grpc_csharp_plugin
Linux x64tools/linux_x64/protoctools/linux_x64/grpc_csharp_plugin
Linux arm64tools/linux_arm64/protoctools/linux_arm64/grpc_csharp_plugin
macOS universaltools/macosx_universal/protoctools/macosx_universal/grpc_csharp_plugin

注意 macOS 上只发布单个 universal(x64 + arm64)二进制。构建时由ProtoToolsPlatform任务检测当前机器的 OS 与 CPU 来决定选用哪个二进制,并支持通过 MSBuild 属性或环境变量覆盖为自定义可执行文件:

  • Protobuf_ProtocFullPath属性,或PROTOBUF_PROTOC环境变量:protoc可执行文件的完整路径
  • gRPC_PluginFullPath属性,或GRPC_PROTOC_PLUGIN环境变量:gRPC C# 插件的完整路径

二、自定义目标如何挂钩到标准构建流程

Grpc.Tools不在构建流程中"另起炉灶",而是通过 MSBuild 标准的BeforeTargets/AfterTargets/DependsOnTargets机制,把自定义目标插入到预定义目标的前后(实现笔记称这种目标为glue,即"胶水"目标)。三处挂钩点如下:

挂钩位置注入的目标作用
PrepareForBuild之前Protobuf_SanityCheck校验项目类型是否受支持(如是否为 C# 项目)
BeforeCompile之前所有编译.proto并生成.cs的目标生成的文件会被加入 C# 编译器输入列表
CoreClean之后Protobuf_Clean清理由 protobuf 编译器生成的产物

其中两个glue目标承担"插入"职责:

  • _Protobuf_Compile_BeforeCsCompile:表面上看似没有实质动作,但通过指定BeforeTargetsDependsOnTargets,把Protobuf_Compile插入构建流程——并且仅当这是 C# 项目时才生效。
  • _Protobuf_Clean_AfterCsClean:把Protobuf_Clean插入构建流程——同样仅在 C# 项目中生效。

这种"条件插入"设计保证了非 C# 项目(如 VB、F# 项目)引用该包时不会触发 protobuf 编译逻辑。


三、四个自定义 MSBuild 任务及其源码实现

这些任务全部以 C# 实现于 Grpc.Tools 项目(对应目录 src/csharp/Grpc.Tools),打包进Protobuf.MSBuild.dll。实现笔记逐一列举了四个任务,下面结合源码说明其职责与核心逻辑。

3.1 ProtoToolsPlatform:探测操作系统与 CPU

任务源码 ProtoToolsPlatform.cs 输出两个属性:

  • Oslinuxmacosxwindows(未知则置空)
  • Cpux64x86arm64universal(未知则置空)

从 Execute 方法 可以看到两条重要的平台归一化逻辑:

  1. macOS 统一为 universal:只要Os == "macosx"Cpu一律被改写为"universal",对应包内单一tools/macosx_universal/目录;
  2. Windows arm64 降级为 x86:在 Windows arm64 上暂用 x86 二进制("until a native protoc is shipped",即直到官方提供原生 arm64 protoc 为止)。

这一探测结果最终用于拼接tools/{os}_{cpu}/protoc[.exe]形式的二进制路径。

3.2 ProtoCompilerOutputs:预测 protoc 的产出

该任务用于不真正调用 protoc的情况下,尽量猜出会生成哪些文件(best-effort)。源码 ProtoCompilerOutputs.cs 的关键点在 Execute 方法:

  • 通过GeneratorServices.GetForLanguage(Generator, Log)获取语言相关的生成器服务(当前支持csharpcpp);
  • 对每个<Protobuf>项调用generator.PatchOutputDirectory(proto)得到补齐了输出目录元数据的副本(输出为PatchedProtobuf);
  • 调用generator.GetPossibleOutputs(patchedProto)得到可能的输出文件列表(输出为PossibleOutputs),并为每个输出项设置Source元数据,形如<ItemName Include="MyProto.cs" Source="my_proto.proto" />,该Source是后续将生成文件映射回.proto文件的键。

注释中特别说明:即使存在旧的依赖缓存也不会参考它,因为文件可能被重构过(例如某.proto是否生成 gRPC 代码会发生变化),因此收集所有"可能"的产物,稍后由ProtoCompile返回"实际"产物列表。

3.3 ProtoReadDependencies:回读 .protodep 依赖文件

增量构建需要知道上一次实际生成了哪些文件(可能与ProtoCompilerOutputs的猜测不一致)。该任务负责读取此前由 protoc 写出的.protodep依赖文件。源码 ProtoReadDependencies.cs 的 Execute 方法 遍历每个 proto 项,通过DepFileUtil.ReadDependencyInputs(ProtoDepDir, proto.ItemSpec, Log)读取其依赖输入,输出Dependencies项列表——每个依赖项同样带有Source元数据以标明它属于哪个.proto。若ProtoDepDir未设置,则输出空列表(尽力而为,不报错)。

3.4 ProtoCompile:真正驱动 protoc 编译

这是最核心的任务,继承自 MSBuild 的ToolTask(见 ProtoCompile.cs)。实现笔记总结其执行三步曲:

  1. 先写出响应文件(response file),包含传给 protoc 的全部参数;
  2. 运行 protoc 可执行文件,生成.cs文件与.protodep依赖文件;
  3. 读取依赖文件找出实际生成的文件,以 MSBuilditems列表返回,供后续目标使用。

结合源码可以补充大量细节:

  • 响应文件生成ProtocResponseFileBuilder(L444-L474)把每个参数写成一行(protoc 对响应文件的要求是"一行一个参数"),且响应文件使用无 BOM 的 UTF-8 编码(L438),因为 protoc 会拒绝 BOM。
  • 参数映射GenerateResponseFileCommands(L477-L508)将任务属性映射为 protoc 命令行开关:
    • OutputDir--{generator}_out=(如--csharp_out=
    • OutputOptions--{generator}_opt=
    • GrpcPluginExe--plugin=protoc-gen-grpc=
    • GrpcOutputDir--grpc_out=
    • GrpcOutputOptions--grpc_opt=
    • ProtoPath--proto_path=(可多个)
    • DependencyOut--dependency_out=
    • 固定追加--error_format=msvs,使 protoc 的错误输出采用 Visual Studio 可识别的格式
    • AdditionalProtocArguments原样透传(用于实验性开关,如--experimental_allow_proto3_optional
  • 参数校验(L395-L435):校验Generator必须是cppcsharpjavajavananojsobjcphppythonruby之一;ProtoDepDirDependencyOut互斥;使用--dependency_out时 protoc 当前只允许单个输入文件;若指定GrpcPluginExe而未给GrpcOutputDir,则默认与OutputDir相同。
  • 错误/警告解析LogEventsFromTextOutput(L590-L610)用一组正则过滤器(s_errorListFilters,L131-L288)把 protoc 及插件的输出解析为带文件名、行号、列号的 MSBuild 诊断消息,支持带位置/不带位置的 error、warning,以及[libprotobuf WARNING/ERROR/FATAL ...]格式的插件日志。
  • 产物回读Execute(L613-L643)在 protoc 成功运行后,通过DepFileUtil.ReadDependencyOutputs读取依赖文件,得到实际生成文件列表GeneratedFiles,并把依赖文件本身记入AdditionalFileWrites
  • 路径细节TrimEndSlash(L512-L531)处理 protoc "无法消化目录名尾部斜杠"的怪癖,同时小心保留根目录斜杠与 Windows 盘符(如C:\)。

四、高层构建步骤全解

实现笔记给出了五个高层步骤,并提醒"文中提到的 items/properties 名称在写作时点是准确的"。下面按执行顺序逐步展开。

4.1 准备待编译的 .proto 文件列表

构建会在多个阶段创建或更新<Protobuf>项的副本,以设置元数据、剔除不需要的项:

  1. 确保ProtoRoot元数据就绪:由原Protobuf项派生出新列表Protobuf_Rooted,规则如下:
    • 若项目中已显式设置ProtoRoot,保持原值;
    • .proto文件位于项目目录之下,设置ProtoRoot="."
    • .proto文件位于项目目录之外,设置ProtoRoot="<项目目录的相对路径>"
  2. 剔除不需要编译的项:从Protobuf_Rooted中剔除ProtoCompile元数据不为true的项,得到Protobuf_Compile
  3. 设置Source元数据:在Protobuf_Compile项上把Source设为.proto文件名。Source之后作为"生成文件 ↔ .proto 文件"映射的关键键值。

4.2 增量构建处理

增量构建是这套集成的精髓,其目标是在.proto及其依赖未变化时跳过重复编译

收集用于增量判断的文件
  • 目标Protobuf_PrepareCompile调用ProtoCompilerOutputs任务,不实际运行 protoc地猜测将生成哪些文件,结果存入Protobuf_ExpectedOutputs(预期输出);
  • 同一目标还调用ProtoReadDependencies任务,从历史.protodep文件读取上次实际生成的文件,结果存入Protobuf_Dependencies(实际依赖)。之所以两者都要:当实际产物与上次的"最佳猜测"不一致时,以实际为准。

预期输出与上次实际输出共同构成了后续时间戳比较的对象。

增量判断机制

目标_Protobuf_GatherStaleBatched利用 MSBuild 内置的增量构建特性(比较目标InputOutput的时间戳)来判断哪些文件过期:

  • Inputs(输入)
    • .proto文件的时间戳
    • 上次生成文件的时间戳(来自.protodep
    • MSBuild 项目文件的时间戳
  • Outputs(输出)
    • 预期生成文件的时间戳

判断采用MSBuild 目标批处理(target batching):通过在 Input 中指定Source元数据来分批,输入与输出中Source元数据相同的项归入同一批次,从而逐个.proto文件对照其预期产物检查过期状态。

过期项会被打上_Exec=true元数据写入_Protobuf_OutOfDateProto列表;随后在目标_Protobuf_GatherStaleFiles中,把_Protobuf_OutOfDateProto没有_Exec==true的项剔除,最终只剩真正需要重新编译的项。

4.3 编译 .proto 文件

目标_Protobuf_CoreCompile_Protobuf_OutOfDateProto列表中的每个需要编译的.proto文件逐一运行ProtoCompile任务,调用 protoc 完成编译,实际生成的文件返回在_Protobuf_GeneratedFiles列表。

关于"预期文件未生成"的处理:如果存在预期文件实际未被 protoc 生成,行为取决于生成目录位置:

  • 预期文件应在项目内(如中间目录obj):创建空文件作为占位,防止增量构建反复触发不必要的重编译;
  • 预期文件在项目外:默认不创建空文件,而是输出一条警告(此行为可通过属性配置,见下文Protobuf_NoWarnMissingExpected)。

实现笔记在此留下了一个开放问题(TODO):为什么项目内与项目外的文件要区别对待?结合 BUILD-INTEGRATION.md 的说明可以找到部分答案:在项目外创建空文件会"污染"项目目录之外的位置,因此用警告替代;典型场景是.proto中没有 service 定义时*Grpc.cs不会被生成,却又是预期输出。

4.4 把生成的 .cs 文件加入 C# 编译

目标_Protobuf_AugmentLanguageCompile预期生成的文件加入Compile项列表(即 CSC 编译的文件清单)。

实现笔记特意强调:加入的是预期(expected)文件,而非实际(actual)生成文件,并且这一步发生在 protoc 真正运行之前。原因从增量机制可以推断:只有让Compile列表在编译开始前就稳定包含这些路径,MSBuild 的输入/输出比较(第 4.2 节)才有一致的参照系;若依赖 protoc 运行完毕后的实际产物,则会形成先有鸡还是先有蛋的循环依赖。实现笔记同样在此留了一个 TODO,说明该设计取舍仍是维护者持续审视的点。


五、设计时构建(Design-Time Builds)的处理

设计时构建是 Visual Studio 为收集项目信息而触发的特殊构建:并非用户主动发起,而是在文件被添加、删除或保存时可能自动触发(例如用于 IntelliSense、错误列表等 IDE 功能)。

Grpc.Tools曾试图优化设计时构建——在设计时构建中禁用对 protoc 的调用。但这个优化会在 Visual Studio 中引发问题:因为生成的.cs文件可能不存在或已过期,依赖它们的代码随之报错。

因此当前行为是:设计时构建与普通构建完全一致,照常调用 protoc。若确实要恢复旧行为,可在项目文件中将DisableProtobufDesignTimeBuild属性设为true且仅当处于设计时构建时,例如:

<PropertyGroup Condition="'$(DesignTimeBuild)' == 'true' "> <DisableProtobufDesignTimeBuild>true</DisableProtobufDesignTimeBuild> </PropertyGroup>

六、自动包含 .proto 文件

对于 SDK 风格项目,可以免去逐个书写<Protobuf>项,自动纳入项目目录及其子目录下发现的所有.proto文件。只需在项目文件中设置属性:

<PropertyGroup> <EnableDefaultProtobufItems>true</EnableDefaultProtobufItems> </PropertyGroup>

需要注意:

  • 该属性默认不设置(默认false),即默认情况下必须在项目中显式包含<Protobuf>项,.proto文件才会被编译;
  • BUILD-INTEGRATION.md 同时建议:除最简单的项目外,不建议依赖自动包含,因为自动包含无法对单个文件精细化控制GrpcServices等元数据。

七、配套的构建集成配置参考

虽然实现笔记面向维护者,但为了让读者能把内部机制与日常用法对应起来,这里补充 BUILD-INTEGRATION.md 中与上文机制直接相关的配置要点。

7.1<Protobuf>项的核心元数据

名称默认值说明
GrpcServicesBoth生成哪些 gRPC 存根:None/Client/Server/BothBoth会同时生成Myfile.csMyfileGrpc.csNone只生成消息代码
ProtoRoot见注释.proto文件的公共根目录;项目内文件默认.,项目外文件默认其所在目录名
ProtoCompiletrue设为false时不调用 protoc
CompileOutputstrue设为false时仍生成 C# 代码,但不纳入 C# 编译("只生成不编译"场景)
OutputDirProtobuf_OutputPath(默认即IntermediateOutputPath,如obj/Debug/net6.0/消息代码输出目录
GrpcOutputDir跟随OutputDirgRPC 存根输出目录
OutputOptions/GrpcOutputOptions传给--csharp_opt/--grpc_opt的额外选项,多项用分号;分隔
AdditionalProtocArguments原样透传给 protoc 的额外命令行参数(如实验性开关),多项用;分隔
AdditionalImportDirs见注释追加的--proto_path搜索目录,按给定顺序搜索
Accesspublic生成类的访问级别:public/internal

这些元数据中的ProtoRootGrpcServicesOutputDir等,正是第 4.1、4.3 节所述内部目标处理的对象——Protobuf_RootedProtoRoot推导、ProtoCompilerOutputsPatchOutputDirectoryProtoCompile--grpc_out参数映射,均围绕它们展开。

7.2 与增量机制直接相关的 MSBuild 属性

属性默认值说明
Protobuf_NoWarnMissingExpectedfalse设为true时,对"预期文件未生成"的情况不再告警(对应第 4.3 节项目外不建空文件时的警告)
Protobuf_OutputPathIntermediateOutputPath设置<Protobuf>OutputDir的默认值
EnableDefaultProtobufItemsfalse自动包含项目目录下的.proto文件(第 6 节)
Protobuf_StandardImportsPathNuGet 包内 well-known types 目录自动通过-I/--proto_path传给 protoc 的标准导入路径
Protobuf_ProtocFullPath/gRPC_PluginFullPath包内二进制覆盖 protoc / 插件路径(与PROTOBUF_PROTOC/GRPC_PROTOC_PLUGIN环境变量等价)

7.3 常见使用片段

基础用法——声明.proto并默认生成 client 与 server 存根:

<ItemGroup> <Protobuf Include="Protos\greet.proto" /> </ItemGroup>

只生成客户端存根:

<ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Client" /> </ItemGroup>

引用项目目录之外的共享.proto(配合Link让文件在 VS 中可见):

<ItemGroup> <Protobuf Include="..\Proto\aggregate.proto" GrpcServices="Client" Link="Protos\aggregate.proto"/> </ItemGroup>

批量设置:全部.proto默认不生成 gRPC 代码,仅hello/bye子目录生成 client+server(注意用Update而非Include,避免重复添加):

<ItemGroup> <Protobuf Include="**/*.proto" GrpcServices="None" /> <Protobuf Update="**/hello/*.proto;**/bye/*.proto" GrpcServices="Both" /> </ItemGroup>

只生成 C# 源码而不参与编译(输出到与.proto同目录):

<ItemGroup> <Protobuf Include="**/*.proto" OutputDir="%(RelativeDir)" CompileOutputs="false" /> </ItemGroup>

八、结语

Grpc.Tools的 MSBuild 集成是一套精心设计的构建管线:以.props/.targets的自动注入为入口,通过Protobuf_SanityCheck_Protobuf_Compile_BeforeCsCompile_Protobuf_Clean_AfterCsClean三个挂钩点接入标准构建;用ProtoToolsPlatform(平台探测)、ProtoCompilerOutputs(产物预测)、ProtoReadDependencies(依赖回读)、ProtoCompile(真实编译)四个自定义任务,配合Protobuf_RootedProtobuf_Compile_Protobuf_OutOfDateProto_Protobuf_GeneratedFiles的项流转,最终实现"声明即编译、变更才重编"的增量体验。

对维护者与进阶使用者而言,implementation_notes.md 中标注的两处 TODO(项目内/外空文件策略差异、为何加入预期文件而非实际文件)正是理解其设计取舍的窗口;而 BUILD-INTEGRATION.md 与 Grpc.Tools/README.md 则提供了面向使用者的完整参考。掌握这套内部机制后,无论是排查"改了 proto 不重新生成"的增量问题,还是为特殊架构接入自定义编译器,都能做到心中有数。

【免费下载链接】grpcC++ based gRPC (C++, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询