首页
/ Protobuf Editions 迁移工具 Prototiller 的需求设计与 ChangeSpec 接口详解

Protobuf Editions 迁移工具 Prototiller 的需求设计与 ChangeSpec 接口详解

2026-09-04 09:29:14作者:平淮齐Percy

本篇解读 Protobuf 仓库中 Prototiller 面向 Editions 的需求设计文档,梳理 Prototiller 这一批量重构工具的三大核心升级工作流(UpgradeEdition、CleanUpFeatures、ModifyFeature),完整剖析推荐的 ChangeSpec 消息 Schema——包括动作类型、安全性开关与路径匹配模式;读完你将理解 Editions 迁移工具如何把“升级 edition、清理冗余 features、精细修改 feature”封装为一套可扩展的变更规格接口,以及该设计与 protoc 内建工具链(janitor / adopter / updater)之间的衔接关系。

1. 背景:Prototiller 是什么,为什么为 Editions 而生

Prototiller 是 Protocol Buffers 项目规划中的“批量重构瑞士军刀”,定位类似 Bazel 的 Buildozer:对大量文件做安全、机械化的结构修改。它的两大目标是:

  1. 在 google3 内部启用 LSC(Large-Scale Change,大规模变更);
  2. 让内外部用户能够安全地修改 .proto 文件。

从设计文档的脉络看(见 prototiller 目录 README),Prototiller 是 Editions 项目 的一部分,开发优先级是让 Editions 相关的重构尽快落地、解除 2023 年 Editions 迁移的阻塞。需要特别注意的是:该目录下存放的是历史设计文档,README 明确说明这些文件代表文档发布时的状态,仅具历史参考价值,不应视为当前状态的文档。

2. 需求总览:只服务 Editions,但保留扩展性

原型接口设计来自内部的 Protochangifier Semantic Actions(未对外公开):它消费一个描述“对输入的 .proto 文件要做什么修改”的 Protobuf 消息。本文档在此基础上规定了一个变体接口——只满足 Editions 的当前需要,同时为未来的变更动作留好扩展空间。

文档给出的总体需求有两组:

需求一:动作(Actions)必须覆盖以下三个 Editions 升级工作流

  • 将文件升级到指定 edition——无论该文件当前处于 syntax 模式(proto2/proto3)还是 editions 模式,升级时要同步更新 features,保证升级结果是语义上的 no-op(不改变行为)。这是最高优先级的工作流;
  • “清理”文件中的 features——运行一个简单算法,确定每一级语法元素上需要保留的最小 feature 集合(即删掉冗余的显式设置);
  • 修改某个具体语法元素上的 features

需求二:动作必须同时支持“精确”与“泛化”两种粒度

  • 精确:面向特定语法元素(Schema 消费者将变更规格与 .proto 文件一起签入时);
  • 泛化:单个或一组变更规格即可驱动大规模变更。

文档最后强调一个边界:文中给出的 Schema 只是推荐,Prototiller 项目所有者可以按实现需要修改;真正具有约束力的只有本文档中的需求,Schema 只是这些需求的图示化表达。

3. 推荐 Schema 详解:ChangeSpec 与三个动作

推荐 Schema 全文如下(proto2 语法,prototiller 包):

syntax = "proto2";

package prototiller;

// This is the proto that Prototiller accepts as input.
message ChangeSpec {
  // Actions to execute on the file.
  repeated Action actions = 1;

  // Some changes may result in a wireformat break; changing field type is
  // usually unsafe. By default, Prototiller does not allow such changes,
  // users can set allow_unsafe_wire_format_changes to true to force the change.
  optional bool allow_unsafe_wire_format_changes = 2 [default = false];
  optional bool allow_unsafe_text_format_changes = 3 [default = false];
  optional bool allow_unsafe_json_format_changes = 4 [default = false];
}

// A single action. See messages below for description of their
// semantics.
message Action {
  oneof kind {
    UpgradeEdition upgrade_edition = 20;
    CleanUpFeatures clean_up_features = 21;
    ModifyFeature modify_feature = 22;
  }
}

// Upgrades the edition of a file to a specified edition.
// Treats syntax mode as being a weird, special edition that cannot be
// upgraded to.
//
// This action is always safe.
message UpgradeEdition {
  // The edition to upgrade to.
  optional string edition = 1;
}

// Cleans up features in a file, such that there are as few explicitly set
// features as necessary.
//
// This action is always safe.
message CleanUpFeatures {}

