Dart SDK pkg/ 包发布校验机制全解析:pubspec 验证工具与 CI 实践指南
【免费下载链接】sdkThe Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.项目地址: https://gitcode.com/gh_mirrors/sdk1/sdk
导读
本文以 Dart SDK 仓库中pkg/目录的包校验机制为切入点,系统讲解 SDK 内部对pkg/下数百个 Dart 包的自动验证流程。你将掌握dart tools/package_deps/bin/package_deps.dart这一核心校验工具的运行方式、publish_to: none标记的语义、发布包与未发布包的差异化校验规则,以及新增包和测试包的标准操作流程。文中所有结论均对应 pkg/README.md 文档及 package_deps.dart 源码实现。
背景:为什么 SDK 仓库需要对 pkg/ 包做专项校验
Dart SDK 采用单仓库(monorepo)模式,pkg/目录下汇集了analyzer、analysis_server、compiler、dart2js_info、async_helper等大量 Dart 包,它们共同支撑着 SDK 的构建、分析与测试体系。
这些包与普通 pub 生态包存在一个关键差异:开发期间它们并不通过 pub 工具解析依赖,而是从仓库根目录的 DEPS 文件中获取依赖版本。文档明确指出:"It's very easy for the dependencies specified in a package's pubspec file to get out of date wrt the packages and versions actually used."(pubspec 中声明的依赖很容易与实际使用的包和版本脱节)。
正因如此,SDK 在 LUCI CI 机器人上对所有pkg/包执行自动校验,核心校验逻辑由tools/package_deps包承担。这也是 "Package validation" 一节的立意所在。
本地运行校验工具
基本命令
文档给出的校验命令非常直接,在仓库根目录执行:
dart tools/package_deps/bin/package_deps.dart从 入口文件 的源码可以看到,工具启动时首先做两项前置检查:
- 当前目录下必须存在
DEPS文件且存在pkg目录,否则输出Please run this tool from the root of the Dart repo.并以退出码 1 结束; - 支持
-v/--verbose参数(第 16 行),开启后会在校验时打印每个依赖解析到的具体版本。
工具执行流程
工具会扫描pkg/下所有包含pubspec.yaml的子目录(通过_hasPubspec判断,见 第 85-86 行),逐个构建Package对象并调用validate()完成校验。校验结束后若有任一包失败,进程退出码置为 1(第 80-82 行),方便 CI 直接依据退出码判定构建是否通过。
依赖来源解析
SdkDeps类(第 422-503 行)负责解析 SDK 内部真实可用的依赖版本:
- 从 DEPS 文件中以正则
"/third_party/pkg/(\S+)"提取第三方包清单; - 递归扫描
pkg/、third_party/devtools、third_party/pkg三个目录下的pubspec.yaml,读取每个包的name与version字段,建立"包名 → 版本"映射表。
这意味着校验的基准并非 pub.dev 上的最新版本,而是仓库当前实际检入的依赖版本,从而保证声明与实现严格一致。
依赖声明与实际使用的双向一致性校验
这是校验工具的核心逻辑,实现在 Package._validatePubspecDeps。工具通过_parseImports(第 152-180 行)扫描包内全部.dart文件,用正则提取import/export语句中的package:URI 前缀,得到"实际使用"的依赖集合;再与 pubspec 中dependencies:、dev_dependencies:声明的集合做比对,主要检查四类问题:
- 使用了但未声明:
lib/中引用的包未列入dependencies:,或测试/工具目录中引用的包未列入dev_dependencies:; - 声明了但未使用:
dependencies:或dev_dependencies:中声明的包在源码中从未被导入; - 依赖放错位置:只在开发目录中使用的包被错误声明到了
dependencies:(misplacedDeps检查,第 247-253 行); - 包名与目录名不一致:pubspec 的
name字段必须与所在目录名相同(第 185-188 行)。
需要特别说明的是,源码对dev_dependencies的"未使用"检查有白名单例外(第 227-238 行):lints、dart_flutter_team_lints(用于引入analysis_options配置)、build_runner与build_web_compilers(webdev 项目必需)即使未被直接 import 也不会报错。
此外,扫描器对// @skip_package_deps_validation注释、以及以class、typedef、mixin、enum、extension、void、Future、final、const开头的行会停止解析(第 375-386 行),这是为了避免误解析方法体或字符串中的伪 import 语句。
publish_to: none:未发布包的标记方式
文档将pkg/下的包分为两类,判定依据是 pubspec 中是否含有以下标记:
# This package is not intended for consumption on pub.dev. DO NOT publish. publish_to: none未发布(不面向 pub.dev)的包必须在 pubspec 中写入上述注释与publish_to: none字段。这类包在仓库中大量存在,例如 pkg/async_helper/pubspec.yaml、pkg/dart2js_info/pubspec.yaml、pkg/_js_interop_checks/pubspec.yaml,以及校验工具自身的 tools/package_deps/pubspec.yaml 都采用了这一标记。工具的Package构造函数会读取publish_to字段(第 108 行),publishablegetter(第 139 行)据此区分两类包。
文档还强调:未发布包的 pubspec 虽然同样会被校验工具检查,但"contents are more informational",即其结果更多是信息性参考,因为这类 pubspec 不会被 pub 工具或生态消费。
未发布包的校验规则
对publish_to: none的包,除了前述双向一致性检查外,还要求:
- 对 pkg/ 内部包的引用必须使用相对路径依赖(relative path dependency),而非
any或版本约束。
发布包的额外校验规则
对准备发布到 pub 的包,校验更严格(第 255-284 行):
- 普通
dependencies必须使用语义化版本约束(semver constraint),不能写any——例外是 SDK 内置的 vendored 包(SdkPubDep); dev_dependencies则推荐使用any,因为开发依赖在开发期固定即可;- 若依赖的包在仓库中解析不到版本(
resolvedVersion == null),发布包必须报错——只有未发布包才允许依赖无版本声明的包(源码注释以pkg/dartdev依赖package:pub为例)。
版本范围与实际检入版本的一致性检查
工具还会将 pubspec 声明的 semver 范围与 DEPS 解析出的实际版本比对(第 288-324 行):若声明范围不包含仓库实际版本,例如声明^1.0.0而仓库检入的是0.9.x,校验即失败并输出类似PackageX depends on package:foo with a range of ^1.0.0, but the version of foo in the repo is 0.9.5的错误信息。
未发布包的相对路径依赖要求
对于publish_to: none的包,文档列出其必须满足的三条校验:
- pubspec 中声明的依赖都在包中被实际使用;
- 源码用到的所有包都已在 pubspec 中声明;
- 对 pkg/ 包的引用必须通过相对路径依赖。
这一约束与发布包"必须用 semver"形成对照:未发布包内部互相依赖时使用path:相对路径(如dependencies: { expect: { path: ../expect } }),既避免误发布,也确保开发期间直接使用工作区源码。注意校验工具会将路径依赖解析为PathPubDep(第 556-563 行),从而在规则分支中正确放行。
新增包的标准流程
文档对新增包给出了明确指引:添加新包后必须运行gclient sync,以重新生成.dart_tool/package_config.json。这是因为新包的加入会改变仓库的包解析结果,只有重新执行 gclient sync 才能让 IDE、分析器与构建工具感知到新包的存在。
同时建议在新增包时遵循本仓库的既有惯例:
- 面向 pub.dev 的包:声明明确的 semver 版本约束;
- 仅内部使用的包:写入
publish_to: none注释块,并使用any/ 相对路径依赖; - 保证目录名与 pubspec
name一致。
测试包的标准方式
文档特别指出一个容易踩坑的点:目前无法用dart test直接运行 pkg/ 下包的测试,必须沿用仓库统一的test.py测试入口。例如:
./tools/test.py -n...<path>该命令会运行所有以<path>为前缀的测试。test.py位于仓库根目录的 tools/test.py,它负责按测试矩阵调度各目录的测试任务,这也解释了为何包内测试文件(如 pkg/analysis_server/test/ 下的用例)统一由仓库级测试框架驱动,而不是各自独立的dart test。
CI 集成与验证闭环
整个校验机制在 LUCI CI 机器人上自动运行,构成"本地可复现 → CI 强制执行"的闭环:
- 开发者在本地运行
dart tools/package_deps/bin/package_deps.dart即可复现 CI 的大部分检查(包括依赖双向一致性、版本范围匹配、发布/未发布规则); - CI 对每个 pkg 包执行同样的校验,任一失败即终止构建(退出码 1);
- 依赖版本的唯一事实来源是 DEPS 文件,配合
tools/manage_deps.dart做版本滚动更新(见 DEPS 文件头部注释)。
常见校验失败示例与排查思路
结合 package_deps.dart 的输出文案,整理常见失败场景供排查参考:
| 失败场景 | 典型输出 | 修复方向 |
|---|---|---|
| 目录名与包名不一致 | Package name is different from the directory name. | 统一 pubspecname与目录名 |
| lib/ 用到未声明的包 | package:foo used in lib/ but not declared in 'dependencies:'. | 将 foo 加入dependencies: |
| 声明未使用的依赖 | package:bar declared in 'dependencies:' but not used in lib/. | 从 pubspec 移除 bar |
| 开发依赖放错位置 | package:baz declared in 'dependencies:' but only used in dev dirs. | 移到dev_dependencies: |
发布包用any约束 | Published packages should use semver deps: | 改为^x.y.z |
| 版本范围不含仓库版本 | ...range of ^1.0.0, but the version of foo in the repo is 0.9.5 | 对齐 DEPS 中的实际版本 |
结语
Dart SDK 对pkg/包的自动校验机制,本质上是用一套可本地复现、CI 强制的静态检查,守住"pubspec 声明"与"DEPS 事实"之间的平衡。无论是发布包还是内部包,package_deps工具都能在早期发现依赖漂移、误声明和版本错配。理解publish_to: none语义、双向依赖校验逻辑与gclient sync/test.py的配套流程,是每个向 SDK 仓库贡献或维护pkg/包的开发者必备的实战技能。
【免费下载链接】sdkThe Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.项目地址: https://gitcode.com/gh_mirrors/sdk1/sdk
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考