dependency resolution - mclucy/lucy GitHub Wiki

依赖解析与版本方言

Lucy 在处理不同平台的 mod/插件依赖时,需要同时支持三套互不相同的版本描述语法(版本方言,Version Dialect)。每套方言都对应特定生态与上游数据源,并使用各自的版本格式与区间表达方式。

Lucy 的版本处理由两个彼此独立、但由同一方言控制的阶段构成:

  1. 解析(Parse):把原始版本字符串(RawVersion)转换为可比较对象(ComparableVersion)。
  2. 区间匹配(Range):把依赖约束(如 >=1.0.0 <2.0.0)解析为 VersionConstraint 集合,再与具体版本进行评估。

方言选择由包来源平台(Platform)决定。

关键要点

快速对照

三种方言总览

维度 semver(npm/Fabric/MCDR) maven minecraft
应用平台 Fabric、MCDR Forge、NeoForge 游戏本体版本判断
版本格式示例 1.2.32.0.0-alpha.1 1.0.0(版本号本身同 semver) 1.20.125w14a26.1-snapshot-1
区间写法示例 >=1.0.0 <2.0.0^1.2.0~1.2 [1.0,2.0)[1.0.0,] 无独立区间语法
OR 支持 ` `(Fabric/npm)
实现入口 parseMcdrSemverRange / parseSemverRange parseMavenRange parseMinecraftRelease / parseMinecraftSnapshot
来源标准 npmjs semver、MCDR metadata 规范 Apache Maven 区间语法 Mojang 版本命名规范
// ../lucy/dependency/version_parse.go:16-38
func Parse(
 raw types.RawVersion,
 scheme types.VersionScheme,
) types.ComparableVersion {
 switch raw {
 case types.VersionLatest, types.VersionCompatible, types.VersionNone, types.VersionAny, types.VersionUnknown:
  logger.Error(
   fmt.Errorf("attempting to parse an ambiguous version: %s", raw),
  )
  return nil
 }

 switch scheme {
 case types.Semver:
  return parseSemver(raw)
 case types.MinecraftRelease:
  return parseMinecraftRelease(raw)
 case types.MinecraftSnapshot:
  return parseMinecraftSnapshot(raw)
 default:
  return nil
 }
}

方言细节

semver 方言

semver 用于 Fabric 模组与 MCDR 插件的依赖区间声明,底层基于 Masterminds/semver/v3

  • 相关实现:dependency/version_semver.go:10dependency/version_range_semver.go

版本格式

semver 使用 MAJOR.MINOR.PATCH[-prerelease]

输入字符串 解析结果 说明
1.20.4 SemverVersion{Major:1, Minor:20, Patch:4} 标准三段版本
2.0.0-alpha.1 SemverVersion{Major:2, Minor:0, Patch:0, Prerelease:"alpha.1"} 含预发布标签

