如何让 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 了。