Flutter 基础体系 · 第 31/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。

Dart Pub 与包管理:约束、锁文件、Workspace、发布和供应链

Dart 的包管理工具是 Pub。Flutter 项目通常通过 flutter pub ... 使用 Pub,纯 Dart 项目则使用 dart pub ...。两者共享 Dart Pub 的依赖解析模型;区别主要在于命令所使用的 SDK 上下文,以及 Flutter 项目可以依赖 flutter SDK 和 Flutter 插件。

一个包管理过程并不只是“下载几个目录”:

pubspec.yaml
    │
    ├── 直接依赖、SDK 约束、依赖来源
    │
    ▼
Pub solver 解析所有传递依赖
    │
    ├── 版本选择
    ├── SDK 兼容性检查
    └── 冲突回溯
    │
    ▼
pubspec.lock
    │
    ▼
缓存、源码包、Flutter/Dart 编译器、平台构建系统

其中:

  • pubspec.yaml 描述允许什么
  • Pub solver 决定这一次实际选择什么
  • pubspec.lock 记录这一次选择的结果
  • 包缓存保存下载或检出的内容
  • Android、iOS、桌面和 Web 的原生构建工具还会继续处理插件的平台代码。

因此,修改一个版本约束、删除锁文件、使用 Workspace 或发布一个包,都会改变依赖图及其可复现性。


1. Pub 管理的对象:包、依赖图和来源

1.1 Dart 包不等于 Flutter 插件

Dart 包是一个带有 pubspec.yaml 的目录。最小示例:

name: greeting
description: A small greeting package.
version: 1.0.0
environment:
  sdk: ^3.3.0

dependencies:
  intl: ^0.19.0

dev_dependencies:
  test: ^1.25.0

这里的 greeting 是一个 Dart 包。它可以被:

  • 另一个 Dart 包依赖;
  • Flutter 应用依赖;
  • Flutter 插件作为纯 Dart 层使用。

Flutter 插件通常还包含原生目录,例如:

my_plugin/
├── lib/
├── android/
├── ios/
├── macos/
├── pubspec.yaml
└── example/

Pub 负责解析和取得这个包,但不会替代 Android Gradle、Xcode、CocoaPods、Swift Package Manager 或 Web 的 JavaScript 构建工具。一个包能够通过 Pub 解析,只说明 Dart 层依赖图成立,并不保证所有原生平台都能编译。

1.2 pubspec.yaml 是声明,不是最终安装清单

应用的 pubspec.yaml 可以写:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0
  my_shared:
    path: ../my_shared

这表示:

  • flutter 来自 Flutter SDK;
  • http 使用某个符合 ^1.2.0 的版本;
  • my_shared 使用本地相对路径,而不是 pub.dev 上的版本。

它没有直接写出 http 的传递依赖,也没有指定最终确切版本。最终版本由 Pub solver 结合所有约束后决定,并写入锁文件。

1.3 依赖来源

常见来源有四种。

Hosted 依赖

dependencies:
  http: ^1.2.0

默认来源是 pub.dev,也可以配置其他兼容 Pub 协议的 hosted server:

dependencies:
  internal_models:
    hosted: https://pub.example.com
    version: ^2.0.0

Hosted 包通常由版本号标识,并在锁文件中记录来源和内容摘要。

Git 依赖

dependencies:
  my_shared:
    git:
      url: https://github.com/example/my_shared.git
      ref: 8e4b9c2

Git 依赖的 ref 可以是分支、标签或提交。分支会继续移动,因此生产环境更适合使用不可变的提交哈希。Git 仓库还可以包含多个包:

dependencies:
  my_shared:
    git:
      url: https://github.com/example/monorepo.git
      path: packages/my_shared
      ref: 8e4b9c2

Path 依赖

dependencies:
  my_shared:
    path: ../my_shared

Path 依赖适合本地联调和同一代码库内的开发。它不是发布依赖的可靠替代:发布到 pub.dev 时,消费者无法访问发布者机器上的 ../my_shared