// Modifies a specific feature on all syntax elements that match and which can
// host that particular feature.
//
// Prototiller must be aware of which changes affect wire format, so that it
// can flag them as unsafe.
message ModifyFeature {
  // The name of the feature to modify.
  repeated proto2.UninterpretedOption.NamePart feature = 1;

  // A pattern for matching paths to syntax elements to modify.
  //
  // Elements of this field can either be identifiers, or the string "*", which
  // matches all identifiers. Thus, ["foo", "Bar"] matches the message foo.Bar,
  // ["foo", "Bar", "*"] matches all fields and nested types of foo.Bar
  // (recursively), and ["*"] matches all elements of a file.
  repeated string path_pattern = 2;

  // The value to set the feature to. If not set, this means that the
  // feature should be deleted.
  oneof value {
    int64 int_value = 20;
    double double_value = 21;
    // ... and so on.
  }
}

3.1 ChangeSpec:入口消息与三个安全开关

ChangeSpec 是 Prototiller 接受的唯一输入,结构非常克制:

  • repeated Action actions:按顺序对目标文件执行的动作列表;
  • 三个 allow_unsafe_* 布尔开关,默认均为 falsewire format(线上二进制兼容性)、text formatJSON format。注释明确给出动机——“某些变更可能导致 wireformat break,修改字段类型通常是不安全的”;默认情况下 Prototiller 拒绝这类变更,用户必须显式打开对应开关才能强制执行。

这一设计把“变更是否破坏兼容性”的判断从动作层面提升到了整个变更规格层面,意味着 Prototiller 自身必须具备知道哪些 feature 变更影响 wire format 的能力(这一点在 ModifyFeature 的注释中被再次强调)。

3.2 Action:oneof 扩展点

Actiononeof kind 承载三种动作,字段号刻意从 20、21、22 开始而非 1、2、3——从字段编号的取舍可以推断,这是为未来追加新的动作类型预留空间,避免占用起始字段号,体现“仅服务 Editions、但可被扩展”的需求定位。

  • upgrade_edition(20):升级文件 edition,始终安全
  • clean_up_features(21):清理冗余 features,始终安全
  • modify_feature(22):修改指定 feature,安全与否取决于具体 feature,由安全开关把关。

3.3 UpgradeEdition:把 syntax 模式视为“特殊的 edition”

UpgradeEdition 只有一个 optional string edition 字段,指定期望升级到的目标 edition。语义上有两个要点:

  1. 无论源文件是 proto2/proto3(syntax 模式)还是某个 editions 文件,都统一处理;
  2. “把 syntax 模式当作一种奇怪的、特殊的、不可升级至的 edition”——即 edition = "proto2" 这类目标是非法的,edition 只能升级到真正的 editions 版本号。

仓库中的 descriptor.proto 定义了 Edition 枚举(EDITION_2023 = 1000,以及测试用的 EDITION_1_TEST_ONLY = 1),edition 字符串字段对应的就是这类取值;Editions 的默认值机制(FeatureOption.edition_defaults)也已落地,这正是“升级到目标 edition 必须同时改写 features 才能保持 no-op”这一需求的底层前提。

3.4 CleanUpFeatures:无参消息,算法内置

CleanUpFeatures 是一个空消息——它没有参数,因为“最小化显式 feature 集合”的算法完全由工具内置:对文件中每一级语法元素,只保留为保持语义所必需的最少显式 feature。这与 editions-tooling.md 中描述的 “features janitor” 是同一件事:先假设性地展开全部 features(含 edition 默认值),再按“多数子节点取值”启发式自下而上折叠,最后删掉被 edition 默认值隐含的显式设置。janitor 的示例效果是把 Foo 消息下五个字段都显式声明 string_type = VIEW 的文件,整理成一条消息级 option features.(pb.cpp).string_type = VIEW;,而只保留 Bar.a 一条显式声明。

3.5 ModifyFeature:feature 名 + 通配路径 + 类型化取值

ModifyFeature 是三者中唯一带参数、也最复杂的动作,三个字段各司其职:

(1)feature:用 repeated proto2.UninterpretedOption.NamePart 表达 feature 名

这里值得展开:为什么不直接用 string,而借用 UninterpretedOption.NamePart?因为 Editions 的 feature 既包括核心 feature(如 field_presence),也包括带扩展标识符的后端 feature,例如 cpp_features.protopb.cpp 包下的 string_type(其中已声明 edition_introduced: EDITION_2023)。NamePart 恰好能同时表达普通名称与 (.extension).subname 形式的扩展名。这也从 Schema 层面印证了 editions 文档中提到的 feature 布局设计

(2)path_pattern:支持 * 通配的元素路径