区间运算符(MCDR / DialectNpmSemver

参见 dependency/version_range_dialect.go:14

运算符 示例 展开结果
>= >=1.0.0 version >= 1.0.0
<= <=2.0.0 version <= 2.0.0
> >1.0.0 version > 1.0.0
< <2.0.0 version < 2.0.0
= / == =1.5.0 version == 1.5.0
^(caret) ^1.2.0 >= 1.2.0, < 2.0.0(锁定主版本)
~(tilde) ~1.2.3 >= 1.2.3, < 1.3.0(锁定主版本.次版本)
通配符 1.x1.2.* >= 1.0.0, < 2.0.0

多个 token 以空格分隔时表示 AND:

>=1.0.0 <2.0.0   ->   version >= 1.0.0 AND version < 2.0.0
// ../lucy/dependency/version_range_semver.go:25-49
func parseMcdrSemverRange(raw string) types.VersionConstraintExpression {
 raw = strings.TrimSpace(raw)
 if raw == "" || isWildcardToken(raw) {
  return nil
 }

 tokens := strings.Fields(raw)
 if len(tokens) == 0 {
  return nil
 }

 andConstraints := make([]types.VersionConstraint, 0, len(tokens))
 for _, token := range tokens {
  constraints, ok := parseMcdrSemverCriterion(token)
  if !ok {
   return nil
  }
  andConstraints = append(andConstraints, constraints...)
 }

 if len(andConstraints) == 0 {
  return nil
 }
 return types.VersionConstraintExpression{andConstraints}
}

Fabric 扩展(DialectFabricSemver

在上述基础上,Fabric 方言额外支持:

  • || 表示 OR。
  • ^ 固定为“锁定主版本”模式(不采用 npm 的 0.x 特殊处理)。

参见 dependency/version_range_dialect.go:65dependency/version_range_semver.go:336

输入/输出示例

输入:^2.0.0
展开:>= 2.0.0 AND < 3.0.0

输入:~1.4
展开:>= 1.4.0 AND < 2.0.0   (minor 未指定时升至下一 major)

输入:1.0.0 - 2.0.0   (连字符区间,Fabric 方言)
展开:>= 1.0.0 AND <= 2.0.0

参见 dependency/version_range_semver.go:336(caret 展开)和 dependency/version_range_semver.go:372(tilde 展开)。

maven 方言

maven 用于 Forge/NeoForge 的依赖区间(mods.tomlversionRange 字段),遵循 Apache Maven 区间语法

  • 相关实现:dependency/version_range_maven.go

Warning

Forge 文档将 versionRange 指向 Maven 区间语法。Lucy 的 parseMavenSingleRange 内部复用 parseSemver 解析版本号,因此 maven 方言实际上要求版本号本身可被 semver 解析;若不兼容,约束会被静默丢弃。

版本与区间格式

版本号本身采用 semver 形态(如 1.0.0),但区间语法使用括号/方括号:

语法 含义 示例
[a,b] a <= version <= b(两端闭合) [1.0.0,2.0.0]
[a,b) a <= version < b(左闭右开) [1.0.0,2.0.0)
(a,b] a < version <= b(左开右闭) (1.0.0,2.0.0]
(a,b) a < version < b(两端开放) (1.0.0,2.0.0)
[a,) version >= a(无上界) [1.0,)
[a] version == a(精确匹配) [1.5.0]
>=a version >= a(简写) >=1.0.0

多个括号区间在括号外以 , 分隔表示 OR。注意:, 在括号内是区间分隔,在括号外才是 OR 分隔。

参见 dependency/version_range_maven.go:35splitMavenUnions)。

// ../lucy/dependency/version_range_maven.go:9-33
func parseMavenRange(raw string) types.VersionConstraintExpression {
 raw = strings.TrimSpace(raw)
 if raw == "" || raw == "*" || strings.EqualFold(raw, "none") {
  return nil
 }

 parts := splitMavenUnions(raw)
 if len(parts) == 0 {
  parts = []string{raw}
 }

 result := make(types.VersionConstraintExpression, 0, len(parts))
 for _, part := range parts {
  constraints := parseMavenSingleRange(strings.TrimSpace(part))
  if len(constraints) == 0 {
   continue
  }
  result = append(result, constraints)
 }

 if len(result) == 0 {
  return nil
 }
 return result
}
// ../lucy/dependency/version_range_maven.go:66-166
func parseMavenSingleRange(raw string) []types.VersionConstraint {
 if raw == "" {
  return nil
 }

 if strings.HasPrefix(raw, "^") || strings.HasPrefix(raw, "~") {
  // Not part of Maven version range syntax.
  // Forge docs (1.21.x, checked 2026-02-24) point dependency versionRange
  // to Maven Version Range syntax, which only defines bracket/parenthesis
  // ranges and basic comparison operators.
  // References:
  //   - https://docs.minecraftforge.net/en/latest/gettingstarted/modfiles/
  //   - https://maven.apache.org/enforcer/enforcer-rules/versionRanges.html
  return nil
 }

 if len(raw) >= 2 {
  left := raw[0]
  right := raw[len(raw)-1]
  if (left == '[' || left == '(') && (right == ']' || right == ')') {
   body := strings.TrimSpace(raw[1 : len(raw)-1])
   if strings.Contains(body, ",") {
    bounds := strings.SplitN(body, ",", 2)
    lowerToken := strings.TrimSpace(bounds[0])
    upperToken := strings.TrimSpace(bounds[1])
    out := make([]types.VersionConstraint, 0, 2)
    if lowerToken != "" {
     lower := parseSemver(types.RawVersion(lowerToken))
     if lower == nil {
      return nil
     }
     op := types.OpGt
     if left == '[' {
      op = types.OpGte
     }
     out = append(
      out,
      types.VersionConstraint{Value: lower, Operator: op},
     )
    }
    if upperToken != "" {
     upper := parseSemver(types.RawVersion(upperToken))
     if upper == nil {
      return nil
     }
     op := types.OpLt
     if right == ']' {
      op = types.OpLte
     }
     out = append(
      out,
      types.VersionConstraint{Value: upper, Operator: op},
     )
    }
    if len(out) == 0 {
     return nil
    }
    return out
   }

   // Exact value form: [1.0]
   if left == '[' && right == ']' && body != "" {
    v := parseSemver(types.RawVersion(body))
    if v == nil {
     return nil
    }
    return []types.VersionConstraint{
     {
      Value: v, Operator: types.OpEq,
     },
    }
   }
   return nil
  }
 }

 operator := types.OpEq
 versionToken := raw
 for _, op := range []struct {
  prefix   string
  operator types.VersionOperator
 }{
  {prefix: ">=", operator: types.OpGte},
  {prefix: "<=", operator: types.OpLte},
  {prefix: "!=", operator: types.OpNeq},
  {prefix: ">", operator: types.OpGt},
  {prefix: "<", operator: types.OpLt},
  {prefix: "=", operator: types.OpEq},
 } {
  if strings.HasPrefix(raw, op.prefix) {
   operator = op.operator
   versionToken = strings.TrimSpace(strings.TrimPrefix(raw, op.prefix))
   break
  }
 }
 v := parseSemver(types.RawVersion(versionToken))
 if v == nil {
  return nil
 }
 return []types.VersionConstraint{{Value: v, Operator: operator}}
}

输入/输出示例

输入:[1.20.0,2.0.0)
展开:version >= 1.20.0 AND version < 2.0.0

输入:[1.5.0]
展开:version == 1.5.0

输入:[1.0,)
展开:version >= 1.0.0

Warning

maven 方言不支持 ^ / ~,相关输入会直接返回 nildependency/version_range_maven.go:71),即约束被丢弃且不报错。

