公告

FujoOS公布日延期,以仓库发布为准

文档 语言 Lompi · 包管理器

Lompi · 包管理器

Lompi 是库系统的工具,也是它的第一个独立实现:一个用 Loment 写的程序——入口加七个模块编成一个可执行文件,跑起来不需要 Python。它自己读源码里的 use 找出依赖,自己算身份,自己发锁。

一句话说清它和别的包管理器差在哪:pip 里最难的是「挑版本」,Lompi 里最难的是「把实例身份算准」。选版只剩一条规则——版本最大;而身份要对得起「同一份源码绑到不同依赖上就该是两个东西」这件事。

仓库长什么样

text 仓库布局
<store>/
  <名字>/
    <版本>/
      <名字>.lomt     ← 入口。文件名必须与包名一致
      *.lomt          ← 包内其余模块
      pkg.lomp        ← 可选。只放两个给人看的标签

pkg.lomp 是可选的,写成普通 Loment 源码。deps/ 里的东西不算本包的源码——否则会凭空多出依赖边,并把 vendored 的副本算进身份。

两种边,只有一种算依赖

写法 含义
use 名字 指向另一个包。解析、算身份、进锁,都看它。
use "io.lomt" 包内引用。列出来是为了看得见,但那个文件本来就在本包源码里,所以不参与解析,也不进哈希
一条边界: 路径边如果指向包外use "../other/x.lomt"),Lompi 不跟踪。跨包引用请一律写成 use 名字,并让那个名字落在仓库里。

命令全表

命令 作用
lompi install <名字> [--into DIR] [--apply] [--from-lock F]取件 / 校验 / 安装。默认装进全局 store--apply 才真写盘
lompi check <目录>这个目录是不是一个合法的 Loment 库
lompi fetch [--registry URL]打出网址库的下载计划
lompi config显示全局根与网址库(含命中了哪条规则)
lompi version版本
lompi index <store>仓库里有哪些包
lompi show <store> <名字[@版本]>元数据 / use 边 / 身份
lompi tree <store> <名字[@版本]>依赖树
lompi resolve <store> <名字[@版本]>打出锁(身份钉死的清单)
lompi verify <store> <名字[@版本]> <锁文件>重算并逐字节比对
lompi plan <store> <名字[@版本]> [--into D]闭包与物化计划
lompi hash <包目录>单个包的自身源码哈希

<名字[@版本]> 的写法:mathutil 意思是「版本最大的那个」,mathutil@0.1.0 是钉死版本。版本按 . 分段做数值比较,所以 0.10.0 大于 0.9.0

下面每一条都是在真实仓库上跑出来的,不是示意。

console 清点仓库
$ lompi index fixture/store
5 package(s) in store
geom 1.0.0 06b4cc0d3b1f136f
mathutil 0.1.0 53ce217a387ed58a
mathutil 0.2.0 c5f88a0519c41888
multi 2.0.0 8326d0125e3b111d
util 0.3.0 e0e90317698ff804

每行是「名字 版本 身份前 16 位」。同一个名字出现两次是正常的——那是两个实例(多版本共存),不是重复;第一行那个 5 package(s) 数的是实例数。

console 看一个包的底细
$ lompi show fixture/store multi
name:    multi
version: 2.0.0
id:      8326d0125e3b111d51d050e49395400524a18ddc31eb730f7afbc9cb6a4955fb
dir:     fixture/store/multi/2.0.0
1 use edge(s):
  name util

id实例身份(64 位十六进制)。把它和别的构建里同一个包的 id 比一比,就知道两边是不是同一个东西。

console 依赖树 / 发锁 / 验锁
$ lompi tree fixture/store multi
multi@2.0.0 8326d0125e3b
  util@0.3.0 e0e90317698f

$ lompi resolve fixture/store multi
# lompi lock v1
multi 2.0.0 8326d0125e3b111d51d050e49395400524a18ddc31eb730f7afbc9cb6a4955fb
util 0.3.0 e0e90317698ff804db92a01bce6b7a965e9e679eb6bb80b25f02c2d05207b041

$ lompi verify fixture/store multi lompi.lock
[OK] lompi: 锁与仓库一致 (169 bytes)

缩进表示层次;菱形依赖(两个包依赖同一个)第二次出现时会标 (already shown),不重复展开——那是同一个实例,展开两次会骗人。锁是身份清单而不是版本清单;验锁会把整棵树重算一遍再逐字节比对,拿错一份锁得到的是 [DIFF] 锁与仓库不一致 并以 1 退出,不是「差不多能用」。

取件那一半:全局根、网址库、install

上面那七条是只读的。下面这一半会动你的磁盘,所以先说清楚它从哪儿拿、往哪儿放。

全局根是「推」出来的,不是读环境变量