路径模式由一串字符串组成,每个元素要么是标识符、要么是 "*"(匹配任意标识符)。文档给出了三个精确示例,务必记住:

  • ["foo", "Bar"] 匹配消息 foo.Bar
  • ["foo", "Bar", "*"] 递归匹配 foo.Bar 下的所有字段与嵌套类型;
  • ["*"] 匹配文件中的全部元素。

这正是需求二“既能精确、也能泛化”的直接实现:一份规格里可以同时写“只改 foo.Bar 下某个字段”和“改整个文件”的混合规则。

(3)value:oneof 类型化取值,不设置即“删除”

oneof valueint64 / double / … 枚举出取值类型(注释表明后续会按需追加)。关键语义在注释里:若不设置 value,则意味着删除该 feature——即把显式设置摘掉、回退到上级作用域或 edition 默认值。这样“修改”和“删除”被统一进同一个动作,无需第二个 DeleteFeature 消息。

3.6 安全性模型的实现要点

ModifyFeature 注释中要求“Prototiller 必须知道哪些变更影响 wire format,以便把它们标记为不安全”。对照仓库现状可以印证这一判断是有数据支撑的:feature 的元信息(feature_supportedition_defaults 等,见 descriptor.protounittest_custom_features.proto)让工具可以程序化地查询“某 feature 是什么类型、影响什么”,editions/defaults.bzl 及各 edition2023_*.proto 测试用例(如 edition2023_ctype.proto)则覆盖了各类 feature 的实际行为验证。也就是说,安全开关不是拍脑袋的布尔值,而是有 feature 元数据可查的白名单式判定。

4. 在 protoc 中的落地形态:--change_spec 与工作流编排

配套的 editions-tooling.md 说明了这批需求最终如何变成用户可执行的命令。三个工具(janitor / adopter / updater)都“说 ProtoChangeSpec 这门语言”,并直接内建于 protoc,交互模式为:

  • protoc --change_spec=spec.pb --change_out=foo-changed.proto foo.proto:把规格应用到 .proto 文件,写出结果文件;change_out 可以省略(打印到 stdout)或指向原文件(就地更新);
  • --janitor[=spec.pb] 之类的分析开关:带参数时把生成的规格写到该路径,不带参数时直接就地应用变更。

需求文档中三个动作与工作流文档中三个工具正好一一对应:UpgradeEdition 对应 adopter/updater 的输出,CleanUpFeatures 对应 janitor,ModifyFeature 则支撑 Schema 消费者对导入 schema 的细粒度 feature 管控。

5. Alternatives Considered:两个被否决的简化方案

文档结尾简要记录了两项被否决的替代设计,理解它们有助于把握 Schema 的设计取舍:

  1. 不设独立的 feature 清理动作,把它隐式并入其他动作——被否决。因为希望这个操作可以“激进地”到处运行,甚至可以作为 Cider 等 IDE 的 “format on save” 的一部分;独立动作(CleanUpFeatures 空消息)让它可以被单独、频繁、无副作用地调用。
  2. ModifyFeature 作用于文件的全部语法元素——被否决。ModifyFeature 的目标是让 Schema 消费者能对导入的 .proto 文件做细粒度 feature 控制:用户可能想清除整个文件所有字段上的某 feature,也可能只想改其中几个字段。简单的模式匹配(path_pattern + *)同时满足这两种诉求。

6. 总结与适用边界

回看这份需求文档,它的价值在于用约一百行的 Schema 把 Editions 迁移工具的接口收敛成三个可组合的动作,并配套一套默认拒绝、显式放行的不兼容变更安全模型;UpgradeEdition 保证“升级 edition 必为语义 no-op”这一最高优先级承诺,CleanUpFeatures 保证机械化的文件整洁度,ModifyFeature 则打通了“单文件签入规格”到“大规模变更”的粒度谱系。

适用边界需要向读者说明清楚:

  • 本文档(2022-11-29 批准,作者 mcy)是需求与推荐 Schema,明确声明 Schema 仅为示意、可按实现调整,只有需求本身是约束性的;
  • 该目录下的所有 prototiller 文档均为历史设计文档,不代表仓库当前已交付的工具状态——本仓库中可检索到的 prototiller 痕迹仅存在于 docs/design/prototiller/ 目录及 docs/design/editions/life-of-an-edition.md 中对 ProtoChangeSpec 工作流的引用,并无对应实现源码;
  • 文档中引用的 Protochangifier 系列内部文档“未对外可用”,其细节只能从本文档与同目录的 Editions ToolingEdition Zero 需求 中交叉印证。

对希望理解 Protobuf 如何为 Editions 做迁移工程化的读者,建议按 prototiller README 的三篇文档顺序通读,再结合 Editions 设计文档目录 与仓库中 editions/src/google/protobuf/descriptor.proto 的实现交叉验证。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384