minecraft 方言

minecraft 方言用于表示 Minecraft 游戏本体版本。它与 semver/maven 的本质差异是:这是一个版本解析方言,而不是区间声明方言。Lucy 用该方言把游戏版本文本转换成可比较对象,用于判定 mod 版本与游戏版本的兼容关系。

  • 相关实现:dependency/version_minecraft_parse.godependency/version_minecraft_types.go

Lucy 支持两套命名体系。

正式版(Release)

命名体系 版本格式 示例 说明
Pre-2026(旧体系) MAJOR.MINOR[.HOTFIX][-preN/-rcN] 1.20.41.21-pre31.21-rc1 Year < 26
Post-2026(新体系) YEAR.UPDATE[.HOTFIX][-pre-N/-rc-N] 26.126.1.126.1-pre-1 Year >= 26,连字符风格变化

参见 dependency/version_minecraft_parse.go:92parseMinecraftRelease)。

快照版(Snapshot)

命名体系 版本格式 示例 说明
Pre-2026 快照 YYwWWa(年+周+序号字母) 25w14a24w46b 字母范围 a-h
Post-2026 快照 YEAR.UPDATE-snapshot-N 26.1-snapshot-1 新体系快照

参见 dependency/version_minecraft_parse.go:10parseMinecraftSnapshot)。

跨命名体系比较限制

MinecraftVersion.Compare() 对 Post26 与 Pre-26 的比较返回 (0, false)

也就是说,新旧体系版本对象不可互相比大小。参见 dependency/version_minecraft_types.go:139

输入/输出示例

输入:1.20.1
解析:MinecraftVersion{Year:1, Update:20, Hotfix:1, Post26:false}

输入:25w14a
解析:Pre26MinecraftSnapshotVersion{Year:25, WorkCycle:14, Index:'a'}

输入:26.1-snapshot-1
解析:Post26MinecraftSnapshotVersion{Year:26, Update:1, SnapshotN:1}

方言推断与 ParseRange

Lucy 通过 InferRangeDialect(platform) 自动映射平台到方言(dependency/version_range_dialect.go:30):

Platform 推断方言
PlatformMCDR DialectNpmSemver
PlatformFabric DialectFabricSemver
PlatformForge DialectMavenRange
PlatformNeoforge DialectMavenRange
其他 DialectUnknown(返回 nil

ParseRange(raw, dialect, scheme) 是统一区间入口,按方言把原始字符串展开为 VersionConstraintExpressiondependency/version_range_dialect.go:48)。