SDK 依赖

dependencies:
  flutter:
    sdk: flutter

或:

dependencies:
  cupertino_icons: ^1.0.8

environment:
  sdk: ^3.3.0
  flutter: ">=3.19.0"

flutter SDK 依赖只在 Flutter 环境中成立。纯 Dart 项目不能把 Flutter 包当作普通 hosted 包使用。


2. 版本约束:Pub solver 究竟在求解什么

2.1 约束是集合,不是建议文本

假设包 app 声明:

dependencies:
  logging: ^1.2.0

在 Dart 的版本约束语义中,^1.2.0 表示:

1.2.0v<2.0.01.2.0 \leq v < 2.0.0

其中 vv 是最终选择的 logging 版本。

如果另一个依赖要求:

logging: ">=1.5.0 <1.8.0"

那么两个约束的交集是:

[1.2.0,2.0.0)[1.5.0,1.8.0)=[1.5.0,1.8.0)[1.2.0, 2.0.0) \cap [1.5.0, 1.8.0) = [1.5.0, 1.8.0)

所以 logging 1.7.2 可能成立,而 logging 1.8.0 不成立。

Pub 解析的是所有依赖关系的交集,而不是逐行执行 YAML。

2.2 常见约束写法

dependencies:
  a: ^1.2.3
  b: ">=2.0.0 <3.0.0"
  c: ">=1.0.0"
  d: any

它们分别表示:

  • ^1.2.3:兼容版本范围;
  • >=2.0.0 <3.0.0:显式范围;
  • >=1.0.0:没有设置上界;
  • any:允许仓库中满足其他条件的任意稳定版本。

对于 caret 约束,主要规则是:

^1.2.3  => >=1.2.3 <2.0.0
^0.2.3  => >=0.2.3 <0.3.0
^0.0.3  => >=0.0.3 <0.0.4

原因是 SemVer 对 0.x 版本的兼容承诺更窄:0.2.x0.3.0 通常已经可能包含破坏性变化。

any 不是“永远使用最新版”。它只是放宽了当前包的直接约束,最终仍受其他包约束、SDK 约束、预发布版本规则和锁文件影响。

2.3 SDK 约束也是依赖解析条件

推荐在每个包中声明 SDK 下界:

environment:
  sdk: ^3.3.0

它的含义是:

3.3.0DartSDK<4.0.03.3.0 \leq DartSDK < 4.0.0

如果一个包依赖:

environment:
  sdk: ">=3.4.0 <4.0.0"

而当前使用的 Dart SDK 是 3.3.4,即使所有第三方包版本都能匹配,dart pub get 仍会失败,因为根条件不满足。

Flutter 项目还可以声明 Flutter 下界:

environment:
  sdk: ^3.3.0
  flutter: ">=3.19.0"

Flutter SDK 自带对应的 Dart SDK。升级 Flutter 可能同时改变 Dart SDK 版本,进而改变某些包是否可解析。

2.4 一个完整的冲突推导

假设依赖图如下:

app
├── package_a ^1.0.0
└── package_b ^2.0.0

package_a 1.4.0 ──> common ^1.0.0
package_b 2.1.0 ──> common ^2.0.0

求解 common 时需要满足:

common[1.0.0,2.0.0)common \in [1.0.0, 2.0.0)

以及:

common[2.0.0,3.0.0)common \in [2.0.0, 3.0.0)

交集为:

[1.0.0,2.0.0)[2.0.0,3.0.0)=[1.0.0, 2.0.0) \cap [2.0.0, 3.0.0) = \varnothing

因此失败不是因为“Pub 没有下载到最新版”,而是因为当前依赖图没有任何满足全部条件的 common 版本。

典型错误会指出类似:

Because package_a >=1.4.0 depends on common ^1.0.0
and package_b >=2.1.0 depends on common ^2.0.0,
package_a ... is incompatible with package_b ...
So, because app depends on both package_a and package_b, version solving failed.

