版本管理(稿2)
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 5
- Forks
- 4
- Avg merge
- 2d 13h
- Merged PRs (30d)
- 22
Description
背景
我们为什么要重新建立一个版本管理系统?
直接复用别人的不可以吗?
答案是不可以。因为,LLAR目标是任意语言的资产管理。
不同语言之间,往往管理体系不一样,甚至压根没有管理体系(C/C++),我们需要为他们提供一套统一,标准的版本管理方案。
这样我们才能彻底消除不同语言的隔阂,实现统一的包资产管理。
目标
- 尽量不破坏原库版本管理体系前提下,为其引入LLAR版本管理
- 统一,标准版本管理
版本管理
结构设计
综上,LLAR版本号由两部分构成,如下
type PackageVersion {
// 原版本号
Version string
}
自定义比较
默认情况下,LLAR使用GNU Coreutils的sort -V算法,下称GNU算法,其算法来源为Deb版本比较。
GNU的版本比较为我们提供了一套尽可能通用的版本比较算法,然而C/C++并无明确版本规范,GNU的算法仅仅是竭尽所能去比较(Best Effort),并不适合所有,其规则就不适用于semver(因为GNU算法并没有先行版本号规则)
如果维护者发现该包需要自定义比较算法,我们为自定义比较设计了一套规则
由于配方会因为上游版本号变动导致配方出现版本号问题,但是如果存在自定义比较逻辑,那我们首先得拿到这个比较逻辑方法,再比较配方版本,选择合适配方。然而最开始设计将这个比较逻辑放到配方中,导致其跟着出现了版本号,会导致我们无法实际拿到这个比较方法。
所以,我们将其重新拆出来一个独立文件,后缀为_cmp.gox,必须存在于当前包根目录下,例子如下:
DaveGamble
└── cJSON
├── 1.0.0
│ ├── cJSON_llar.gox
│ └── versions.json
├── 1.5.0
│ ├── cJSON_llar.gox
│ └── versions.json
├── 2.0.0
│ ├── cJSON_llar.gox
│ └── versions.json
├── cJSON_cmp.gox
├── go.mod
└── go.sum
这个文件可以不提供,当不提供时候,默认使用GNU算法。当提供时,则使用该自定义比较。而go.mod和go.sum可以不提供,当不提供,LLAR会自动根据import生成go.mod和下载对应的Go module(得益于ixgo)
维护者还需要提供以下接口:
type ComparableVersion interface {
// 当 a > b,返回 1, a == b, 返回 0, a < b,返回 -1
Compare(a, b PackageVersion) int
}
由于XGO Classfile最佳实践是避免用户写函数声明,使用闭包函数替代,所以上述接口定义将变成如下
type ComparableVersion interface {
Compare(comparator func (a, b PackageVersion) int)
}
一个符合规范的自定义比较大概如下:
import (
semver "github.com/Masterminds/semver/v3"
)
compare (a, b) => {
echo "a: ${a.Ver} b: ${b.Ver}"
v1 := semver.NewVersion(a.Ver)!
v2 := semver.NewVersion(b.Ver)!
return v1.Compare(v2)
}
配方版本管理
在中心化配方管理仓库下,其存储结构如下
DaveGamble/
└── cJSON/
├── go.mod
├── go.sum
├── 1.x/
│ └── CJSON_llar.gox
└── 2.x/
└── CJSON_llar.gox
formula命名格式:由于Classfile生成的结构体根据文件名称进行生成对应的结构体,所以文件名应该给对应的命名空间首字母进行大写处理,例如 cJSON => CJSON
目录规范:目录名称应该与配方FromVersion保持一致,例如
CJSON_llar.gox:
fromVersion "1.0.0"
其目录结构为:
DaveGamble/
└── cJSON/
├── go.mod
├── go.sum
└── 1.0.0/
└── CJSON_llar.gox
这么做目的只是便于管理。
实际实现是根据依赖版本来查找对应的配方
为什么要在采用版本号为目录的方式
在LLPkg中,我们遇到了类似的问题,只不过在LLPkg的解决方案是使用Go Nested Module方式,然而这类解决方案看起来优雅,却有相当多的问题:
- 对历史Formula进行维护会产生诸多问题(这可以参考LLPkg历史版本维护章节)
- 版本映射问题
- 容易产生大量的tags
我们吸取LLPkg教训后,决定不再采取Go Nested Module设计
依赖管理
类似于Go Module,LLAR有类似于go.mod依赖管理文件
其设计如下,文件名为: versions.json
文件格式如下:
{
"name": "DaveGamble/cJSON",
"deps": {
"1.0.0": [{
"name": "madler/zlib",
"version": "1.2.1"
}],
"1.2.0": [{
"name": "madler/zlib",
"version": "1.2.3"
}]
}
}
结构化定义如下:
type Dependency struct {
PackageName string `json:"name"`
Version string `json:"version"`
}
type PackageDependencies struct {
PackageName string `json:"name"`
Dependencies []Dependency `json:"deps"`
}
其中Version目前为依赖项版本号,后面会兼引入Version Range(不确定,可能会和我们高保真设计冲突?)
顺序
Package依赖顺序为 有向图拓扑顺序 摊平后的顺序,具体看MVS Buildlist
配方版本选择
通常来说,LLAR版本号并不是确定的,因为LLAR是一个服务型工具,具体要求什么版本往往是用户指定的
经过讨论,我们引入了 fromVersion 机制
例如用户需要 DaveGamble/cJSON 的 1.7.18 版本
其选择流程如下:
graph TD
A[DaveGamble/cJSON 1.7.18] --> B{查找 fromVersion <= 1.7.18 配方}
B -- 找到配方 --> C{调用onRequire回调函数}
B -- 否 --> D[报错退出]
C --> E[解析 versions.json,发现依赖 madler/zlib ]
E --> F{查找 fromVersion <= 1.1.0 配方}
F -- 找到配方 --> G[... 完成依赖有向图构建]
F -- 否 --> H[报错退出]
上述选择由依赖管理模块自动完成,得益于ixgo允许动态导入,我们可以轻松实现这样的操作
下面则演示一下选择过程:
用户需要DaveGamble/cJSON的1.7.18版本
DaveGamble
└── cJSON
├── 1.0.0 // fromVersion: 1.0.0
│ ├── CJSON_llar.gox
│ └── versions.json
├── 1.5.0 // fromVersion: 1.5.0
│ ├── CJSON_llar.gox
│ └── versions.json
├── 2.0.0 // fromVersion: 2.0.0
│ ├── CJSON_llar.gox
│ └── versions.json
├── CJSON_version.gox
├── go.mod
└── go.sum
其过程如下:
graph TD
A[随机选择一个配方] --> B{DaveGamble/cJSON 1.5.0 <= 1.7.18 ?}
B -- 是 --> C[记录当前配方信息]
C --> D
B -- 否 --> D{DaveGamble/cJSON 2.0.0 <= 1.7.18 ?}
D -- 是 --> E{是否最优配方?}
E -- 是 --> F[记录当前配方信息]
E -- 否 --> G
F --> G
D -- 否 --> G{DaveGamble/cJSON 1.0.0 <= 1.7.18 ?}
G -- 是 --> H{是否最优配方?}
H -- 是 --> I[记录当前配方信息]
I --> J
H -- 否 --> J[结束]
自定义依赖管理
在设计这套版本管理方案的时候,我们一开始太过于理想化,没有考虑到库本身自带版本依赖的问题。
什么意思呢,例如该库原本使用了类似于conan,ninja类似包管理工具的时候,我们其实可以从这些工具中读取准确和最新的依赖信息。这可以大幅度减少维护者维护工作。
在配方设计中,我们提供了onRequire方法供用户传入一个自定义依赖管理的回调方式。
如果用户不提供这个方法,默认读取 versions.json 中的信息
以下是onRequire使用方式:
onRequire deps => {
graph := readDepsFromNinja()?
graph.visit((parent, dep) => {
deps.require(parent, dep)
})
}
onRequire传入从versions.json解析得出的有向图,维护者可以根据从ninja之类的包管理工具解析出依赖信息,并进行替换,随后MVS算法会重新算出最佳构建路径
其中onRequire传入一个deps.Graph参数,其定义如下
type Graph interface {
// 修改 packageName 的 依赖 为 deps
Require(packageName string, deps []Dependency)
// 获取 版本为version的packageName 的依赖
RequiredBy(packageName string, version version.Version) ([]Dependency, bool)
}
versions.json 自动生成
我们觉得让用户去手动填写这样的配置是复杂的,类似于Go module,我们提供了类似的cli去管理
#17
版本选择设计
#14
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read this proposal together with issues #17 and #14 first; no source file or test entry point is identified. Before implementation, clarify the version-selection, dependency graph, comparator, and CLI scope, then define acceptance tests for recipe lookup and dependency resolution.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- devtools
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100