| name | xpkg-creater |
| description | 在 xim-pkgindex 中创建/更新 xpkg 包(V2/兼容 V1),遵守 xlings SubOS 隔离规范,补齐多架构资源与测试,并在本地与测试集验证通过后再提交 PR。 |
xpkg-creater
用于在 xim-pkgindex 仓库中新增或维护 xpkg 包文件,确保满足:
- XPackage Spec V2(新包推荐
spec = "2",历史 V1 继续兼容)
- hooks 约束(尤其
install / config)
- subos 环境隔离规范
- 本地验证 + 测试集验证 + CI 要求
详细安装命令、测试命令清单、相关链接见:
references/xlings-setup-and-links.md
references/testing-and-acceptance.md
0) xlings 工具入口(必须具备)
开发/验证 xpkg 之前,先确保环境可用:
- 已安装
xlings(用于 xim/xlings/xvm 命令)
xlings 命令在 shell 中可执行
安装方式与快速命令见 references/xlings-setup-and-links.md。
1) 包格式规范
一个 xpkg 文件由两部分组成:
package = { ... } 元数据域
- hooks 函数域(
installed/build/install/config/uninstall,按需实现)
1.1 必填与推荐字段
至少保证:
spec = "2"(仅维护历史配方时可继续使用 "1")
name
description
type(常见:package/script/config/template)
xpm(平台、版本、资源映射)
常见推荐字段:
archs, status, categories, keywords
authors/maintainers/licenses/repo/docs/homepage
xvm_enable = true(需要 xvm 管理时)
1.2 xpm 写法要点
- 按平台配置:
windows/linux/macosx/ubuntu/debian/...
- 版本常用:
{"latest" = { ref = "x.y.z" }}
{"x.y.z" = { url = "...", sha256 = "..." }}
"XLINGS_RES"
- 新包默认来源推荐使用
xpm.source:
source = "xlings-res":官方资源服务器,版本项提供每个架构的 sha256
source = "https://.../${version}/...${arch}...":第三方 URL template
- URL 不规则时使用版本项 per-arch resource map;特殊版本用显式
url 覆盖默认 source。
- 多架构条目的
sha256 必须覆盖每个受支持架构;缺失时版本检查器必须 fail closed。
- 可选
ci = { mirror = true, update = true } 只声明是否加入官方自动化;扫描周期统一由
仓库 .github/xpkg-ci.yml 管理,不能在单个包中写入周期。
- 平台继承:
ubuntu = { ref = "linux" }
- script/config 类型可使用空资源:
["0.0.1"] = {}
资源选择策略(默认使用 xlings-res)
官方二进制优先使用 xpm.source = "xlings-res",并为每个平台/架构写入权威 SHA256。
第三方 release 使用 URL template + per-arch SHA256;只有 URL 不规则时才展开 per-arch
resource map。显式版本 url 可以覆盖根级或平台级 source。
xpm = {
source = "xlings-res",
linux = {
["latest"] = { ref = "1.0.0" },
["1.0.0"] = {
sha256 = {
x86_64 = "<linux-x86_64-sha256>",
aarch64 = "<linux-aarch64-sha256>",
},
},
},
},
参考实现:docs/V2/xpackage-spec.md 与 pkgs/g/github-gh.lua
1.2.1 XLINGS_RES 镜像发布要求
这里的 XLINGS_RES / 镜像表解析的是软件包二进制,走资源服务器
GLOBAL = github.com/xlings-res、CN = gitcode.com/xlings-res。这与「索引仓库本身」
的分发(索引即资源 / Y-asset)是同一套资源服务路径但不同资产,互不混淆。
索引机制全貌见 docs/design/index-distribution.md(同步自 xlings v0.4.55 源码)。
当某个版本使用 xpm.source = "xlings-res"(历史写法为 "XLINGS_RES")时,该版本已经进入 xlings 多镜像资源服务链路。发布前必须同时满足:
https://github.com/xlings-res/<pkg> 与 https://gitcode.com/xlings-res/<pkg> 都存在同名 tag/release。
- 两边 release 都包含该版本声明会使用的全部平台资产;文件名必须符合 xlings-res 约定。
- 两边资产必须来自同一个权威上游 release 或同一次构建产物;发布后从 GitHub RES、GitCode RES、权威上游各下载一次并比对 sha256,确认字节一致。
- 每个归档都要发布同名
.sha256 sidecar;索引版本项必须为每个受支持架构写入与 sidecar 一致的 SHA256。
version-check.py --apply 缺少平台、架构、资产或 sidecar 时必须 fail closed,不得生成不完整条目。
- 如果补发历史版本,发布后确认两边
latest 仍指向应当作为最新的版本,不要因为补旧版本导致 latest 倒退。
- PR 描述或汇报中写清楚 GitHub RES、GitCode RES 的 release/tag,以及 sha256 校验结果。
如果 GitHub RES 和 GitCode RES 任一侧缺资源、版本不一致、资产不一致,不能把该版本切到 "XLINGS_RES";先补齐镜像资源,再改包索引。
2) hooks 实现规范(核心)
2.1 import 规范
优先使用新版 API:
import("xim.libxpkg.pkginfo")
import("xim.libxpkg.xvm")
import("xim.libxpkg.system")(可选)
import("xim.libxpkg.log")(可选)
避免旧 API:
import("xim.base.runtime")
import("common")
import("platform")
2.1.1 通用 Lua/API 边界
一般情况下,新增或维护 xpkg 只能使用三类能力:
- XPackage Spec V1 规定的
package 元数据、xpm 描述和 lifecycle hooks。
- 标准 Lua 语法与标准库(例如
string/table/io/os.getenv/pcall/error 等)。
- 必要的
xim.libxpkg.* API(例如 pkginfo/xvm/system/log/json)。
不要默认引入 xmake 私有 runtime/API。除非某个既有包的兼容性约束已经证明必须使用,否则避免:
core.*、detect.*、runtime.*、xim.base.runtime
common、platform
path.*、os.host()、is_host()、try { ... }、raise(...)
测试也应默认锁定这条边界:import 只能来自 xim.libxpkg.*,路径、错误处理、文件 IO 优先用标准 Lua 或 libxpkg 可移植封装。
hook runtime 里没绑定的东西会静默毁掉安装。 已确认不可用的:
| 写法 | 现象 | 换成 |
|---|
os.exists(p) | attempt to call a nil value (field 'exists') | os.isdir(p) or os.isfile(p) |
os.arch() | 返回 nil / _RUNTIME.arch 为空 | 从 pkginfo.install_file() 推导 |
os.curdir() | attempt to call a nil value (field 'curdir') | os.cd 之后用一个已知目录回去(pkginfo.install_dir()) |
os.execv(...) | 同上 | system.exec / os.exec |
os.files(...) | 同上 | os.dirs 递归,或交给 xcopy / shell |
path.absolute(p) | 同上 | pkginfo.install_dir() 本来就是绝对路径 |
先 grep,再写。 上面五条都是同一个形状,而每一条在写下去之前都能用一条
命令排除:
grep -rn "os\.curdir" pkgs/ | wc -l
整个 index 里零处使用的 sandbox API,基本可以认定它不在 runtime 里。
另外两条不在上表里,因为它们存在、只是行为和你以为的不一样:
os.cd(dir) 之后 system.exec 不继承那个 cwd(os.exec 才继承)。
照抄别的 recipe 的「cd 之后用相对路径」时,先看清它用的是哪一个 ——
命令可以拼得完全正确,然后找不到自己的文件。
path.join 在 Windows 上混用分隔符(保留已有的 \、新加 /)。
"C:\Windows/System32/tar.exe" 执行不了,msiexec 也拒收。
凡是要交给 Windows 程序的路径都过一遍 winpath() —— 包括可执行文件
本身的路径,不只是它的参数。
Windows 上的 tar:PATH 会替你选,而两个 tar 能力不同
runner 上同时存在两个 tar,它们不是同一个程序:
| 来源 | 读 zip? | C:\... 参数 |
|---|
| bsdtar | %SystemRoot%\System32\tar.exe(Win10 1803+) | ✅ | 正常 |
| GNU tar | Git for Windows / MSYS2 | ❌ 完全不支持 | 当成 host:path,报 Cannot connect to C: |
.vsix / .zip 只有 bsdtar 读得了。而哪一个被选中取决于 PATH 顺序 ——
同一个 GitHub 镜像上,index 自己的 windows-test 拿到 bsdtar 通过,
mcpp 的 e2e 拿到 GNU tar 失败,recipe 一个字都没变。
所以:解 zip 时写绝对路径的 System32\tar.exe,不要写裸 tar。
钉死之后盘符问题也随之消失(那是 GNU tar 独有的),路径可以放心用绝对的。
--force-local 不是答案 —— GNU tar 认、bsdtar 拒收,是拿一个坏环境换另一个。
这不是概率判断:能用的东西早就有人用了。而代价是不对称的 —— 猜对省几秒,
猜错要等一轮 Windows CI(约 4 分钟)才知道,而且失败信息出现在
install hook 里、离你写的那一行有几层。
危险的地方在于表现形式:install hook 抛错之后,安装目录里往往只剩一个 .xpkg.lua、
没有 payload,而外层可能仍然打印 ✓ N package(s) installed。所以
install() 结尾一定要断言真正的产物存在(raise(...) 或 return os.isfile(exe)),
别只 return true;验收时也要真的去 ls 安装目录,不要只看安装命令的退出码。
注意 raise() 本身也不进汇总 —— 它不会让外层报失败;而 hook 里的 Lua 运行时错误会
浮出来([error] [pkg] failed: ...)。所以 raise 只是给读日志的人看的,不能当成保护。
"安装目录是空的"最常见的原因不是 hook 有问题
xlings 在同名同版本已经装在另一个 namespace 下时,会整个跳过 install hook,并且照样
打印成功。 xim:foo@1.2.3 已装的情况下,每一次 local:foo@1.2.3 安装都是静默 no-op,
只写下 .xpkg.lua —— 看起来和 install hook 坏掉一模一样。测之前先清两边:
rm -rf ~/.xlings/data/xpkgs/{xim,local}-x-<pkg>/<version>
(这条是用 hook 里塞 io.writefile 探针确认的:日志文件根本没生成。曾因此把一个好端端的
os.mv 误判成 bug —— 实测 xlings 会重新解压、归档没了也会重新下载,os.mv 连装两次没问题。)
另外:往 local index 里放两个 package.name 相同的文件,会让整个 local repo 静默从搜索
路径消失(package 'local:foo' not found, searched repos: [xim, scode])。删掉重复文件即恢复。
2.1.2 配置型包的 Lua 边界
对 type = "config" 且会写入用户工具配置的包(例如 Claude/LLM 配置):
- 只使用标准 Lua 语法、
package 元数据、hooks,以及必要的 xim.libxpkg.* import。
- 不使用 xmake 私有 import/API:
core.*、detect.*、xim.base.runtime、runtime.*、is_host()、os.host()、path.*、try { ... }。
- Lua 错误使用标准
error(...),不要使用 raise(...)。
install() 保持轻量,默认 return true;实际配置写入放在 config()。
- 修改已有 JSON 配置时先读取并保留原对象,只更新本包负责的 key;写入前备份,并用
log.info/log.warn/log.error 说明结果,敏感 token 不要明文打印。
- 如果用户未输入新 key 但已有有效配置,使用
log.warn 提示继续复用旧 key 且不改 token;如果没有可复用 key,使用 log.error 后失败。
- 针对独立行为(例如修复 Claude token 缓存的 env 项)单独抽成函数,便于测试锁定边界。
2.2 install() 约束
install() 只负责安装动作本身:
- 使用
pkginfo.install_file() 获取下载/解压后的输入路径
- 使用
pkginfo.install_dir() 作为目标安装目录
- 可先
os.tryrm(pkginfo.install_dir()) 再 os.mv(...)
- 若是 Linux 预构建 ELF,必要时做可重定位修复(如 patchelf)
2.2.1 installed() 约束 —— 「装好了」必须等于「能用」⚠️
installed() 的含义是:payload 处于「这份 recipe 产出的状态」,
不是「这儿有个目录」。两者只在 recipe 变更时不一样,而那正是它要紧的时候。
2026-08-16 这一轮 MSVC 生态里,九层缺陷有四层是这一条:每一层的
windows-test 都是绿的,而包对使用者是坏的。
(a) 必须查文件,不能查目录
if os.isdir(path.join(d, "Lib", VER, "um", "x64")) then return true end
if not os.isfile(path.join(d, "Lib", VER, "um", "x64", "kernel32.lib")) then
return false
end
真实后果:少了带 kernel32.lib 的那个 MSI,目录照样存在(另外 341 个 um 库
落进去了),installed() 说 yes,而任何程序的链接都失败。
Windows 文件系统大小写不敏感,os.isfile("kernel32.lib") 照样匹配磁盘上的
Kernel32.Lib。不要用"文件名大小写不确定"当作查目录的理由。
(b) 必须是覆盖,不是抽样
一个 payload 一条断言,而且那条断言的文件只有那个 payload 提供。
这样哪个 payload 没下来/没解开/没合并,报错就点名哪一个。
windows-sdk 的做法(8 个 payload → 7 条断言):
| 断言的文件 | 唯一提供它的 payload |
|---|
Include/<v>/ucrt/corecrt.h | Universal CRT |
Include/<v>/um/winnt.h | Store Apps Headers |
Include/<v>/shared/windef.h | Store Apps Headers OnecoreUap |
Lib/<v>/um/x64/kernel32.lib | Store Apps Libs |
Lib/<v>/um/x64/gdi32.lib | Desktop Libs x64 |
bin/<v>/x64/rc.exe / mt.exe | Store Apps Tools |
选 gdi32.lib 而不是随便一个库,是因为它只在 Desktop Libs 里 ——
那 365 个库和 Store Apps Libs 的 116 个完全不相交。抽样会漏,覆盖不会。
(c) 必须能表达「什么不该在」
包的版本号不会因为 recipe 改了就变。所以对已经装了旧布局的机器,
installed() 是唯一能把它们拉回来的东西:
for _, d in ipairs(bin_arch_dirs()) do
if os.isfile(path.join(d, "vctip.exe")) then return false end
end
新增的文件靠断言"它在"就能发现;删掉的文件必须显式说"它不该在"。
(d) 失败时要点名缺哪个文件
local missing = {}
for _, f in ipairs(required_files()) do
if not os.isfile(f) then table.insert(missing, f) end
end
log.error("... 不在应该在的位置:\n " .. table.concat(missing, "\n "))
「wanted: <四个路径>」不算 —— 那是把清单再抄一遍,读的人还得自己比对。
2.3 config() 约束
config() 负责将该版本注册到 xvm(subos 隔离路由):
- 使用
xvm.add("tool")
- 或
xvm.add("tool", { bindir = ..., alias = ... })
- 可执行文件不在安装根目录时,必须明确
bindir
2.3.1 共享名与 flavor 版本(注册前必查)
一个 xvm 名字(程序名或 lib 名)可能被多个包提供:java 来自每个 JDK 发行版,
gcc 来自 gcc.lua 和 musl-gcc.lua,crt1.o/libc.so 来自 glibc.lua 和 musl.lua。
用裸版本号注册共享名有两种失败方式,长得完全不一样:
| 情况 | 结果 |
|---|
| 两个包注册同名同版本 | xvm 直接拒绝,第二个包整批 config 失败:another package already owns this exact name and version |
| 两个包注册同名不同版本 | 接受,名字变成双 owner。一次 xvm use 落到另一侧就静默改写共享 lib/ 里的符号链接 |
第二种更危险,因为安装当下一切正常。实测(musl 加入前):
crt1.o = {"active": "glibc-2.39", "installed": ["glibc-2.39", "musl-1.2.5"]}
crt1.o 一旦切到 musl 那侧,该 subos 里所有 glibc C 链接全部静默挂掉。
做法:共享名注册到 <version>-<flavor>,并在配方里把撞名集合单独列成一张表。
local SHARED_LIBS = { "crt1.o", "crti.o", "crtn.o", "Scrt1.o", "libc.a",
"libc.so", "libdl.a", "libm.a", "libpthread.a",
"librt.a", "libutil.a" }
local MUSL_ONLY_LIBS = { "ld-musl-x86_64.so.1", "rcrt1.o", "libcrypt.a",
"libresolv.a", "libxnet.a" }
local FLAVOR = "musl"
local function flavor_version() return pkginfo.version() .. "-" .. FLAVOR end
规则:
-
撞名集合要算,不要估。 拿对方配方里的注册表和自己 payload 里真实的目录求交集,
并用测试锁住这个集合 —— 上游改了文件集,测试要能发现。
-
撞名的必须带 flavor;不撞名的也一起带,这样整组能被同一次 xlings use 切换,
将来第三个包进来也是撞上约定而不是撞上一个恰好空着的版本号。
-
绑定根用 type = "group"。 根节点不对应任何可执行文件(没有 bin/musl),
留成默认的 program 类型会生成一个永远失败的 shim
(subos/*/bin/musl -> bin/xlings),self doctor 会把它报成 orphan
(openxlings/xlings#452)。
-
uninstall() 必须版本内收敛:xvm.remove(name, <stored key>)。
用裸名删会把对方包的注册一起删掉。
⚠️ xvm.add 会自己补索引命名空间前缀,xvm.remove 不会。
从 local: 或任何非 xim 的索引仓库安装时,version = "1.2.5-musl" 实际存成
local:1.2.5-musl;卸载时传裸键匹配不到,根节点删了、所有 lib 节点全留下。
CI 抓不到——posix-test.sh 的卸载后检查只看 bin/ 里残留的 shim,
而 lib 节点不产生 shim。照 glibc.lua 的 __version_key() 写:
function __stored_version()
local store = path.filename(path.directory(pkginfo.install_dir()))
local ns = store:match("^(.-)%-x%-")
local bare = flavor_version()
if ns and ns ~= "" and ns ~= "xim" then return ns .. ":" .. bare end
return bare
end
验收方式:装完 → 看 subos/<name>/.xlings.json 的 workspace → 卸载 → 再看一次,
必须回到 null。只跑 posix-test.sh 不足以说明卸载干净。
已有先例(新包照抄即可):jdk-temurin/corretto/zulu 的 25.0.4+7-temurin、
musl-gcc.lua 的 16.1.0-musl、musl.lua 的 1.2.5-musl。
2.4 禁止事项(隔离合规)
- 不要
os.exec("xvm add ...") / os.exec("xvm remove ...")
- 不要修改
.bashrc / shell profile
- 不要直接
os.addenv("PATH") 或 os.setenv("PATH")
- 不要直接
apt install / brew install / pacman -S
依赖请通过 xpm.<platform>.deps 声明;命令路由请通过 xvm shim 完成。
3) 新增/修改包的标准流程
- 在
pkgs/<首字母>/<name>.lua 新增或修改包。
- 若新增包,创建镜像测试文件:
tests/<首字母>/test_<name_with_underscore>.py
- 测试默认锁定:只 import
xim.libxpkg.*,只使用标准 Lua + xpkg 规范,不使用 path.*/os.host()/try {}/raise() 等 xmake 私有 API。
- 对写用户配置的
type = "config" 包,额外锁定:install() 轻量,实际写入在 config()。
- 先跑本地直接命令验证(索引/安装/搜索/卸载)。
- 再跑测试集验证(L0~L4,至少 L0/L1/L2)。
- 准备 PR:写清楚包用途、安装/卸载行为、系统影响、测试结果。
详细步骤与命令见 references/testing-and-acceptance.md。
4) PR 提交硬性要求
- 本地通过直接命令验证 + pytest 测试验证。
- 新增包必须带对应
tests/ 测试文件。
- 不破坏 subos 隔离。
- PR 描述中必须包含:
- 包的作用
- 安装时做了什么
- 卸载时做了什么
- 是否修改系统配置/环境变量
- 本地测试与 CI 测试结果
5) 最小骨架(V2)
package = {
spec = "2",
name = "demo",
description = "demo package",
type = "package",
archs = {"x86_64"},
status = "stable",
categories = {"tools"},
keywords = {"demo"},
xvm_enable = true,
xpm = {
source = "xlings-res",
linux = {
["latest"] = { ref = "1.0.0" },
["1.0.0"] = {
sha256 = { x86_64 = "<sha256>", aarch64 = "<sha256>" },
},
},
},
}
import("xim.libxpkg.pkginfo")
import("xim.libxpkg.xvm")
function install()
os.tryrm(pkginfo.install_dir())
os.mv("demo", pkginfo.install_dir())
return true
end
function config()
xvm.add("demo")
return true
end
function uninstall()
xvm.remove("demo")
return true
end