正确的诊断顺序是:

  1. 看冲突中最底层的公共包;
  2. 确认是哪两个直接或传递依赖施加了不相交的范围;
  3. 使用兼容的新版本;
  4. 降低其中一个顶层依赖;
  5. 必要时升级或修改包,使其放宽或迁移约束。

直接添加 dependency_overrides 只能改变本地求解结果,不能证明被强制的版本真的兼容。


3. Pub 的求解、获取和升级命令

3.1 pub get:按当前约束和锁文件取得依赖

Flutter 项目执行:

flutter pub get

纯 Dart 项目执行:

dart pub get

大致过程是:

  1. 读取当前目录的 pubspec.yaml
  2. 读取已有 pubspec.lock
  3. 检查锁文件中的版本是否仍满足当前声明;
  4. 对新增、删除或不再满足约束的部分重新求解;
  5. 下载或复用缓存中的包;
  6. 更新 .dart_tool/package_config.json 等本地解析文件;
  7. 更新 pubspec.lock

因此,修改 pubspec.yaml 后,通常应运行 pub get,而不是手工编辑锁文件。

3.2 pub upgrade:主动重新选择较新版本

flutter pub upgrade

它会在当前约束允许的范围内重新求解,通常用于升级依赖。即使 pubspec.yaml 没有变化,upgrade 也可能修改 pubspec.lock

例如:

dependencies:
  http: ^1.2.0

若当前锁定 http 1.2.0,仓库中后来出现 http 1.2.1,执行 pub get 可能继续使用锁定版本;执行 pub upgrade 才会主动尝试选择 1.2.1

upgrade 不是无条件升级。若约束是:

http: ">=1.2.0 <1.3.0"

它不会选择 1.3.0。若希望升级到新主版本,应先审查 API 和迁移说明,再修改约束。

3.3 pub downgrade:验证约束的下界

flutter pub downgrade

它尝试在当前约束允许的范围内选择较低版本,适合验证包是否真的支持自己声明的下界。

如果包声明:

environment:
  sdk: ">=3.3.0 <4.0.0"

但源码实际使用了 Dart 3.5 才提供的语言能力,那么普通开发环境可能测试不出问题,pub downgrade 或在最低支持 SDK 上运行测试时才可能暴露错误。

3.4 查看过时依赖

flutter pub outdated

它通常会区分:

  • 当前解析版本;
  • 在现有约束下可升级到的版本;
  • 修改约束后可能达到的最新版本;
  • 被其他依赖限制的版本。

“有更新版本”不等于“应该立即升级”。需要同时检查 API 变更、SDK 要求、平台插件变更和锁文件差异。


4. pubspec.lock:锁定解析结果,而不是替代约束

4.1 锁文件记录什么

简化的 pubspec.lock 类似:

packages:
  http:
    dependency: "direct main"
    description:
      name: http
      sha256: "..."
      url: "https://pub.dev"
    source: hosted
    version: "1.2.2"

  meta:
    dependency: transitive
    description:
      name: meta
      sha256: "..."
      url: "https://pub.dev"
    source: hosted
    version: "1.15.0"

sdks:
  dart: ">=3.3.0 <4.0.0"

字段的重点是:

  • dependency:直接依赖还是传递依赖;
  • source:hosted、git、path、sdk 等来源;
  • version:实际解析版本;
  • description:来源的附加信息;
  • sdks:本次解析所需的 SDK 范围。

具体字段会随 Dart/Flutter 版本演进,不能把锁文件格式当作稳定 API。应让 Pub 生成它,而不是依赖内部字段编写脆弱脚本。

4.2 为什么应用应提交锁文件

对 Flutter 应用和可部署的 Dart 命令行程序,通常应将 pubspec.lock 提交到版本控制系统:

pubspec.yaml
pubspec.lock

原因是应用需要复现一次已经测试过的依赖图。否则两次 CI 构建可能都满足同一份 pubspec.yaml,但因仓库中新发布了兼容范围内的新版本而得到不同结果。

例如:

第一次构建:
http 1.2.0
meta 1.15.0

后来重新构建:
http 1.2.2
meta 1.16.0

这两组版本都可能符合约束,但它们的行为、编译结果或平台实现可能不同。

4.3 库是否提交锁文件

可复用库通常不提交 pubspec.lock,尤其是要发布到 pub.dev 的库。库的消费者需要将该库与自己的应用依赖图共同求解;库作者本地锁定的传递依赖不应强行成为所有消费者的锁定结果。

常见约定是:

  • 应用、服务、CLI:提交 pubspec.lock
  • 可发布库:通常不提交 pubspec.lock
  • Workspace:由根项目统一管理共享解析结果,根锁文件是否提交取决于它是否同时承担应用或可复现仓库的角色。

这不是 Pub 的绝对语法强制,而是不同项目角色的可复现性策略。

4.4 删除锁文件的后果

rm pubspec.lock
flutter pub get

删除锁文件会触发更大范围的重新求解,可能升级大量传递依赖。它有时能解决“旧锁文件无法在新约束下使用”的问题,但会放大变更面。

更稳妥的操作是:

  1. 先复制或提交当前锁文件;
  2. 修改直接依赖约束;
  3. 执行 flutter pub upgrade
  4. 检查 pubspec.lock 的差异;
  5. 运行测试和各平台构建;
  6. 若失败,恢复锁文件并缩小升级范围。

在 CI 中可以使用:

flutter pub get --enforce-lockfile

该选项要求锁文件与 pubspec.yaml 和当前 SDK 条件一致;不一致时失败,而不是悄悄重新解析。它适合把“依赖图发生变化”变成显式构建错误。不同 Flutter/Dart 版本对命令选项的支持可能不同,应先用 flutter pub help get 验证。


5. dependency_overrides:局部强制,不是兼容性证明

开发时可能需要测试本地修改:

dependency_overrides:
  common:
    path: ../common

或者临时验证某个 Git 提交:

dependency_overrides:
  common:
    git:
      url: https://github.com/example/common.git
      ref: 8e4b9c2

它的语义是:在当前根包的解析中,覆盖其他依赖对 common 的普通约束。于是即使某个包声明 common ^1.0.0,本地也可能强行使用一个不符合该范围的路径版本。

这会带来三个重要边界:

  1. 它不修改被依赖包的源码约束;
  2. 它不保证 API、ABI 或运行时行为兼容;
  3. 消费者不会自动继承你本地根项目的覆盖策略。

因此,dependency_overrides 适合本地联调、临时 bisect 和等待上游发布修复,不适合把未经验证的强制版本作为库的兼容性承诺。发布前应检查:

flutter pub publish --dry-run

并确认发布包不会依赖发布者本地路径或私有 Git 地址。


6. Dart Pub Workspace:多个包共享一次解析

6.1 Workspace 解决什么问题

大型仓库常有以下结构:

repo/
├── pubspec.yaml
├── packages/
│   ├── data/
│   │   └── pubspec.yaml
│   └── ui/
│       └── pubspec.yaml
└── apps/
    └── demo/
        └── pubspec.yaml

没有 Workspace 时,每个包都有自己的依赖解析和锁文件。这样会出现:

  • data 使用 collection 1.18.x
  • ui 使用 collection 1.19.x
  • 本地包之间通过 path 依赖连接;
  • 修改共享依赖后需要分别执行命令。

Dart Pub Workspace 让一组包共享一个依赖解析结果,使同一仓库中的包在一次求解中协调版本。

6.2 基本配置

Workspace 根目录的 pubspec.yaml 可以声明:

name: example_workspace
publish_to: none

environment:
  sdk: ^3.6.0

workspace:
  - packages/data
  - packages/ui
  - apps/demo

成员包声明:

name: data
description: Shared data layer.
version: 0.1.0

environment:
  sdk: ^3.6.0

resolution: workspace

dependencies:
  collection: ^1.18.0

另一个成员包:

name: ui
description: Shared UI components.
version: 0.1.0

environment:
  sdk: ^3.6.0

resolution: workspace

dependencies:
  data: ^0.1.0

这里的关键点是:

  • 根包通过 workspace 列出成员路径;
  • 成员包通过 resolution: workspace 表明使用 Workspace 解析;
  • 根包和成员包的 SDK 要求必须兼容;
  • Workspace 的语法和最低 SDK 要求属于版本敏感能力,应使用与当前稳定 Dart SDK 匹配的文档和 dart pub help 验证。

Workspace 配置不是把多个目录简单拼接成一个包。每个成员仍然有自己的:

  • 包名;
  • 版本;
  • lib/test/
  • pubspec.yaml
  • 发布边界。

共享的是依赖解析,而不是包身份。

6.3 Workspace 中的依赖关系

如果 ui 依赖仓库内的 data,它应像普通包一样声明:

dependencies:
  data: ^0.1.0

Workspace 会识别该包属于同一工作区,并将其作为本地工作区包参与解析。不要同时把同一个包写成:

dependencies:
  data:
    path: ../data

除非项目明确不使用 Workspace 解析;混用两种模型容易导致成员包的解析方式不一致。

典型数据流是:

根 pubspec.yaml
   │
   ├── packages/data/pubspec.yaml
   ├── packages/ui/pubspec.yaml
   └── apps/demo/pubspec.yaml
   │
   ▼
一次全局求解
   │
   ├── 统一选择 collection
   ├── 统一选择 meta
   └── 建立成员包之间的本地解析关系
   │
   ▼
根目录共享 pubspec.lock

dataui 对同一个第三方包提出不相交约束时,Workspace 会在根目录直接报告冲突,而不是让两个成员各自形成看似成功、实际不一致的解析结果。

6.4 Workspace 的边界

Workspace 不会自动解决:

  • Android Gradle 依赖版本;
  • iOS CocoaPods 或 Swift Package 依赖;
  • Rust、C/C++、Node.js 等外部依赖;
  • 多个 Flutter 应用之间的运行时配置;
  • 包的发布版本管理。

例如,一个 Flutter 插件的 Dart 依赖可以在 Workspace 中统一,但其 android/build.gradle 使用的 Android 库版本仍由 Gradle 处理。Dart 层锁文件不能替代 Podfile.lock、Gradle 的依赖锁定或 Web 工具链的锁文件。

Workspace 也会改变发布操作的工作目录和包边界。发布某个成员包前,应进入该成员目录,检查它实际包含的 pubspec.yaml、README、许可证和源码,而不是把整个仓库误当成一个可发布包。


7. 依赖解析后的本地状态

执行 flutter pub get 后,除了 pubspec.lock,通常还会产生 .dart_tool/ 下的本地文件,例如:

.dart_tool/
└── package_config.json

编译器和分析器通过 package configuration 将:

import 'package:http/http.dart' as http;

映射到本地缓存目录中的实际源码。

这解释了两个常见现象:

  • 删除 .dart_tool/ 后重新执行 pub get 通常可以恢复本地解析元数据;
  • 只复制 pubspec.lock 而不执行 pub get,不能直接获得可编译的工作区。

Pub 缓存通常位于用户目录中。缓存损坏、部分下载或 Git 检出异常时,可以尝试:

flutter pub cache repair

该命令会重新安装缓存中的包,可能耗费网络和磁盘空间。它不会修复错误的版本约束,也不会改变 Git 仓库本身的兼容性。


8. 发布一个 Dart 或 Flutter 包

8.1 发布前的包结构

一个可发布包通常至少需要:

my_package/
├── lib/
│   └── my_package.dart
├── test/
├── README.md
├── CHANGELOG.md
├── LICENSE
└── pubspec.yaml

pubspec.yaml 示例:

name: my_package
description: A package for validating identifiers.
version: 1.2.0
repository: https://github.com/example/my_package

environment:
  sdk: ^3.3.0

dependencies:
  meta: ^1.15.0

