installation pipeline - mclucy/lucy GitHub Wiki

安装流水线(Installation Pipeline)

本文系统性描述 Lucy install/ 模块的完整实现细节,面向希望深入理解安装机制的贡献者。

在阅读本页前,建议先了解:

install 模块位于 Lucy 数据流末端:接收 routing 层解析出的 PackageRemote,并将目标文件落地到服务器目录。模块内部采用 "注册表 + 函数分发" 模式:每个平台对应独立 platformInstaller,并由全局 map 在进程启动时通过 init() 完成注册。

关键要点

调度流程(Dispatch Flow)

用户执行 lucy add <package> 后,调用链如下:

用户输入: lucy add fabric/fabric-api@latest
  |
  v
cmd/cmd_add.go -> actionAdd()                         cmd/cmd_add.go:49
  |  syntax.Parse() 解析字符串为 PackageId
  |  版本 VersionLatest/VersionAny -> VersionCompatible  cmd/cmd_add.go:54
  |
  v
install/install.go -> Install(id, source)             install/install.go:25
  |
  +- [identity package 分支]
  |    installPlatform(id)                            install/install.go:110
  |    处理 minecraft/fabric/forge/neoforge/mcdr 平台自身安装
  |
  '- [普通 mod/plugin 分支]
       |
       v
     ensureServerPlatformMatch(id)                    install/install.go:35
       评估 topology 兼容性,验证目标平台与当前服务器匹配
       |
       v
     probe.ServerInfo()                               install/install.go:39
       获取服务器环境快照(platform、topology、mod 路径等)
       |
       v
     routing.ResolveProvidersByTopology(...)          install/install.go:43
       根据 topology + source 选择 Provider 列表
       检测 MCDR 环境时追加 MCDR Provider              install/install.go:52
       |
       v
     routing.FetchMany(providers, id)                 install/install.go:68
       并行 fetch,返回全部成功的 PackageRemote 候选列表
       |
       +- 0 候选 -> 返回错误
       +- 1 候选 -> 直接选用
       '- 多候选 -> selectFromCandidates() 触发 TUI 选择  install/install.go:91
       |
       v
     installers[id.Platform](p)                       install/install.go:98
       按 Platform 从注册表取出 platformInstaller 并调用
       |
       v
     util.CachedDownload(...)
       content-addressed 缓存层,cache hit 时跳过网络

平台兼容性校验

ensureServerPlatformMatch() 会调用 probe.EvaluateCompatibility(topology, requiredCapability) 评估拓扑兼容性,结论与行为如下:

Verdict 含义 行为
CompatCompatible 直接能力匹配 放行
CompatDegraded 桥接兼容(risk < High) 警告后放行
CompatDegraded 桥接兼容(risk >= High) 返回错误,拒绝安装
CompatIncompatible 无法匹配 返回错误
CompatUnresolved topology 未解析 返回错误

参见 install/install.go:200probe/probe_topology_evaluation.go:13

Install() 主流程入口

// ../lucy/install/install.go:25-108
func Install(id types.PackageId, source types.Source) error {
 p := id.NewPackage()

 // route to platform installer if it's an identity package
 if id.IsIdentityPackage() {
  return installPlatform(id)
 }

 // this is order-sensitive, ensureServerPlatformMatch() does not check for
 // identity packages
 if err := ensureServerPlatformMatch(id); err != nil {
  return err
 }

 serverInfo := probe.ServerInfo()
 serverPlatform := serverInfo.Executable.DerivedModLoader()
 hasMcdr := serverInfo.Environments.Mcdr != nil

 providers, err := routing.ResolveProvidersByTopology(
  serverInfo.Executable.Topology,
  serverPlatform,
  source,
 )
 if err != nil {
  return err
 }

 if hasMcdr {
  mcdrProviders, err := routing.ResolveProviders(
   types.PlatformMCDR,
   types.SourceAuto,
  )
  if err != nil {
   logger.ShowInfo(
    fmt.Errorf(
     "failed to resolve MCDR provider: %w",
     err,
    ),
   )
  }
  providers = append(providers, mcdrProviders...)
 }

 remotes, errs := routing.FetchMany(providers, id)
 for _, err := range errs {
  if source == types.SourceAuto && len(providers) > 1 {
   logger.ReportWarn(
    fmt.Errorf(
     "search on %s failed: %w",
     err.Source.Title(),
     err.Err,
    ),
   )
   continue
  }
 }

 switch len(remotes) {
 case 0:
  return fmt.Errorf("no candidates found for %s", id.String())
 case 1:
  // good,follow through
  p.Remote = &remotes[0]
 default:
  // prompt user to select one
  var err error
  p.Remote, err = selectFromCandidates(remotes)
  if err != nil {
   return err
  }
 }
 source = p.Remote.Source

 installer := installers[id.Platform]
 if installer == nil {
  return fmt.Errorf("no installer found for platform %s", id.Platform)
 }
 err = installer(p)
 if err != nil {
  return err
 }

 return nil
}

