源码拆解|OpenTAP 本地包安装与依赖解析全链路演进

背景

在 CI 流水线或离线工控场景下,工程师常需要“把一个 .TapPackage 文件丢到目标机,一键装完所有依赖”。OpenTAP 的 tap package install 看似只是 unzip,背后却有一套并行下载、版本协商、冲突降级的完整流程。今天把镜头对准 Package 子系统,拆解本地安装链路。

框架分析

Package 子系统核心角色:

  • Installation —— 表示一个 OpenTAP 根目录,维护已安装列表 (/Packages/*.PackageDef.xml)
  • IPackageRepository —— 抽象源,可以是文件夹、HTTP、缓存
  • DependencyResolver —— 已废弃,逻辑已迁至 Image/PackageDependencyQuery.cs
  • Installer —— 真正落盘的“安装工”,负责解压、文件锁定、回滚
  • PackageDef —— 描述包元数据,含 Dependencies 列表与 Architecture/OS 约束

安装入口统一在 tap.exe package install xxx.tappackage,CLI 动作类 PackageInstallAction 把参数转成 PackageInstallStep,再交给 Installer.Install

实现过程

  1. 前置检查
    Installer 构造时即校验目标目录是否被占用:若 tap.exe 正在运行,会抛出 InstallationLockedException;否则在 %LocalAppData%\OpenTap\InstallLock\{Installation.Id}.lock 创建 Mutex。

  2. 元数据反序列化
    .TapPackage 视为 ZIP,先读头文件 Package/package.xml 得到 PackageDef;此时并不解压 payload,仅做兼容性预检(CPU、OS、MinOpenTapVersion)。

  3. 依赖展开
    调用 PackageDependencyQuery.ResolveDependencies

    • 以请求包为根,广度优先遍历依赖树
    • 对已安装节点,采用最大满足策略(已装版本 ≥ 所需版本则复用)
    • 对缺失节点,按 Repo 优先级依次下载;若存在版本冲突,则选最低兼容版本降低爆炸半径
      最终得到 List<PackageDef> toInstall,并按拓扑排序保证依赖先装。
  4. 并发下载 & 缓存
    使用 HttpPackageRepository 时,会先把包写入 %LocalAppData%\OpenTap\PackageCache\{hash}.tappackage;后续同名安装直接走缓存,避免重复拉取。

  5. 原子落盘
    Installer.InstallPackage 逐条执行:

    • 解压到临时目录 OpenTap\Packages\.staging\{name}\
    • 若存在同名旧版,先重命名为 .backup
    • .staging 移入正式目录,同时写入 PackageDef.xml
    • 若全部成功,删除 .backup;否则触发回滚,把 .backup 还原并删除新目录
  6. 后置钩子
    如果包内含 install.batinstall.shInstaller 会在事务外调用,失败仅警告不回滚,保证脚本副作用可控。

可复现命令

1
2
3
4
5
# 离线安装本地包,跳过下载阶段,强制覆盖同版本
tap package install ./MyPlugin-1.2.3.TapPackage --force --no-cache

# 查看安装事务日志,定位依赖冲突
tap log --level=Debug --pattern="Installer*" -f

注意事项

  • 同一进程内禁止并发安装;CI 中若并行调用 tap package install,需外加文件锁
  • 若目标机无网络,把依赖包事先放到 Repositories: - path: ./local-repo 文件夹,即可当作本地源
  • 降级场景下,Installer 不会自动卸载高版;如需干净环境,先 tap package uninstall --all 再批量装
  • Windows 长路径问题:包内若嵌套 node_modules 深度 > 8,需提前启用长路径组策略或改用 Linux 构建机

小结

OpenTAP 把“安装”拆成元数据读取→依赖协商→缓存下载→事务落盘→钩子触发五步,每一步都可单测、可回滚。理解这一链路的意义在于:当离线交付或国产化编译器出现“装得上跑不起来”时,你能迅速定位是版本协商失败,还是脚本钩子写错。下一篇我们把镜头转向 Engine/TestStep,看看执行引擎如何把 TestPlan 转成线程树。

关键源码路径

  • Package/Installer.cs —— 安装事务主循环
  • Package/Installation.cs —— 已安装列表与架构检测
  • Package/Image/PackageDependencyQuery.cs —— 新版依赖解析器
  • Package/PackageDef.cs —— 包元数据 POCO
  • Package/PackageManagerSettings.cs —— 源优先级与缓存开关