- 虚拟化
- 开发工具
- 云原生
【免费下载链接】multipass
Multipass orchestrates virtual Ubuntu instances
Multipass 的图形化客户端(GUI)基于 Flutter 构建,其依赖、版本与构建流程都遵循一套独立的维护规范。本文以开发文档 dev-docs/managing-flutter.md 为主线,结合仓库中的 pubspec.yaml、FFI 胶水层源码与子模块配置,系统讲解如何升级 Flutter 版本、维护 GitHub 分叉依赖、同步 protobuf,以及排查 snapcraft 本地构建失败等实际问题。读完本文,你将掌握一套可复现的 Flutter GUI 升级与调试流程,并理解 C++/Flutter 胶水层dart_ffi的真实工作原理。
更新 Flutter 版本
Flutter 大约每季度发布一个新的稳定版本,补丁版本(patch release)则随时可能推出。Multipass 的 Flutter 来源是3rd-party/flutter这个 git 子模块(仓库内所有第三方依赖的子模块版本可参考 3rd-party/submodule_info.md,该文件要求子模块更新时同步维护版本信息)。
当新版本发布后,进入子模块目录依次执行:
git fetch git checkout <release-tag> # 切换到实际发布标签,例如 3.32.5 flutter precache # 下载新版 Flutter 的预编译二进制其中flutter precache会下载对应平台(macOS / Linux / Windows)所需的 Flutter 引擎与工具链二进制,是切换版本后让本地 SDK 可用的关键一步。
更新 Flutter 依赖
Flutter 依赖集中声明在 src/client/gui/pubspec.yaml 中。每次 Flutter 升级后,需要手动逐项核对其中的依赖版本,去 pub.dev 检查是否有更新的版本。理论上可以用flutter pub upgrade --major-versions一键升级主版本,但文档明确建议逐个阅读上游 changelog 后再决定是否升级——因为部分依赖是 Multipass 自己维护的分叉(fork),盲目升级可能破坏已打上的补丁。
来自 GitHub 的依赖与本地补丁
仓库中有一部分依赖并非来自 pub.dev,而是直接引用 GitHub 仓库,并在分叉上维护了针对 Multipass 场景的补丁。逐一说明如下:
dartssh2(canonical/dartssh2)
这是 pub.dev 上 dartssh2 包的一个分叉,包含两个补丁:
- 从 octetstring 序列中解析 RSA 密钥数字:改变了 RSA 密钥的解析方式,因为 Multipass 使用的密钥无法被原库解析。
- 用 RawSocket 替代 Socket 以获得更多定制能力:改变了从 SSH socket 读取数据的方式——引入读取间隔并手动进行更大规模的读取,而不是让库在数据可用时总是自动读取并推送到监听流中。这是必要的:当 socket 上有大量数据(例如
yes命令的输出)时,自动读取会过量填充 Dart 的 microtask 队列,导致 Dart 无法及时处理 Flutter 事件(比如按 Ctrl+C 停止yes),因为那些事件运行在正常的 event loop 上。
在 pubspec.yaml 中它的引用方式为:
dartssh2: git: url: https://github.com/canonical/dartssh2.git ref: 2.17.1+mp注意ref指向2.17.1+mp分支,mp后缀即 Multipass 补丁分支的命名约定(详见下文"分叉依赖的升级")。
tray_menu(canonical/tray_menu)
这是 Andrei 开发、用于创建系统托盘图标的库。文档坦率指出它缺乏文档和测试,只有必要代码,且代码本身也很需要重构。目前 pubspec.yaml 中直接引用其main分支:
tray_menu: git: url: https://github.com/canonical/tray_menu.git ref: mainxterm(canonical/xterm.dart)
这是 pub.dev 上 xterm 包的分叉,由 Lev 打了一个补丁:实现行高对齐(line-height snapping)与整行渲染,让终端只显示完整可见的行,并以一行为单位滚动,取代原来连续的平滑滚动。在 pubspec.yaml 中:
xterm: git: url: https://github.com/canonical/xterm.dart ref: 4.0.0+mphotkey_manager(canonical/hotkey_manager)
pub.dev 上 hotkey_manager 包的分叉,包含一个补丁:禁用 cooked accelerators,使 keybinder 能识别如ctrl+shift+1这样的组合键,而不是把它归一化/重解释成ctrl+!。该补丁在 pubspec.yaml 中通过dependency_overrides以更细粒度引入:
dependency_overrides: hotkey_manager_linux: git: url: https://github.com/canonical/hotkey_manager.git ref: no-cooked-accel path: packages/hotkey_manager_linux vector_math: ^2.2.0window_size(google/flutter-desktop-embedding)
window_size 插件同样来自 GitHub,但 Multipass 没有为它打任何补丁——它只是没有发布到 pub.dev 而已,因此维护时基本无需担心:
window_size: git: url: https://github.com/google/flutter-desktop-embedding.git path: plugins/window_size ref: 6c66ad2分叉依赖自身的升级
上述分叉在必要时也要跟着升级。流程是:先检查 pub.dev 上的上游包是否有新版本;如果有,就在最新稳定版之上重新应用 Multipass 的补丁。补丁要打在一个以"目标版本号 +mp后缀"命名的分支上——例如 dartssh2 2.12.0 发布时,补丁就打在了2.12.0+mp分支上。新分支名必须同步写进pubspec.yaml的ref字段,否则 CI 和本地构建仍会拉取旧分支。
更新后的验证与收尾
更新完pubspec.yaml中的包之后,执行flutter pub upgrade以生成最新的pubspec.lock。随后:
- 处理告警:如果
flutter analyze报告了任何 deprecation 警告,应尽早修复,不要拖延。 - 全平台构建验证:必须确认 GUI 在所有平台上都能构建,使用
flutter build <macos|linux|windows>。 - 处理平台目录改动:构建时 Flutter 可能会尝试修改
macos、linux、windows这些平台特定目录。如果因为 Multipass 已有的改动而无法自动应用,Flutter 会打印出它试图应用的内容,需要手动照做。 - 提交:一切正常后,提交
pubspec.{yaml,lock}以及macos、linux、windows目录中的相关改动。 - 格式化:最后用
dart format src/client/gui做一遍 lint/格式化,确保代码符合规范。 - CI 验证:本地构建通过后,还需确认 CI 中也能正常构建,才算完成。
更新 protobuf
protobuf 是一个特殊依赖:Multipass 同时维护着3rd-party/protobuf.dart子模块仓库,以及 pubspec.yaml 中的版本声明(当前为protobuf: 6.0.0),两者必须保持同步。升级时要同时更新子模块与 pubspec 中的版本号,缺一不可。GUI 通过 gRPC 与守护进程(daemon)通信(依赖见 pubspec.yaml 中的grpc: ^5.0.0),protobuf 与 gRPC 的版本配套关系因此尤为重要。
Multipass GUI:C++/Flutter 胶水层
GUI 的一个重要组成部分是 Multipass 自建的dart_ffi库。它是一层C 库,把一些对 GUI 同样有用的 C++ 代码以 C ABI 的形式暴露给 Dart 侧调用。需要特别强调的是:这层胶水是纯客户端库,不允许你从 daemon 获取任何信息,它只封装客户端本地的能力。
导出接口一览
C 侧接口声明在 include/multipass/dart_ffi.h,实现位于 src/client/gui/ffi/dart_ffi.cpp。主要导出函数包括:
| 函数 | 作用 |
|---|---|
multipass_version() | 返回 Multipass 版本字符串(mp::version_string) |
generate_petname() | 生成随机实例名(复用mp::petname名称生成器) |
get_server_address() | 返回客户端要连接的服务器地址(unix socket 或 tcp) |
get_cert_pair() | 返回客户端证书与私钥(PEM 格式) |
get_root_cert() | 返回根证书内容 |
settings_file() | 返回持久化设置文件路径 |
get_setting(key, output)/set_setting(key, value, output) | 读写客户端设置,返回SettingResult枚举(Ok/KeyNotFound/InvalidValue/UnexpectedError) |
uid()/gid()/default_id() | 返回用户、组及默认 ID |
memory_in_bytes(value)/human_readable_memory(bytes) | 内存字符串与字节数互转 |
get_total_disk_size() | 返回 Multipass 存储位置所在磁盘的总大小 |
default_mount_target(source) | 返回给定源路径的默认挂载目标 |
实现要点
从 src/client/gui/ffi/dart_ffi.cpp 的实现可以看到几个值得注意的设计:
- 错误处理:绝大多数导出函数都包在 try/catch 中,异常时通过
multipass::logging以dart-ffi分类输出 warning,并返回nullptr或错误枚举(如memory_in_bytes解析失败返回-1),避免异常跨 C ABI 边界传播。 - 设置读写:
get_setting/set_setting内部通过std::call_once确保只注册一次全局设置处理器(mpc::register_global_settings_handlers),再调用MP_SETTINGS完成实际读写;UnrecognizedSettingException映射为KeyNotFound,InvalidSettingException映射为InvalidValue。 - 磁盘容量:
get_total_disk_size优先取MP_PLATFORM.multipass_storage_location(),为空时回退到StandardPaths::AppDataLocation,再借助 Qt 的QStorageInfo获取该位置的bytesTotal()。 - 内存单位换算:
human_readable_memory复用mp::MemorySize::from_bytes(...).human_readable(precision=2, trim_zeros=true),保证 GUI 与 CLI 显示格式一致。
Dart 侧的绑定
Dart 侧绑定在 src/client/gui/lib/ffi.dart,通过dart:ffi的DynamicLibrary.open(mpPlatform.ffiLibraryName)加载动态库(各平台库名由lib/platform/下的平台代码给出)。加载失败时会记录loadError,GUI 据此优雅降级。get_cert_pair与get_root_cert会把 PEM 内容转为字节列表供 gRPC 客户端使用;getServerAddress会解析unix:<path>与<host>:<port>两种地址形式。值得注意的是 src/client/gui/ffi.dart 中ffi.Pointer<Utf8>的扩展在读取 Dart 字符串后会malloc.free(this)释放由 C 侧strdup分配的内存,与 dart_ffi.cpp 的内存管理约定一一对应,避免了泄漏。
排查:本地 snapcraft 构建失败
在本地用 snap 方式构建 GUI 时,构建过程可能失败。文档指出一个已知的常见原因:
- snapcraft 会把 GUI 的源码目录整体拉入 snapcraft VM,而源码目录中可能包含你本机机器上的引用路径(reference paths),这些路径在 VM 内是无效的。
- 根本原因是:Flutter 的构建产物文件无法与源码完全分离,而 snapcraft 又没有类似 ignore 文件的机制,无法指定"不要拉取这些文件"。
修复步骤(按文档建议,注意会丢失构建缓存):
- 删除 snapcraft VM(是的,snapcraft 将不得不从头重新构建一切);
- 删除
build、.dart_tool和linux/flutter/ephemeral目录; - 重新执行 snap 构建。
这三类目录都是 Flutter 在构建过程中生成的中间产物/缓存目录,其中linux/flutter/ephemeral存放 Linux 桌面嵌入层的临时生成文件,里面极易混入本机绝对路径;清掉它们并重建一个干净的 snapcraft VM,即可绕开无效引用路径导致的失败。
小结
维护 Multipass 的 Flutter GUI 是一项环环相扣的工作:子模块升级(3rd-party/flutter)→ 依赖核对与分叉补丁重打(+mp分支约定)→pubspec.lock重新生成 → 全平台构建验证 → 格式检查与提交;同时还要维护 protobuf 子模块与 pubspec 版本的双重同步,理解dart_ffi胶水层的职责边界(纯客户端、不触碰 daemon),并掌握 snapcraft VM 缓存污染的排查手段。以上流程与源码证据均可在 dev-docs/managing-flutter.md、src/client/gui/pubspec.yaml、include/multipass/dart_ffi.h 及 src/client/gui/ffi/dart_ffi.cpp 中找到对应依据,可作为后续版本升级时的操作清单复用。
- 虚拟化
- 开发工具
- 云原生
【免费下载链接】multipass
Multipass orchestrates virtual Ubuntu instances
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考