platformInstaller 注册机制

安装器注册表由 map[types.Platform]platformInstaller 维护;每个平台文件通过 init() 注册对应函数。

// install/install.go
type platformInstaller func(p types.Package) error
var installers = map[types.Platform]platformInstaller{}

func registerInstaller(platform types.Platform, installer platformInstaller) {
    if installer == nil {
        panic("install: nil installer")
    }
    installers[platform] = installer
}

参见 install/install.go:14

// install/install_generic.go
func init() {
    registerInstaller(types.PlatformAny, installGenericPackage)
}

平台安装器(Platform Installers)

注册表汇总

平台 注册函数 注册文件
PlatformAny installGenericPackage install_generic.go:12
PlatformFabric installFabricMod install_fabric.go:36
PlatformForge installForgeMod install_forge.go:68
PlatformNeoforge installNeoForgeMod install_neoforge.go:6
PlatformMCDR installMcdrPlugin install_mcdr.go:16

Generic 安装器(PlatformAny

installGenericPackage 是最短路径,适用于不依赖 mod loader 的通用包。它直接调用 CachedDownload,将文件落地到工作目录根("."):

result, err := util.CachedDownload(p.Remote.FileUrl, ".", util.DownloadOptions{
    Kind:          cache.KindArtifact,
    Filename:      p.Remote.Filename,
    ExpectedHash:  p.Remote.Hash,
    HashAlgorithm: cache.ParseHashAlgorithm(p.Remote.HashAlgorithm),
})

参见 install/install_generic.go:21

Fabric Mod 安装器

installFabricMod 委托 installModLoaderPackage(p, types.PlatformFabric),最终将文件写入 serverInfo.ModPath[0](服务器 mod 目录):

result, err := util.CachedDownload(
    p.Remote.FileUrl,
    serverInfo.ModPath[0],
    util.DownloadOptions{...},
)

参见 install/install_modloader.go:27。若 serverInfo.ModPath 为空则返回错误。

Forge Mod 安装器

installForgeMod 同样委托 installModLoaderPackage(p, types.PlatformForge),落地路径为 mod 目录。参见 install/install_forge.go:71

NeoForge Mod 安装器

installNeoForgeMod 委托 installModLoaderPackage(p, types.PlatformNeoforge),落地路径为 mod 目录。参见 install/install_neoforge.go:9

MCDR Plugin 安装器

installMcdrPluginserverInfo.Environments.Mcdr.Config.PluginDirectories[0] 获取插件目录并落地文件。若 MCDR 环境或插件目录不存在,则返回错误。参见 install/install_mcdr.go:19

平台自身安装(Identity Package 分支)

PackageId.IsIdentityPackage()true 时,Install() 将进入 installPlatform(),跳过 routing 层,直接安装平台本身。

Vanilla(Minecraft 原版服务端)

installMinecraftServer() 通过 Mojang 版本清单 API 解析目标版本;在用户同意 EULA 后下载并落地 server JAR,同时设置可执行权限(chmod +x)。参见 install/install_vanilla.go:34

Fabric 平台

installFabric() / installFabricWithOverride() 调用 Fabric Meta API 解析 loader 与 game 版本,构造 server JAR 下载 URL,并通过 CachedDownload 落地到 serverInfo.WorkPath。参见 install/install_fabric.go:39

Forge 平台

installForge() 从 Forge promotions API 解析版本,下载 installer JAR,然后本地执行 java -jar forge-*-installer.jar --installServer。安装过程中会实时解析日志驱动进度条;完成后调用 verifyForgeInstallation() 校验产物。参见 install/install_forge.go:188

NeoForge 平台

Warning

此功能尚未完整/正确实现 NeoForge 平台自身安装(installNeoForge())尚未实现;当前调用将直接 panic。详见 install/install_neoforge.go:14

MCDR 平台

initMcdr() 的初始化步骤为:

  1. 检查 mcdreforged 命令是否在 PATH 中。
  2. 创建 server/ 子目录。
  3. 将当前目录下所有文件移入 server/
  4. 执行 mcdreforged init
  5. 调用 probe.Rebuild() 重新探测服务器环境。

参见 install/install_mcdr.go:50

冲突处理(Conflict Handling)

多候选冲突:TUI 选择

routing.FetchMany() 返回多个 PackageRemote 时,selectFromCandidates() 基于 huh 库弹出交互式选择界面,由用户确认最终安装项。参见 install/install.go:253

平台冲突:Guard 检查

installPlatform() 对平台冲突设有明确保护:

  • 检测到 Forge/NeoForge 时,不允许安装 Fabric(反向同理)。
  • 检测到 Fabric 时,不允许安装 Forge。
  • MCDR 已安装时,不允许重复初始化。

这些判定基于 probe.ServerInfo().Executable.DerivedModLoader() 返回的当前平台信息。

Overwrite 冲突:已知限制

Warning

此功能尚未完整/正确实现 Minecraft Vanilla 服务端的覆盖安装(overwrite confirmation)尚未完整实现。当检测到已有服务器时,当前实现会直接返回错误 "a server is already installed",而非提示用户确认是否覆盖。源码中已以 TODO 标记该缺口。详见 install/install.go:131

case types.PlatformMinecraft:
    if serverPlatform != types.PlatformNone {
        // TODO: ask if overwrite existing server
        return errors.New("a server is already installed")
    }
    return installMinecraftServer(id)

Vanilla -> Fabric 覆盖

Fabric 平台安装对 Vanilla 提供特殊覆盖流程:若检测到 Vanilla,promptOverrideVanillaWithFabric() 会通过 TUI 询问是否覆盖,并可选择删除原 Vanilla JAR。这是当前唯一完整实现的覆盖确认路径。参见 install/install_platform_fabric.go:130

安装后处理(Post-Install)

多数安装器在文件落地后无额外动作。例外如下:

  • Forge 平台安装:installer 执行完成后调用 verifyForgeInstallation() 校验产物,再调用 probe.Rebuild() 刷新环境信息。
  • Fabric 平台安装:调用 probe.Rebuild() 刷新环境信息(参见 install/install_fabric.go:103)。
  • MCDR 平台初始化:调用 probe.Rebuild() 重新探测整个服务器目录结构。

Warning

以下内容基于非完整代码推断,可能与实际预期行为存在偏差。

请以 install/install.go:98 的实际分发逻辑为准。普通 mod/plugin 安装器(Generic、Fabric Mod、Forge Mod、NeoForge Mod、MCDR Plugin)在文件落地后通常不调用 probe.Rebuild(),因为单个 mod/plugin 安装不会改变服务器平台拓扑,无需重建 topology 缓存。

错误处理策略

Install() 中:当 SourceAuto 且 Provider 数量大于 1 时,单个 Provider 的 fetch 失败会以警告(logger.ReportWarn)上报,而非立即终止,以保留其他 Provider 继续提供候选的机会。

仅当所有 Provider 均无法提供候选(候选列表为空)时,流程才返回最终错误。

参见 install/install.go:69

相关页面

参考文件

本页内容综合以下源码(基线 commit 86d3480cffa821b9b9c0747263a637b2973a7366):

  • install/install.go:14 - platformInstaller 类型定义与 registerInstaller() 注册模式
  • install/install.go:25 - Install() 主流程入口
  • install/install.go:98 - 按 Platform 分发至 platformInstaller
  • install/install.go:110 - installPlatform() identity package 分支
  • install/install.go:131 - overwrite 未完成实现(L-3 已知限制)
  • install/install.go:180 - ensureServerPlatformMatch() 兼容性校验
  • install/install.go:253 - selectFromCandidates() 多候选 TUI 选择
  • install/install_generic.go:12 - Generic 安装器注册与实现
  • install/install_fabric.go:36 - Fabric mod 安装器注册
  • install/install_fabric.go:39 - Fabric 平台安装实现
  • install/install_forge.go:68 - Forge mod 安装器注册
  • install/install_forge.go:188 - Forge 平台安装(包含 java installer 执行)
  • install/install_neoforge.go:6 - NeoForge mod 安装器注册
  • install/install_neoforge.go:14 - NeoForge 平台安装(panic,未实现)
  • install/install_mcdr.go:16 - MCDR plugin 安装器注册
  • install/install_mcdr.go:50 - MCDR 平台初始化逻辑
  • install/install_modloader.go:15 - 通用 mod loader 包安装函数
  • install/install_vanilla.go:34 - Minecraft Vanilla 服务端安装
  • install/install_platform_fabric.go:130 - Vanilla -> Fabric 覆盖确认 TUI
  • probe/probe_topology_evaluation.go:13 - EvaluateCompatibility() 拓扑评估
  • probe/probe_topology_evaluation.go:67 - CapabilityForPlatform() 平台能力映射
  • cmd/cmd_add.go:49 - actionAdd CLI 入口
  • cmd/cmd_add.go:59 - install.Install() 调用点
⚠️ **GitHub.com Fallback** ⚠️