Warning

当前三种区间方言都要求 scheme == types.Semver。若传入 MinecraftReleaseMinecraftSnapshotParseRange 在 scheme 校验后返回 nildependency/version_range_dialect.go:58-80)。Minecraft 版本对象仅用于内部兼容性判断,不参与区间匹配流程。

已知限制

DL-1:方言间不兼容

字段 内容
影响范围 跨平台依赖解析
当前状态 ⚠️ 设计限制
证据路径 dependency/version_range_dialect.go:56

不同方言的约束表达式不可互换。Forge(maven)中的 [1.0,2.0) 与 Fabric(semver)中的 >=1.0.0 <2.0.0 虽语义接近,但解析器彼此独立;若传错方言,约束会被丢弃而非报错。

DL-2:maven 方言不支持 ^~

字段 内容
影响范围 Forge/NeoForge 依赖区间
当前状态 ✅ 有意设计
证据路径 dependency/version_range_maven.go:71

Forge 官方文档要求 versionRange 使用 Maven 区间语法,而该语法不包含 ^/~。因此代码显式拒绝这两类前缀并返回 nil,以保持与文档一致。

DL-3:Minecraft 跨命名体系不可比较

字段 内容
影响范围 Minecraft 版本排序与兼容性判断
当前状态 ⚠️ 已知行为
证据路径 dependency/version_minecraft_types.go:139

Pre-2026(如 1.20.1)与 Post-2026(如 26.1)内部结构不同,Compare() 在跨体系比较时返回 (0, false),不会给出大小关系。

DL-4:minecraft 方言无独立区间语法

字段 内容
影响范围 游戏版本范围声明
当前状态 ⚠️ 设计限制
证据路径 dependency/version_range_dialect.go:48

Warning

MinecraftRelease / MinecraftSnapshot 当前不通过 ParseRange 参与区间匹配。游戏版本仅用于解析与内部比较;mod 的游戏版本兼容性主要依赖上游 API 提供的结果。

DL-5:解析失败时静默丢弃

字段 内容
影响范围 所有方言
当前状态 ⚠️ 已知行为
证据路径 dependency/version_parse.go:16

当版本字符串无法解析时,Parse()ParseRange() 都返回 nil 而非错误。无效约束会被静默忽略,不会中断流程,这会让最终约束可能比预期更宽松。

相关页面

参考文件

以下源码文件是本页内容的主要依据(基线 commit 86d3480cffa821b9b9c0747263a637b2973a7366):

  • dependency/version_parse.go:16 - Parse() 主入口,按 scheme 分发解析
  • dependency/version_range_dialect.go:10 - VersionRangeDialect 枚举定义
  • dependency/version_range_dialect.go:30 - InferRangeDialect():平台 -> 方言映射
  • dependency/version_range_dialect.go:48 - ParseRange():统一区间解析入口
  • dependency/version_range_semver.go:25 - parseMcdrSemverRange():MCDR semver 方言解析
  • dependency/version_range_semver.go:131 - parseSemverRange():Fabric semver 方言解析
  • dependency/version_range_semver.go:336 - parseCaretRangeFromSemver()^ 展开逻辑
  • dependency/version_range_semver.go:372 - parseTildeRangeFromSemver()~ 展开逻辑
  • dependency/version_range_semver.go:400 - parseXRange():通配符 x/* 展开逻辑
  • dependency/version_range_maven.go:9 - parseMavenRange():maven 方言主入口
  • dependency/version_range_maven.go:35 - splitMavenUnions():OR 分段解析
  • dependency/version_range_maven.go:66 - parseMavenSingleRange():单区间解析
  • dependency/version_semver.go:10 - SemverVersion 类型与 parseSemver()
  • dependency/version_minecraft_parse.go:10 - 快照版本解析(新旧体系分支)
  • dependency/version_minecraft_parse.go:92 - 正式版本解析(新旧体系分支)
  • dependency/version_minecraft_types.go:20 - Pre26MinecraftSnapshotVersion 类型
  • dependency/version_minecraft_types.go:56 - Post26MinecraftSnapshotVersion 类型
  • dependency/version_minecraft_types.go:94 - MinecraftVersion 类型与 Compare()
⚠️ **GitHub.com Fallback** ⚠️