dependency resolution - mclucy/lucy GitHub Wiki
Lucy 在处理不同平台的 mod/插件依赖时,需要同时支持三套互不相同的版本描述语法(版本方言,Version Dialect)。每套方言都对应特定生态与上游数据源,并使用各自的版本格式与区间表达方式。
Lucy 的版本处理由两个彼此独立、但由同一方言控制的阶段构成:
-
解析(Parse):把原始版本字符串(
RawVersion)转换为可比较对象(ComparableVersion)。 -
区间匹配(Range):把依赖约束(如
>=1.0.0 <2.0.0)解析为VersionConstraint集合,再与具体版本进行评估。
方言选择由包来源平台(Platform)决定。
| 维度 | semver(npm/Fabric/MCDR) | maven | minecraft |
|---|---|---|---|
| 应用平台 | Fabric、MCDR | Forge、NeoForge | 游戏本体版本判断 |
| 版本格式示例 |
1.2.3、2.0.0-alpha.1
|
1.0.0(版本号本身同 semver) |
1.20.1、25w14a、26.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 用于 Fabric 模组与 MCDR 插件的依赖区间声明,底层基于 Masterminds/semver/v3。
- 相关实现:
dependency/version_semver.go:10、dependency/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"} |
含预发布标签 |
参见 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.x、1.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 方言额外支持:
-
||表示 OR。 -
^固定为“锁定主版本”模式(不采用 npm 的0.x特殊处理)。
参见 dependency/version_range_dialect.go:65、dependency/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 用于 Forge/NeoForge 的依赖区间(mods.toml 的 versionRange 字段),遵循 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:35(splitMavenUnions)。
// ../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 方言不支持 ^ / ~,相关输入会直接返回 nil(dependency/version_range_maven.go:71),即约束被丢弃且不报错。
minecraft 方言用于表示 Minecraft 游戏本体版本。它与 semver/maven 的本质差异是:这是一个版本解析方言,而不是区间声明方言。Lucy 用该方言把游戏版本文本转换成可比较对象,用于判定 mod 版本与游戏版本的兼容关系。
- 相关实现:
dependency/version_minecraft_parse.go、dependency/version_minecraft_types.go
Lucy 支持两套命名体系。
| 命名体系 | 版本格式 | 示例 | 说明 |
|---|---|---|---|
| Pre-2026(旧体系) | MAJOR.MINOR[.HOTFIX][-preN/-rcN] |
1.20.4、1.21-pre3、1.21-rc1
|
Year < 26 |
| Post-2026(新体系) | YEAR.UPDATE[.HOTFIX][-pre-N/-rc-N] |
26.1、26.1.1、26.1-pre-1
|
Year >= 26,连字符风格变化 |
参见 dependency/version_minecraft_parse.go:92(parseMinecraftRelease)。
| 命名体系 | 版本格式 | 示例 | 说明 |
|---|---|---|---|
| Pre-2026 快照 |
YYwWWa(年+周+序号字母) |
25w14a、24w46b
|
字母范围 a-h
|
| Post-2026 快照 | YEAR.UPDATE-snapshot-N |
26.1-snapshot-1 |
新体系快照 |
参见 dependency/version_minecraft_parse.go:10(parseMinecraftSnapshot)。
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}
Lucy 通过 InferRangeDialect(platform) 自动映射平台到方言(dependency/version_range_dialect.go:30):
| Platform | 推断方言 |
|---|---|
PlatformMCDR |
DialectNpmSemver |
PlatformFabric |
DialectFabricSemver |
PlatformForge |
DialectMavenRange |
PlatformNeoforge |
DialectMavenRange |
| 其他 |
DialectUnknown(返回 nil) |
ParseRange(raw, dialect, scheme) 是统一区间入口,按方言把原始字符串展开为 VersionConstraintExpression(dependency/version_range_dialect.go:48)。
Warning
当前三种区间方言都要求 scheme == types.Semver。若传入 MinecraftRelease 或 MinecraftSnapshot,ParseRange 在 scheme 校验后返回 nil(dependency/version_range_dialect.go:58-80)。Minecraft 版本对象仅用于内部兼容性判断,不参与区间匹配流程。
| 字段 | 内容 |
|---|---|
| 影响范围 | 跨平台依赖解析 |
| 当前状态 | |
| 证据路径 | dependency/version_range_dialect.go:56 |
不同方言的约束表达式不可互换。Forge(maven)中的 [1.0,2.0) 与 Fabric(semver)中的 >=1.0.0 <2.0.0 虽语义接近,但解析器彼此独立;若传错方言,约束会被丢弃而非报错。
| 字段 | 内容 |
|---|---|
| 影响范围 | Forge/NeoForge 依赖区间 |
| 当前状态 | ✅ 有意设计 |
| 证据路径 | dependency/version_range_maven.go:71 |
Forge 官方文档要求 versionRange 使用 Maven 区间语法,而该语法不包含 ^/~。因此代码显式拒绝这两类前缀并返回 nil,以保持与文档一致。
| 字段 | 内容 |
|---|---|
| 影响范围 | Minecraft 版本排序与兼容性判断 |
| 当前状态 | |
| 证据路径 | dependency/version_minecraft_types.go:139 |
Pre-2026(如 1.20.1)与 Post-2026(如 26.1)内部结构不同,Compare() 在跨体系比较时返回 (0, false),不会给出大小关系。
| 字段 | 内容 |
|---|---|
| 影响范围 | 游戏版本范围声明 |
| 当前状态 | |
| 证据路径 | dependency/version_range_dialect.go:48 |
Warning
MinecraftRelease / MinecraftSnapshot 当前不通过 ParseRange 参与区间匹配。游戏版本仅用于解析与内部比较;mod 的游戏版本兼容性主要依赖上游 API 提供的结果。
| 字段 | 内容 |
|---|---|
| 影响范围 | 所有方言 |
| 当前状态 | |
| 证据路径 | 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()