dev_dependencies:
  test: ^1.25.0

包的公共 API 通常通过 lib/my_package.dart 暴露:

library my_package;

bool isValidIdentifier(String value) {
  return RegExp(r'^[A-Za-z][A-Za-z0-9_]*$').hasMatch(value);
}

测试:

import 'package:test/test.dart';
import 'package:my_package/my_package.dart';

void main() {
  test('accepts a Dart-like identifier', () {
    expect(isValidIdentifier('user_name'), isTrue);
  });

  test('rejects an identifier beginning with a digit', () {
    expect(isValidIdentifier('1user'), isFalse);
  });
}

执行:

dart test

Flutter 包或插件则通常使用:

flutter test

8.2 发布检查

先执行:

dart pub publish --dry-run

或 Flutter 项目:

flutter pub publish --dry-run

该命令会检查将要上传的包内容和发布条件,但不会真正发布。应重点检查:

  • 包名和版本是否合法;
  • environment 是否准确;
  • 是否误包含密钥、构建产物、私有文件;
  • 是否存在不允许消费者解析的本地 path 依赖;
  • README、CHANGELOG、LICENSE 是否齐全;
  • 导入包名与 name 是否一致;
  • 示例和测试是否能在干净环境运行。

确认后:

dart pub publish

Pub 会执行发布流程并要求确认。发布通常需要通过 pub.dev 的认证流程。具体交互界面和校验规则可能随 pub.dev 变化,不能把本地 dry-run 当作服务器最终接受的保证。

8.3 版本与发布不可逆性

包版本通常遵循 SemVer:

  • 1.2.3:补丁版本;
  • 1.3.0:新增向后兼容功能;
  • 2.0.0:不兼容变更。

一旦某个版本发布,不能用不同内容覆盖同一个版本号。正确做法是发布新版本,例如:

1.2.0  -> 1.2.1

如果版本包含安全问题,应尽快修复并发布新版本,同时评估是否需要让用户升级或撤回问题版本。撤回或标记版本的具体操作和政策属于 pub.dev 服务能力,发布者应以当前服务端文档和命令帮助为准。

库的版本承诺会反过来影响消费者的约束。例如,消费者写:

dependencies:
  my_package: ^1.2.0

它期待 1.x 内的版本兼容。如果 1.3.0 删除了公共 API,问题不仅在实现,也在错误的版本演进。


9. 供应链:从 pubspec 到可执行代码的信任边界

9.1 依赖不是只有一层

应用的依赖图包括直接依赖和传递依赖:

app
├── http
│   ├── meta
│   └── web
└── json_annotation
    └── meta

即使应用只在 pubspec.yaml 中写了 httpmetaweb 也会进入构建输入。供应链审查不能只查看直接依赖列表。

可以用以下命令观察依赖状态:

flutter pub deps
flutter pub outdated

flutter pub deps 用于查看解析后的依赖图;outdated 用于判断版本是否过旧或受约束限制。

9.2 Hosted、Git 和 Path 的风险差异

Hosted

Hosted 依赖通常具有:

  • 版本号;
  • 发布包归档;
  • 公开元数据;
  • 锁文件中的来源和摘要。

但“来自 pub.dev”不等于“自动可信”。仍需要检查维护者、源码、变更记录、依赖数量和平台代码。

Git

Git 依赖增加了仓库可用性和引用可变性风险:

git:
  url: https://github.com/example/pkg.git
  ref: main

main 会变化,今天和下周取得的代码可能不同。生产依赖更适合固定提交:

git:
  url: https://github.com/example/pkg.git
  ref: 8e4b9c2

即使固定提交,也要确认仓库历史、发布来源和构建过程可信。

Path

Path 依赖最适合开发,但最不适合直接作为外部发布包的运行时来源。它可能导致:

  • CI 上路径不存在;
  • 发布检查失败;
  • 本地未提交文件被编译;
  • 不同开发者实际使用不同源码。

9.3 Dart 包代码什么时候会影响构建和运行

