Carbon 语言与工具链版本控制方案(Versioning)完全解析:从 SemVer 规范到 Bazel 构建落地
【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang
导读:本文以 Carbon Language 官方文档《Toolchain and language versioning》(docs/project/versioning.md)为核心骨架,系统讲解 Carbon 如何基于 Semantic Versioning(SemVer 2.0.0)为语言与整个工具链建立统一版本模型——涵盖
MAJOR.MINOR.PATCH各段位的递增触发条件、破坏性变更的判定与排除规则、rc/nightly/dev三类预发布版本的语义与排序机制,并结合仓库中的 Bazel 构建实现(bazel/version/、common/version.h)展示版本字符串从 Starlark 规则计算、模板替换到编译期嵌入的全链路。读者学完后将能准确理解 Carbon 每个版本号片段的含义,掌握用--release、--pre_release、--rc_number、--nightly_date等 Bazel 标志构建指定版本类型的方法,并领会其在 0.x 开发期、1.0 里程碑及未来 LTS/标准化演进中的定位。
概览:单一版本方案(Overview)
Carbon 在语言本身与工具链(包括标准库、编译器、链接器以及主项目发布的所有开发工具)之间采用一套统一的版本号方案,即语言和工具链共享同一个版本号,避免维护多组件间的兼容矩阵。
该方案紧密遵循 Semantic Versioning 2.0.0,其核心要点如下:
- 正式版本号格式:
MAJOR.MINOR.PATCH(如0.0.0)。 - 破坏性(不向后兼容)变更触发
MAJOR递增——在达到1.0里程碑之后;在1.0之前则递增MINOR。这里特意把"可能触发构建告警(build-breaking warning)的弃用(deprecation)"也计入破坏性变更。 - 仅含向后兼容变更的版本:预期不会发生(Carbon 明确预期大部分版本都会包含一定程度的破坏性变更)。
- 仅含缺陷修复的版本递增
PATCH。 - 预发布后缀(见下文专节):
MAJOR.MINOR.PATCH-rc.N:该版本的第 N 个候选发布版(release candidate)。MAJOR.MINOR.PATCH-0.nightly.YYYY.MM.DD:某开发日自动构建的 nightly 增量开发版本。MAJOR.MINOR.PATCH-0.dev:开发者在开发过程中交互式构建的增量开发版本。
该方案的正式定义来源于提案 p004105-establish-toolchain-and-language-versioning.md,提案文档与本文档互为补充,前者还包含方案制定的动机(Abstract/Problem)、未来方向草图与备选方案对比。
主版本(MAJOR)递增规则
与 SemVer 对齐:语言或工具链任何部分的任何破坏性变更,都必须递增主版本号。
从 0 到 1:基于功能完整度的里程碑
从0到1的第一次主版本递增,预期基于"达到预期的功能完整度与质量标准"这一里程碑——即宣告语言进入稳定版本(1.0 稳定版)。在此之前,Carbon 通过0.1、0.2等次版本里程碑(参见 docs/project/milestones.md)逐步逼近 1.0。
1.0 之后:基于时间的发布策略
后续的主版本递增预期采用基于时间的发布策略:已就绪的功能(或破坏性变更)随当次主版本发布,其余内容等待下一次主版本。具体的发布节奏属于未来工作,应在向 1.0 冲刺及达成之后与 Carbon 用户讨论确定。
关键立场:能破坏 ≠ 应当破坏
文档特别强调:即便递增了主版本号、可以做破坏性变更,也不意味着应当做。语言层面的破坏性变更对用户而言代价极其高昂(固有的变更冲击规模)。Carbon 从 C++ 语言、编译器和标准库的更新经验中得出的结论是:真正"零破坏"的发布极其困难且约束过强。因此:
- 预期大多数发布(尤其是语言早期阶段)都会包含一些破坏性变更;
- 项目的工作方向是将这些变更的升级成本降到最低、范围缩到最小;
- 若未来 Carbon 足够稳定,可重新评估是否对部分发布使用次版本递增来标记向后兼容——届时将重新审视版本与发布策略,建立可预期、无意外(predictable and unsurprising)的结构。
破坏性变更的定义(Breaking changes)
除了标准库 API 或工具 API 的传统破坏性变更之外,Carbon 还涵盖语言或工具链层面的破坏性变更,其定义为:
任何导致**正确、可运行、非反射(non-reflective)**的代码变得无效、被拒绝、产生错误结果或行为被静默改变(silently change behavior)的变更。
破坏性变更的排除项(Exclusions)
Carbon 明确将两类变更排除在"破坏性变更"之外:
- 反射式代码(reflective code):即通过某种方式检测 Carbon 版本、或检测某功能存在与否的代码。这类代码可能因检测到"被正确设计为非破坏性"的变更而"坏掉"。项目不希望"新增功能"被算作破坏性变更,因此排除专门检测此类新增的代码。
- 对错误代码的破坏:除非该错误代码尽管有 bug,但仍以某种有用、且通常相当普遍的方式被接受并正常工作,否则对错误代码的破坏不视为破坏性变更。
次版本(MINOR)递增规则
当前阶段:Carbon 计划主要在主版本为0的时期使用次版本递增,以追踪通往 1.0 功能完整里程碑的进度。为此项目定义了0.1、0.2里程碑,并可能按需增加更多步骤。
1.0 之后:在 1.0 之后的世界里,预期大多数重要功能也会伴随一些来自弃用(deprecation)的小型、可控的破坏性变更。Carbon 当前计划不在 1.0 后做次版本发布,而是专注于让这些更新对语言用户既容易又具备可扩展性(easy and scalable)——这一立场未来可能被重新评估。
从 version_base.bzl 可以看到当前仓库正处于开发期的具体体现:version_base = "0.0.0",注释明确说明这是活跃开发版本而非已发布版本,且项目"尚未取得足以升至 0.1 里程碑的进展";同时强调"永远不会为 0.0.0 发布任何非开发性质的预发布版本,仅预期 nightly 开发预发布"。
补丁版本(PATCH)递增规则
补丁版本仅在"变更本质上是对先前已发布版本的缺陷修复"时递增,绝大多数应为严格向后兼容的缺陷修复。
发布时机:需求驱动
补丁发布预期由需求驱动(demand-driven),非必需时可以不发布。具体的日程与流程将在筹备 1.0 里程碑时确定;在 1.0 之前,项目不对补丁发布的流程或节奏做任何承诺,因为 1.0 之前的任何发布都不应被视为稳定。
特殊情形:恢复"预期公共 API"的修复
一个值得注意的细节:恢复某个版本"预期的公共 API(intended public API)"仍被视为缺陷修复。当这类修复在理论上会以通常需要递增主版本的方式破坏新代码时,只要它确实是在恢复该版本预期的行为,就可以仅以补丁版本递增来完成。
但项目对 SemVer 保证非常严肃,因为这类修复仍可能具有破坏性,因此设置了很高的门槛:
- 必须是修复从上一版本引入的回归(regression),而非单纯地补齐缺失功能。回归甚至可以是语言或工具整体凝聚力/可靠性的下降(例如新功能中的 bug 侵蚀了整体可靠性)。关键是任何补丁修复都要以"回归修复与稳定化"的视角来论证,而非"向前修复(fixing forward)"。
- 修复造成的破坏范围被证实很小,通常源于:包含该回归的版本持续时间很短;或可能受影响的代码结构范围很窄。
- 回归的影响很大且难以绕过,例如:损害了大量用户的 Carbon 核心优先级;或使相当规模的用户群体采用该发布的工程成本不经济。
官方示例解析
文档给出了三类典型场景,用于判断"是否值得发布补丁修复":
| 场景 | 分析 | 结论 |
|---|---|---|
| 新功能包含导致**非健全(unsound)**的 bug,允许编译出运行时会崩溃或产生未定义行为(UB)的错误代码 | 虽是新功能而非既有功能缺陷,但这是对语言整体可靠性及用户推理程序正确性能力的严重回归 | 除非发现太晚(发布数月后才被发现)且唯一修复方式会对后续编写的代码产生同样糟糕的影响,否则是补丁发布的好候选;若 bug 导致安全漏洞,即便有用户冲击也可能值得修复 |
| 使用面很窄的新功能存在 bug,使本应可用的代码模式在编译期被拒绝或稳定崩溃 | 使用场景狭窄,为此做侵入式修复得不偿失 | 更好的候选是:在使用可能触发崩溃的方式时引入告警或错误,甚至对使用该功能本身发出告警 |
| 编译器某个默认关闭的 opt-in(通过 flag 开启)功能运行不可靠 | 功能未默认开启,破坏范围可控 | 好候选:禁用该 flag 或触发告警信息;不主张"向前修复"该功能 |
| 编译器某个默认开启的 opt-out(通过 flag 关闭)功能运行不可靠 | 取决于受影响用户规模 | 受影响用户极少:可能只需文档说明如何关闭;影响面大到成为普遍困扰和体验回归:值得发布精准修复或缓解的补丁版本 |
预发布版本(Pre-release versions)
SemVer 虽然为预发布版本提供了基础语法,但对语义几乎是"完全开放"的。Carbon 选择建立小而清晰、语义明确的预发布版本方案,以便准确传达这些版本"是什么、不是什么、应如何解读",同时保证这些预发布版本在 SemVer 规则下可以连贯地排序(order in a reasonably cohesive way)。
预发布版本优先级从高到低依次为:
MAJOR.MINOR.PATCH-rc.N(发布候选)MAJOR.MINOR.PATCH-0.nightly.YYYY.MM.DD(夜间自动构建)MAJOR.MINOR.PATCH-0.dev(开发构建)
发布候选:MAJOR.MINOR.PATCH-rc.N
当项目认为某个版本已完整、可随时发布,并希望收集反馈时,会创建rc预发布版本。要求:
- 与预期发布的版本之间不应存在有意义或重大的差距(即使是已知差距),预期通常是:只要没有相反反馈,该发布候选直接成为正式发布版本。
- 每一类预发布版本都以顺序计数 N结尾;必须以
.0作为起点(如-rc.0),这样同一版本的后续迭代才能在排序时排到第一个之后(.0确保-rc.0 < -rc.1 < ...)。
在 Bazel 构建中,RC 版本通过两个标志组合生成:--pre_release=rc与--rc_number=N,对应 bazel/version/BUILD 中定义的rc_number整型标志(默认-1,非负时才合法)。其拼接逻辑在 compute_version.bzl 的compute_version()中:当rc类型且number >= 0时,追加-rc.N;若number < 0则直接fail报错。
增量开发版本:MAJOR.MINOR.PATCH-0.{nightly,dev}.N
这类版本与"实际预期发布"没有任何关联,只是开发进度的增量追踪产物。它们不受任何完整度或就绪度标准约束,实践上会出现在一次发布与下一次发布之间的每个阶段。
之所以在nightly/dev前加上0组件(形成-0.nightly...、-0.dev),是为了保证这些版本在 SemVer 排序上严格排在所有发布候选(-rc.N)之前——因为预发布标识符按点分段的数字比较,0小于任何正整数N,从而-0.dev < -0.nightly.2024.06.17 < -rc.0 < 正式版。
Nightly 预发布版本:MAJOR.MINOR.PATCH-0.nightly.YYYY.MM.DD
- 构建方式:自动化构建,每天夜间在自动化测试通过时构建。不对代码树中的内容做任何额外的质量门槛或精炼——自动化测试通过即构建。
- 核心用途:不是用于评估潜在发布的,而是为 Carbon 自身的开发者和贡献者追踪增量开发进度。这一开发导向目标决定了它们的构建方式与提供的内容。
- 排序机制:用
0前缀确保排在所有发布候选之前;再用日期派生的后缀YYYY.MM.DD提供同年份 nightly 构建的大致时间排序。 - 源码印证:
compute_version()中 nightly 分支调用_validate_nightly_date()严格校验日期格式必须为YYYY.MM.DD(年 4 位数字、月日各 2 位数字,compute_version.bzl 第 10-22 行),随后拼接-0.nightly.YYYY.MM.DD。
Development 预发布版本:MAJOR.MINOR.PATCH-0.dev
- 构建方式:开发过程中对 Carbon 的交互式构建(interactive builds),是开发活动的产物,永远不会进入任何正式发布流程。
- 特性:不被期望是自动化或可复现的,可能包含**进行中的编辑(in-flight edits)**及各种其他变动。
- 排序机制:与 nightly 相同,以
0前缀保证有效排序;但不再附加任何额外信息(例如不携带构建日期)——这是为了保持开发构建机制的简单性,并最小化对构建缓存的冲击(cache impact)。构建的确切时间戳可能存在,但不参与版本号。
仓库中的落地实现:从 Starlark 规则到编译期嵌入
版本方案不仅是文档层面的约定,在仓库中已有完整实现链路。理解这条链路有助于把版本号格式与真实构建产物对应起来。
版本基线与构建标志
- 基线版本定义在根目录 version_base.bzl:
version_base = "0.0.0",注释强调这是"活跃开发版本",且 0.0.0 仅预期有 nightly 开发预发布。 - 版本构建标志定义在 bazel/version/BUILD:
| Bazel 标志 | 类型 | 默认值 | 取值/说明 |
|---|---|---|---|
--release | bool | False | 启用正式发布版本;启用时必须是唯一使用的版本标志 |
--pre_release=KIND | string | dev | rc/nightly/dev三选一;不能与--release同用 |
--rc_number=N | int | -1 | RC 编号,需--pre_release=rc |
--nightly_date=YYYY.MM.DD | string | 空 | nightly 日期,需--pre_release=nightly,格式严格为YYYY.MM.DD |
- 命令行别名:这些标志可通过根目录 .bazelrc 中定义的别名直接使用:
--release、--pre_release、--rc_number、--nightly_date(对应 Bazel 内部 label//bazel/version:release等),构建命令形如:
# 构建正式发布版本(RC 之后的正式版) bazel build --release //... # 构建第 2 号发布候选 bazel build --pre_release=rc --rc_number=2 //... # 构建 2024 年 6 月 17 日的 nightly 版本 bazel build --pre_release=nightly --nightly_date=2024.06.17 //... # 构建默认的开发版本(--pre_release=dev 为默认值) bazel build //...版本字符串的计算逻辑
核心计算逻辑位于 bazel/version/compute_version.bzl 的compute_version(ctx)函数:
- 以
version_base(0.0.0)为起点; - 若
--release未开启,则按--pre_release值追加后缀:rc:要求rc_number >= 0,拼接-rc.N;nightly:校验日期格式后拼接-0.nightly.YYYY.MM.DD;dev:拼接-0.dev;- 其它值直接
fail(非法标志);
- 若
--release开启,则保持纯MAJOR.MINOR.PATCH形式。
该函数与expand_version_build_info规则(bazel/version/rules.bzl)配合:规则读取 Bazel 的stable-status.txt/volatile-status.txt(由--workspace_status_command=./scripts/workspace_status.py提供,见 .bazelrc 第 12 行),并受--stamp控制(默认--nostamp以降低对构建缓存的冲击)。模板中的$VERSION、$GIT_COMMIT_SHA、$GIT_DIRTY_SUFFIX等键会被替换;gen_tmpl.py(bazel/version/gen_tmpl.py)实现了这一 Pythonstring.Template风格的替换工具。
编译期嵌入:Version 结构体
版本信息最终嵌入工具链二进制。头文件 common/version.h 声明Carbon::Version结构体,提供Major、Minor、Patch三个整型组件、String(完整版本串)与ToolchainInfo(命令行渲染用版本信息串)。模板文件 common/version.tmpl.cpp 与 common/version_stamp.tmpl.cpp 在编译期将$VERSION、$GIT_COMMIT_SHA$GIT_DIRTY_SUFFIX等替换为实际值,其中:
Major/Minor/Patch通过consteval的ToInt/MajorVersion/MinorVersion/PatchVersion从$VERSION字符串中解析(version.tmpl.cpp 第 14-49 行),且不依赖 stamping,避免缓存问题;- 带 stamp 的版本串格式为
$VERSION+$GIT_COMMIT_SHA$GIT_DIRTY_SUFFIX(如0.0.0+abc1234),ToolchainInfo则渲染为Carbon Language toolchain version: $VERSION+$GIT_COMMIT_SHA$GIT_DIRTY_SUFFIX,可用--stamp显式开启后由carbon命令行的版本输出展示。
版本号的排序语义速查
把上述规则汇总,可得一份可直接用于解读任何 Carbon 版本号(并可用 SemVer 比较器排序)的速查表:
| 版本号示例 | 含义 | 排序关系 |
|---|---|---|
0.0.0-0.dev | 开发者交互式构建(默认) | 最低 |
0.0.0-0.nightly.2024.06.17 | 某日夜间自动化构建 | 高于 dev,按日期递增 |
0.1.0-rc.0 | 0.1 的第 0 个发布候选 | 高于所有-0.*开发版本 |
0.1.0-rc.1 | 0.1 的第 1 个发布候选 | 高于rc.0 |
0.1.0 | 0.1 正式发布 | 最高 |
未来方向:提案中的长期规划
版本方案的长远演进在提案 p004105-establish-toolchain-and-language-versioning.md 的 "Directional sketch for the future" 一节中给出方向性指引(注意:这些只是方向草图,未来需要各自的正式提案):
- 语言演化与破坏性变更:单一版本号长期看不足以支撑语言演化需求。项目计划至少将主版本映射为Rust Edition 式的源码内控制机制,让同一工具链能以不同主版本语义编译同一代码库的不同部分,实现破坏性变更的增量采纳(incremental adoption),使"破坏性变更的推出"与"工具链升级"解耦。
- LTS 版本与标准化:SemVer 只解决"检测"问题,不解决"长期稳定"问题。方向是设立基于完整度、质量与用户需求的LTS 版本,支持窗口可能长达数十年(比 Linux 发行版 LTS 更长);LTS 频率应随窗口延长而降低,避免支撑过多版本。标准化则被视为"把某个 LTS 提升为标准"的类比过程。
结语
Carbon 的版本控制方案本质上是用最小的预发布语法集(rc、0.nightly、0.dev)把 SemVer 的开放语义收敛为可预期、可排序、可沟通的项目契约:在 0.x 阶段以次版本里程碑推进语言演进,以夜间与开发版本支撑高频迭代,以rc兜底发布质量;在 1.0 之后以主版本承载必要的破坏性变更并力求升级成本最小化。而 bazel/version/ 与 common/version.h 的完整实现表明,这一方案并非纸面设计,而是已可操作的构建系统能力——理解版本号的含义,即理解 Carbon 语言与工具链当前所处的演化阶段。
【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考