缘起
x-cmd pkg 的核心模式很简单:一个被精心维护的 meta.yml + version.yml + info.yml 三件套索引 tarball,里面写了每个包从哪里下、用什么 hash 验、用什么 build flavor 编译。下载下来之后,走一套 populate.sh 把 bin/ lib/ man/ 搬进 ${SPHERE} 的对应槽位。
这套模式的天花板很明显:
包数量上不去 :每个包都要人手写一份 yml,维护不过来;x-cmd/pkg 现在的索引大概覆盖几百个常用工具,但 GNU/Linux 上真正常用的二进制远不止这个数,conda-forge 一个 linux-64 subdir 就 17k+。
二进制来源单一 :要么自己编译 (build/),要么从 GitHub Release 拿;碰到作者不发 release 的项目(很多科研/数据科学库就这样),就只能放弃收录。
跨语言生态孤岛 :Python 用户想用 conda,R 用户想用 CRAN,都得绕一圈;pkg 内部对 npm/pip 只做了下载代理,没把它们的元数据吃进来。
pixi 的出现改变了第三点的下半场。rattler 把整个 conda 协议(repodata + channel + 求解)用 Rust 重写了一遍,而且把 SAT 求解器(resolvo)作为 crate 暴露出来,意味着 x-cmd/pkg 想用的时候,可以选择:
自己再写一个依赖求解器(不现实,我们人少)
调 rattler_solve 的 API(中等,需要 Rust 工具链)
把 pixi 当成后端,临时生成 pixi.toml 调 pixi install(最现实)
我为什么一开始不理解 SAT
我一开始也觉得:一个包管理器而已,不就是把名字换成 URL 吗?后来看 resolvo 才意识到自己把问题想小了。
依赖解析的复杂度不在"下载",在"选谁的版本 "。一个用户的 manifest 长这样:
python >=3.10,<3.11.0a0
pytorch
但 conda-forge 上 pytorch 有几十个 build:
pytorch-2.4.0-py3.10_cuda12.1_0.conda
pytorch-2.4.0-py3.10_cuda11.8_0.conda
pytorch-2.4.0-py3.11_cuda12.1_0.conda
...
每个 build 的 depends 都不一样(cuda 版本不同、python 版本不同)。这些 depends 又都是版本范围 ,不是固定版本。你要找的是:一个 pytorch build,它的依赖集合 ≠ 空,且能与所有"被它拖进来"的包的版本范围求非空交集 。
这就是经典的 CSP (constraint satisfaction problem) ,教科书上写明 NP-hard。
resolvo 干的事情,直观说就是:
把每个 (name, version, build, channel, url) 候选变成一个 Boolean 变量
把每条约束(依赖、互斥、channel 优先)变成 CNF 子句
跑 CDCL 算法,遇冲突学 clause、剪枝
它比通用 SAT 引擎聪明的地方在于:
变量是"领域概念"(候选包)而不是裸 Boolean,子句可以直接用 MatchSpec 表达
知道 channel 优先级的语义,直接生成排除子句,不用绕弯
学习到的子句按 conda 维度压缩,内存压力比通用 SAT 低(pixi/CHANGELOG.md:3513-3516 还专门记了一笔 RAM 降低的事)
至于并行,pixi 实际并行的不是 SAT 搜索本身(那玩意儿是单线程的),而是上游并行的走(per-requirement BFS)+ 多 env 解算的 semaphore 池。我之前说"环境解析已经全并行",其实更准确的说法是"多 env 并行,单 env 内 BFS 并行,SAT 内核单线程 "。
x-cmd pkg 的模型和 conda 不一样
x-cmd/pkg 的设计是"一包一文件",看到 x pkg add jq,就到 META yml 里找 jq,按 URL 下载,解压,populate。这套在 conda-包面前就撞墙了:
conda 包不是 bin/ + lib/ + man/ 的标准 layout,而是任意前缀 (前缀安装风格)。populate 阶段要重写 info/paths.json 里所有 prefix_placeholder。
conda 包是带传递依赖的;x-cmd/pkg 没记录依赖,一旦 jq 真在 conda 上依赖 oniguruma,我们要不要拉?不拉就跑不起来;拉就要么写死依赖,要么做依赖求解。
包尺寸大一个数量级(几十 MB vs 几 MB),网络和磁盘预算要重新算。
所以接 conda,第一步是"放弃依赖求解",只做"按 URL 单包下载"。这刚好对应方案 A。我倾向于先做 A,把骨架立住,再考虑 B、C 是不是值得做。
一些还没想清楚的细节
离线 :x-cmd/pkg 现在是 dist tarball 离线索引;conda 的 repodata.json 没法打进索引 tarball(太大)。要么每次联网拉 current_repodata,要么预生成一份 per-channel 的 name → (version, build, sha256, size) 子集打成索引。
签名 :conda-forge 不签包,只给 sha256。社区信任模型靠 sha256 + 社区声誉。对 x-cmd/pkg 而言这是降级 (我们连索引 tarball 都做 SHA-512 校验)。
包覆盖 :conda-forge 包含很多 x-cmd/pkg 没必要收录的 Python wheels / R packages;把它们全塞进索引会让 x pkg ll 太长。需要某种过滤(例如只列 xbin 已声明的)。
关联阅读
设计讨论(issue 正文):[pkg→env] x-cmd/env 多 backend 编排 (env.yml 作为 conda 生态入口) #18
pixi 主仓库(本机镜像 /Users/l/.x-repo/github.com/x-cmd-sourcecode/pixi):
入口:crates/pixi_command_dispatcher/src/solve_conda/mod.rs:318(Solver.solve(task)? 是唯一一处触发 resolvo 的地方)
并行模型:crates/pixi_command_dispatcher/src/solve_binary.rs:60-73(conda_solve_semaphore)
通道优先级:docs/advanced/channel_logic.md
x-cmd/pkg 相关 issue:
0119.npm-pip-source.yml — 源镜像 env var 模式,直接套用
0125.版本号问题.yml — conda 版本号格式的特殊情况
0222.调整 download 的包的重命名格式.yml — 包缓存命名约定
rattler 系列 crate(本机未 vendor, 引用自 Cargo.lock):
rattler_solve — 把 MatchSpec 编码成 resolvo 子句
rattler_conda_types — PackageRecord / Platform / MatchSpec
resolvo 0.12.1 — 纯 SAT/CDCL 引擎
推进计划
先做 A 的最小 demo:选 ripgrep 做试验品(小、conda-forge 有、运行时零依赖),验证 populate 阶段能正确重写 prefix。
A 跑通后,写 x pkg conda ls <name>,复用 lib/info/ll 的 advise schema。
评估 C:拉 pixi 当后端的可行性,看用户愿不愿意接受"装 pytorch 需要先装 pixi"这个 dogfood 风险。
如果 C 走通,把 x pkg conda resolve 包装成"调 pixi install → 拿 lock 文件 → 批量方案 A 下载"的脚本。
缘起
x-cmd pkg 的核心模式很简单:一个被精心维护的
meta.yml + version.yml + info.yml三件套索引 tarball,里面写了每个包从哪里下、用什么 hash 验、用什么 build flavor 编译。下载下来之后,走一套populate.sh把bin/ lib/ man/搬进${SPHERE}的对应槽位。这套模式的天花板很明显:
build/),要么从 GitHub Release 拿;碰到作者不发 release 的项目(很多科研/数据科学库就这样),就只能放弃收录。pixi 的出现改变了第三点的下半场。rattler 把整个 conda 协议(repodata + channel + 求解)用 Rust 重写了一遍,而且把 SAT 求解器(resolvo)作为 crate 暴露出来,意味着 x-cmd/pkg 想用的时候,可以选择:
rattler_solve的 API(中等,需要 Rust 工具链)pixi.toml调pixi install(最现实)我为什么一开始不理解 SAT
我一开始也觉得:一个包管理器而已,不就是把名字换成 URL 吗?后来看 resolvo 才意识到自己把问题想小了。
依赖解析的复杂度不在"下载",在"选谁的版本"。一个用户的 manifest 长这样:
但 conda-forge 上
pytorch有几十个 build:pytorch-2.4.0-py3.10_cuda12.1_0.condapytorch-2.4.0-py3.10_cuda11.8_0.condapytorch-2.4.0-py3.11_cuda12.1_0.conda每个 build 的
depends都不一样(cuda 版本不同、python 版本不同)。这些depends又都是版本范围,不是固定版本。你要找的是:一个pytorchbuild,它的依赖集合 ≠ 空,且能与所有"被它拖进来"的包的版本范围求非空交集。这就是经典的 CSP (constraint satisfaction problem),教科书上写明 NP-hard。
resolvo 干的事情,直观说就是:
(name, version, build, channel, url)候选变成一个 Boolean 变量它比通用 SAT 引擎聪明的地方在于:
MatchSpec表达pixi/CHANGELOG.md:3513-3516还专门记了一笔 RAM 降低的事)至于并行,pixi 实际并行的不是 SAT 搜索本身(那玩意儿是单线程的),而是上游并行的走(per-requirement BFS)+ 多 env 解算的 semaphore 池。我之前说"环境解析已经全并行",其实更准确的说法是"多 env 并行,单 env 内 BFS 并行,SAT 内核单线程"。
x-cmd pkg 的模型和 conda 不一样
x-cmd/pkg 的设计是"一包一文件",看到
x pkg add jq,就到 META yml 里找 jq,按 URL 下载,解压,populate。这套在 conda-包面前就撞墙了:bin/ + lib/ + man/的标准 layout,而是任意前缀(前缀安装风格)。populate 阶段要重写info/paths.json里所有prefix_placeholder。所以接 conda,第一步是"放弃依赖求解",只做"按 URL 单包下载"。这刚好对应方案 A。我倾向于先做 A,把骨架立住,再考虑 B、C 是不是值得做。
一些还没想清楚的细节
repodata.json没法打进索引 tarball(太大)。要么每次联网拉current_repodata,要么预生成一份 per-channel 的name → (version, build, sha256, size)子集打成索引。x pkg ll太长。需要某种过滤(例如只列xbin已声明的)。关联阅读
/Users/l/.x-repo/github.com/x-cmd-sourcecode/pixi):crates/pixi_command_dispatcher/src/solve_conda/mod.rs:318(Solver.solve(task)?是唯一一处触发 resolvo 的地方)crates/pixi_command_dispatcher/src/solve_binary.rs:60-73(conda_solve_semaphore)docs/advanced/channel_logic.md0119.npm-pip-source.yml— 源镜像 env var 模式,直接套用0125.版本号问题.yml— conda 版本号格式的特殊情况0222.调整 download 的包的重命名格式.yml— 包缓存命名约定Cargo.lock):rattler_solve— 把MatchSpec编码成 resolvo 子句rattler_conda_types—PackageRecord/Platform/MatchSpecresolvo 0.12.1— 纯 SAT/CDCL 引擎推进计划
x pkg conda ls <name>,复用lib/info/ll的 advise schema。x pkg conda resolve包装成"调pixi install→ 拿 lock 文件 → 批量方案 A 下载"的脚本。