普通 Dart 包没有类似某些生态中的通用“安装后脚本”机制,但这不意味着它是被动数据。包源码会被编译进应用,代码生成器会在开发阶段执行,Flutter 插件的原生实现会在平台构建阶段参与编译。

特别需要审查:

  • build_runner 等代码生成工具;
  • dev_dependencies 中的脚本和生成器;
  • Flutter 插件的 Android、iOS、macOS、Windows、Linux 实现;
  • FFI 动态库和预编译二进制;
  • 网络、文件、进程和反射相关代码;
  • 依赖升级带来的新增权限或原生配置。

对于 Flutter 应用,依赖污染可能表现为:

  • Dart 编译失败;
  • Android Gradle 构建失败;
  • iOS CocoaPods 或 Xcode 编译失败;
  • 运行时加载错误;
  • Web 构建中引入不兼容的浏览器 API;
  • 插件在某个平台声明支持,但实际实现不完整。

因此,Dart 锁文件只能锁定 Dart/Flutter 包解析结果,不会自动锁定全部平台供应链。

9.4 pubspec.lock 与完整可复现构建

一份提交到 Git 的 pubspec.lock 可以固定 Pub 层版本,但完整构建还可能依赖:

Dart/Flutter SDK
pubspec.lock
Pub 缓存内容
Android Gradle Plugin / Gradle / JDK
Android SDK
Xcode / CocoaPods / Swift Package
Web 工具链

例如,Dart 依赖完全未变,但升级 Xcode 后,iOS 插件可能因为新的编译器检查而失败。反过来,Android 的 Gradle 依赖发生变化,也不会反映在 pubspec.lock 中。

生产环境因此应同时记录:

  • Flutter SDK 版本;
  • Dart SDK 版本;
  • Pub 锁文件;
  • 平台工具链版本;
  • 平台侧的锁定文件和构建配置;
  • CI 使用的缓存恢复和清理策略。

9.5 依赖混淆和私有仓库边界

如果公司同时使用内部 hosted server 和公开 pub.dev,应明确包名、来源和解析策略。危险情况包括:

  • 内部包名与公开包名冲突;
  • 团队成员误把私有包改成默认 pub.dev 来源;
  • 使用未认证的镜像;
  • 在 Git URL 中嵌入访问令牌;
  • 将私有仓库地址或凭证提交进公开 pubspec.yaml

凭证不应出现在:

git:
  url: https://token:secret@example.com/repo.git

应使用 CI 的凭证管理和 Git 认证配置。公开包还应避免依赖只能在公司网络访问的私有包,否则消费者无法解析。


10. 诊断失败:从现象定位到恢复

10.1 版本求解失败

现象:

version solving failed

不要先删除所有缓存。先执行:

flutter pub deps
flutter pub outdated

然后检查:

  1. 错误信息中最底层的冲突包;
  2. 两个上游依赖的约束交集;
  3. 当前 Dart/Flutter SDK 是否满足 environment
  4. 是否存在 dependency_overrides
  5. 是否把同一个包通过 hosted、Git 和 path 混用了。

如果是 SDK 不满足,升级 Flutter 或降低包版本;如果是两个约束不相交,必须改变依赖图,而不是反复执行 pub get

10.2 锁文件与 YAML 不一致

现象可能包括:

pubspec.lock is out of date

处理流程:

git diff -- pubspec.yaml pubspec.lock
flutter pub get
git diff -- pubspec.lock
flutter test

如果这是 CI 的 --enforce-lockfile 失败,说明提交的锁文件没有覆盖当前声明。应在受控环境中更新锁文件并提交,而不是在 CI 中自动忽略错误。

10.3 缓存或网络问题

现象包括:

  • hosted 下载超时;
  • Git checkout 失败;
  • 缓存中的包文件损坏;
  • 离线环境无法解析新依赖。

可以先使用详细日志:

flutter pub get -v

必要时修复缓存:

flutter pub cache repair

