xgo-dev / xgo-dev/llar

版本管理(稿2)

Open
#22 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
5
Forks
4
Avg merge
2d 13h
Merged PRs (30d)
22

Description

背景

我们为什么要重新建立一个版本管理系统?

直接复用别人的不可以吗?

答案是不可以。因为,LLAR目标是任意语言的资产管理。

不同语言之间,往往管理体系不一样,甚至压根没有管理体系(C/C++),我们需要为他们提供一套统一,标准的版本管理方案。

这样我们才能彻底消除不同语言的隔阂,实现统一的包资产管理。

目标

  1. 尽量不破坏原库版本管理体系前提下,为其引入LLAR版本管理
  2. 统一,标准版本管理

版本管理

结构设计

综上,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.modgo.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方式,然而这类解决方案看起来优雅,却有相当多的问题:

  1. 对历史Formula进行维护会产生诸多问题(这可以参考LLPkg历史版本维护章节)
  2. 版本映射问题
  3. 容易产生大量的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/cJSON1.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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.