因为没法读:实测 /proc/self/environ 在 PE 垫片上打不开(它只合成 cmdline),语言也没有 getenv。但 argv[0] 是执行文件的全路径,所以从它反推是可行的。规则按可靠性排序,命中哪条 lompi config 会打出来——推错了要看得见,不能静默走错目录。

console 全局根与网址库
$ lompi config
version:  0.1.0
exe:      D:\Dev\Lolment-ku\lompi
global:   D:\Dev\Lolment-ku\.lompi
  rule:   fallback: <exe dir>\.lompi (exe is not under AppData or Users)
store:    D:\Dev\Lolment-ku\.lompi\store
cache:    D:\Dev\Lolment-ku\.lompi\cache
conf:     D:\Dev\Lolment-ku\lompi\lompi.conf  (absent)
registry: (not set) -- install will use the global store

网址库是一个 git 仓库

布局和 store 完全一样:<名字>/<版本>/*.lomt。它的地址写在执行文件旁边lompi.conf 里,内容是 Loment 源码,形状与 pkg.lomp 一样——不新造格式,读它用的就是读清单那套。

loment lompi.conf
module conf

pub fn registry() -> str {
    return "https://example.invalid/loment-registry.git";
}

fetch 不联网,install 才动盘

Lompi 自己做不到联网。PE 目标上只有 8 个 syscall,里面没有 socket;垫片也没有 ws2_32 / winhttp。所以 fetch 给出的是计划,由 shell 或 CI 执行——与「plan 不建目录」是同一条边界,只是这次让出去的是 git。

console fetch 打出什么
$ lompi fetch --registry https://example.invalid/loment-registry.git
# lompi fetch plan (lompi cannot do network on PE -- no socket in the 8 syscalls)
# registry: https://example.invalid/loment-registry.git  (from --registry)
mkdir -p <global>\cache
git clone --depth 1 https://example.invalid/loment-registry.git <global>\cache\loment-registry
# to update later:  git -C <global>\cache\loment-registry pull

克隆下来之后,install缓存里的网址库取件;没配网址库,就从全局 store 取。取到之后先校验——不是合法的 Loment 库就拒装,并且说清是哪一条不过。

默认装进全局 store--into DIR 装到 DIR\<名字>\(项目内)。不带 --apply 就只出计划,要它真写盘得显式加。--from-lock F 则只装 F 钉死的那些实例,对不上就拒。

check:一个目录算不算合法的 Loment 库

装之前校验的就是这一组。它逐条报,过不了的写 [BAD]:

console check 通过 / 不通过
$ lompi check fixture/store/mathutil/0.1.0
[ok] source files: 1
[ok] package name: mathutil
[ok] every module name matches its file name
[ok] no pkg.lomp (optional)
[OK] lompi: this is a valid Loment library

$ lompi check fixture/store_broken
[ok] source files: 0
[BAD] no *.lomt in this directory
[BAD] lompi: 1 check(s) failed

身份怎么算

text 实例身份
id(P) = sha256( 自身源码 ⊕ 每条 name 边: 边名 0x00 子实例身份 0x00 )

自身源码 = 包内 *.lomt(排除 pkg.lompdeps/),按文件名排序后逐文件喂「名字 0x00 内容 0x00」;边也按名字排序后才进哈希。所以换个文件顺序写 use 不会改变身份——目录返回顺序不保证,不排序就没有稳定身份。

这条公式有一个必须接受的后果,把它单独放在这里:

text 同一份源码,两个身份
geom 的源码逐字节相同, 但落在两个仓库里
  仓库 A 有 mathutil 0.1.0 / 0.2.0  →  geom id = 06b4cc0d3b1f…  (绑 0.2.0)
  仓库 B 只有 mathutil 0.1.0        →  geom id = 0f4bb3784d09…  (绑 0.1.0)

两个 id 不同是对的。同一份源码绑到不同依赖上,语义就不同;把它们合并才是有害的。这正是「只看自身源码是错的」那句话的具体样子。

算的时候用的是不动点迭代,不是递归。递归每一层都要留住自己的边表,而这个程序的内存是一块固定工作区(语言没有动态分配),递归就得给每层切一块缓冲,于是深度上限被写死进布局。换成「反复扫到没有新的包可以定身份为止」就没这个问题——代价是最坏 O(n²),而一个仓库的包数很少(上限 512),这个代价换得起。

环也因此自然暴露:A→B→A,两边都永远「还没准备好」,循环结束后它们就是剩下的那些。身份算不出来是响亮的——环与缺依赖各有独立的状态位与消息,不混成一句「失败」。

依赖从源码读,不从清单读

因为模型是「依赖就是源码里的 use」,包管理器必须自己去读源码。参考实现借用了编译器的前端 AST;Lompi 是一个独立程序,借不到,所以它做的是一个够用的词法扫描——只认顶层 use,不建语法树。

扫描要处理三种假 use,否则会凭空多出依赖边,而那是静默的错:行注释里的 // use foo、块注释里的 /* use foo */、以及字符串里的 "use foo"(含反斜杠转义)。

