如何让 nixpkgs 更快地更新
NixOS 的声明式安装 pkgs 确实很爽,但是随之而来的问题是 pkgs 的更新会比较慢,在 Repology 可以看到1,即使是 nixpkgs-unstable 的更新百分比也只有 87.71%,远低于 Homebrew 的 94%。
0x00 为什么 nixpkgs 更新相对会慢一些呢?
一个很大的原因是 reproducible 的原教旨主义导致的。主要不是闭源软件,nixpkgs 都会从源码编译生成官方的 pkgs,相比之下其它发行版只需要下载 binary 放在合适的路径就可以了。
在 nixpkgs 的 repo 可以看到,更新主要来自于 r-ryantm 这个 bot 使用 nix-update 生成的 PR2,而每一个 PR 都要经过繁琐的 CI 验证。
甚至因为 Ghostty 没办法在 nix 中编译,至今 ghostty 都不支持 aarch64-darwin,只有 ghostty-bin 这个替代。
master 分支频繁的更新并不适合用户每天使用,因为这时候还没有 cache,新的版本需要在本地编译,性价比不高。
nixpkgs-unstable 需要等待 Hydra 编译所有更新的 pkgs 并生成缓存,所以一般是隔几天批量更新一次。
对于下游的用户来说,更新自然会晚一些了。
0x01 除了自己打个新包,怎么才能用上某个新版本呢?
很多 pkg 接近日更的频率,显然是不适合 nixpkgs 这种机制的。如果有 critical bugfix 或者大的 feature update,真的需要体验最新的版本。
我们用 opencode3 举例,目前 GitHub release 是 v1.1.16,但是 unstable 还是指向 1.1.11,通过查看 source4 我们可以看到 version 其实也是在一个 nix 文件中指定的。
那可以在本地 override 这个 version 吗?当然可以!
1environment.systemPackages = with pkgs; [
2 opencode
3];
我们只需要使用内置的 overrideAttrs 就行:
1environment.systemPackages = with pkgs; [
2 (opencode.overrideAttrs (old: rec {
3 version = "1.1.16";
4 src = old.src.override {
5 rev = "v${version}";
6 hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
7 };
8 }))
9];
因为我们还不知道 sha256,所以先用 fakeHash 直接编译一次,等报错提示 checksum mismatch 的时候手动替换一下就行。
那我们怎么知道 opencode 什么时候发布了新的 release 呢?
GitHub 有内置的 RSS,只要在 https://github.com/anomalyco/opencode/releases 最后加上 .atom,就变成一个 RSS 订阅了。
0x02. 那还得 switch 之前检查 RSS 有没有更新,可以更自动化一点吗?
当然可以!
nvfetcher 可以自动地把一个 repo 的最新 release 变成 nix 表达式,在我们的配置里引入就可以了。
首先定义一下 pkg release:
1# foo/source.toml
2[opencode]
3src.github = "anomalyco/opencode"
4fetch.github = "anomalyco/opencode"
执行 nvfetcher -c foo/source.toml -o foo/_sources 就会在 foo/_sources 下生成 nix 表达式:
1# This file was generated by nvfetcher, please do not modify it manually.
2{ fetchgit, fetchurl, fetchFromGitHub, dockerTools }:
3{
4 opencode = {
5 pname = "opencode";
6 version = "v1.1.16";
7 src = fetchFromGitHub {
8 owner = "anomalyco";
9 repo = "opencode";
10 rev = "v1.1.16";
11 fetchSubmodules = false;
12 sha256 = "sha256-QOblj/GCMMPnE18SPKVfHZsQveJEHkKCXG+ez/llOdM=";
13 };
14 };
15}
再定义一个 overlay 来全局 overrideAttrs:
1{ pkgs, ... }:
2let
3 sources = pkgs.callPackage ./_sources/generated.nix { };
4in
5{
6 nixpkgs.overlays = [
7 (_: prev: {
8 opencode = prev.opencode.overrideAttrs (_: {
9 inherit (sources.opencode) src version;
10 });
11 })
12 ];
13}
也可以在 toml 中定义多个 pkg,在上面的 nix 中 override 多个 pkg,之后只需要在配置中引入上面这个 nix 就无缝替换了。当然最好把 nvfetcher 的逻辑和 flake 更新写在一起,比如:
1nfu () {
2 treefmt -q . && nix flake update
3 nvfetcher -c nvfetcher/source.toml -o nvfetcher/_sources
4}
0x03. nvfetcher 这么方便,那缺点是什么呢?
#1. 它没办法处理一些需要检查 dep 完整性的 pkg。
比如 crush 的 nix 表达式里5有一个 vendorHash,这是在 build 之前对 input 和 env 做的校验,我们仍然需要先填写 fakeHash 等报错了再更新:
1crush = prev.crush.overrideAttrs (_: {
2 inherit (sources.crush) src version;
3 vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
4});
#2. 失去了 cache
因为 Hydra 还不存在 new release,所以也没有对应的 cache,这部分的 pkg 就只能 clean local build 了。