--offline 只能使用本地已有缓存;如果锁文件要求的版本不在缓存中,它不会创造一个可用包。离线构建前应在联网环境预热并验证缓存,不能把离线选项当作供应链完整性机制。

10.4 Android、iOS、桌面和 Web 的额外失败

Dart 依赖解析成功后,平台构建仍可能失败:

平台 Pub 之后的主要构建边界
Android Gradle、Android Gradle Plugin、JDK、Android SDK、NDK
iOS Xcode、CocoaPods 或 Swift Package、签名和部署目标
macOS Xcode、原生框架、沙盒和签名
Windows Visual Studio、MSVC、原生 DLL
Linux 系统编译器、GTK 等系统库
Web 浏览器支持、JavaScript/WASM 互操作和 Web 构建工具

例如,包在 Dart VM 上测试通过,并不证明它可以在 Web 上运行;使用 dart:io 的库通常不能直接用于 Web。使用条件导入时,应分别执行目标平台测试和构建。


11. 一套可复现的实际工作流

以 Flutter 应用为例:

flutter --version
flutter pub get
flutter analyze
flutter test
flutter pub outdated
flutter build apk

每一步的作用不同:

  1. flutter --version:确认 Flutter 和内置 Dart SDK;
  2. flutter pub get:根据声明和锁文件取得依赖;
  3. flutter analyze:检查静态类型和 API 使用;
  4. flutter test:验证 Dart/Flutter 行为;
  5. flutter pub outdated:审查可升级范围;
  6. flutter build apk:验证 Android 原生边界。

升级一个依赖时:

# 修改 pubspec.yaml 后
flutter pub upgrade
git diff -- pubspec.lock
flutter analyze
flutter test
flutter build apk

发布一个库时:

dart format .
dart analyze
dart test
dart pub publish --dry-run
dart pub publish

如果是 Flutter 插件,则应额外验证:

flutter test
flutter build apk
flutter build ios --no-codesign
flutter build web

其中 flutter build ios --no-codesign 只能验证一部分 iOS 构建流程;真实发布仍涉及签名、证书、Provisioning Profile 和目标机器上的 Xcode 环境。


12. 容易混淆的结论

约束越宽,兼容性越好。
不一定。>=1.0.0 没有上界,可能允许一个尚未验证的主版本;约束应表达包真正支持的范围,而不是尽可能放宽。

锁文件能保证完整构建完全一致。
不一定。它主要锁定 Pub 层依赖,无法替代 Flutter SDK、Gradle、CocoaPods、Xcode 和系统库的版本控制。

pub get 总是获取最新版本。
不一定。已有锁文件满足当前约束时,它通常优先保持锁定结果;主动升级应使用 pub upgrade

dependency_overrides 解决了依赖冲突。
它只是让当前根项目忽略普通约束。被覆盖版本可能编译失败、运行失败或破坏上游包的假设。

Workspace 把多个包变成一个发布包。
不是。Workspace 共享解析,成员仍然是独立包,也仍然有独立版本和发布边界。

Dart 包能解析就代表 Flutter 插件可用。
不是。插件还要通过每个平台自己的原生编译、链接、签名和运行时验证。

发布到 pub.dev 就自动安全。
不是。Hosted 分发提供了规范化入口和包元数据,但维护者仍必须审查依赖、源码、生成器、原生实现和升级变化。

Pub 的核心可以归纳为一个约束系统:

最终依赖图=直接依赖约束传递依赖约束SDK 约束来源可用性\text{最终依赖图} = \text{直接依赖约束} \cap \text{传递依赖约束} \cap \text{SDK 约束} \cap \text{来源可用性}

pubspec.yaml 定义允许集合,solver 在集合交集中选择版本,pubspec.lock 保存一次选择结果,Workspace 扩大了共同求解的包集合,发布则把其中一个包及其声明交给消费者重新求解。理解这条数据流,才能准确判断升级、冲突、复现、发布和供应链问题分别发生在哪一层。


系列导航与关联阅读

官方资料

本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。