一个明确的边界: 只认行首use(前面只允许空白)。语言里 use 只在顶层,而顶层语句在本项目里一行一条——这条规则够用,也不会把 x.use 之类的误认成边。真有跨行写的 use,这里会漏,所以 Lompi 会把「扫到的边」和「包目录里存在的模块」对一遍,不匹配就报出来,而不是默默少一条边。

两件被工具链逼出来的事

  • 为什么函数不返回结构,而是用一块工作区 —— 这些函数的结果是多块的(名字池 + 记录表 + 内容缓冲),而语言没有出参、也没有可变全局,包内的链接器又不支持按值传聚合。所以照仓库工具的成法:调用方给一块内存,本层按命名偏移使用,签名保持标量。这正是库系统里那两张真库的接口形状,不是 Lompi 自己的口味。
  • 为什么两层挤进同一个文件 —— 源码扫描(lex_*)与包 / 仓库模型(pkg_* / sto_*)本该是两层,但打包版编译器的传递导入深度上限是 8,而 Lompi 这条链正好 8 层(lompi → cli → idn → pkg → sha → dir → txt → sys)。再加一层,最底层的 sys_* 会整片报「未声明」,而且报错指向的层与真正的原因无关。分层没变,只是两层住在一个文件里。

第二条值得单独看一眼:它不是设计,是被工具链的护栏顶回来的一道痕迹。项目的做法是把原因写在文件头上,而不是假装那是自然的。

退出码与输出分家

含义
0成功(含 lompi help
1失败:仓库打不开 / 依赖成环 / 依赖找不到 / 没有这个包 / 锁与仓库不一致
2用法错:缺参数、不认识的命令、锁文件读不了

诊断走 stderr,正常输出走 stdout——所以 lompi resolve … > lompi.lock 得到的是干净的一份锁,不用过滤。

报错对照

你看到的 意思 / 怎么办
仓库里没有这个包名字或版本对不上。用 lompi index <store> 看目录名与版本
仓库里没有这个依赖: X某个包的 use X 在仓库里找不到 X。把 X 放进仓库,或改那个包
依赖成环 …A→B→A。拆环(这门语言在解析期就该拒掉)
[DIFF] 锁与仓库不一致重算出来的锁与文件不同:源码或依赖变了。确认后重打锁
有源码文件读不出来或太大 …单文件超过 256 KiB,或读不到。拆文件
不认识的命令: X命令名打错。lompi help

和 pip 对照着看

pip lompi 差别
pip install Xlompi plan <store> X --into depsLompi 输出计划,落盘交给上层
pip show Xlompi show <store> X多给实例身份与 use 边
pip freezelompi resolve <store> X钉的是内容哈希,不是版本号
pip checklompi verify <store> X <锁>重算 + 逐字节比对,强得多
pipdeptreelompi tree <store> X同一个包的不同版本各占一行
requirements.txtlompi.lock没有版本区间可写;哈希就是身份

边界(诚实清单)

  • 没有注册中心服务端,Lompi 也不自己联网 —— 网址库就是一个 git 仓库(布局与 store 相同),不需要谁去运维一个服务端。Lompi 自己做不到联网:PE 上那 8 个 syscall 里没有 socket,垫片也没有 ws2_32 / winhttp;所以网络那一步是 fetch 打印出来交给 git 的,由 shell 或 CI 执行。
  • 不做二进制分发 —— 泛型是类型定向单态化的,使用方必须看得到源码。
  • plan 不建目录 —— 工具链的 Windows PE 目标没有 mkdir,它只打计划;deps/ 下的目录要自己先建好。
  • 单文件上限 256 KiB —— 超了 Lompi 拒算这个包的身份,而不是截断。静默截断会算出一个错的身份,那比报错坏得多。
  • 容量护栏 —— 仓库 512 个实例、单包 use 边 2048 条、目录项 4096 条、名字 / 路径池各 64 KiB 与 256 KiB、依赖树深度 256。超出这些会被静默截断,属于已知上限,不是设计。
  • 环单独检测 —— A use B 且 B use A 会被拒。Merkle 身份替代不了这条检测:两边哈希都算得出,所以它得单独做。
它现在到哪一步: 从 0.1.4 Alpha2.3 起,Lompi 随 Loment 一起安装——装好 Loment,lompi 就在 PATH 上,不用单独装。同样的处理方式也用在 agent 指南上:那份让 agent 一装上就会写 Loment 的东西,随工具链一起落地,不需要另外去拿。

Lompi 覆盖的是解析与身份这一层:读依赖、算身份、发锁、验锁、给落盘计划。它不做编译器那一侧的物化改名——那是 库系统materialize 的活。它自己也是用 Loment 写的,所以在源码树里 loment build lompi.lomt -o lompi 就能把它重编出来(必须在那个目录里调用——这条工具链的路径导入按当前目录解析)。