NixOS 系列(三):软件打包,从入门到放弃 的插图

NixOS 系列(三):软件打包,从入门到放弃

NixOS 系列文章目录:

  • NixOS 系列(一):我为什么心动了
  • NixOS 系列(二):基础配置,Nix Flake,和批量部署
  • 推荐阅读: NixOS 与 Nix Flakes 新手入门 ,作者 Ryan Yin
  • NixOS 系列(三):软件打包,从入门到放弃
  • NixOS 系列(四):「无状态」操作系统
  • NixOS 系列(五):制作小内存 VPS 的 DD 磁盘镜像
  • NixOS 的一大特点是,系统所有的二进制程序和库文件都在 /nix/store 目录中,由 Nix 包管理器管理。这也意味着,NixOS 不符合 Linux 的 FHS 标准 ,它的 /lib /lib64 目录下不存在类似 ld-linux-x86-64.so.2 之类的库文件动态加载器,更不存在 libc.so 之类的库文件。因此,除非静态链接,否则为其它 Linux 下编译的二进制文件将完全无法在 NixOS 下运行。

    所以,要在 NixOS 上使用尚不存在于 Nixpkgs 仓库中的软件,最佳方案是自己用 Nix 语言写一份打包脚本,给这个软件打一个包,然后把打包定义加入 configuration.nix 中,从而安装到系统上。

    关于 NixOS 的软件打包,有三个好消息和两个坏消息。好消息是:

  • Nixpkgs,也就是 NixOS 的软件仓库,提供了大量的打包自动化函数,对于很多使用常见编程语言的开源软件(包括 C/C++,Python,Go,Node.js,Rust 等,但不包括 Java),你只需要调用现成的函数,指定一下源码的下载方式,Nixpkgs 就能自动检测软件的打包系统,自动传入合适的参数并完成软件打包。
  • 对于以二进制方式分发的软件(常见于闭源软件),Nixpkgs 也提供了现成的自动化解决方案:
  • 一种是 Autopatchelf,自动修改二进制文件中的库文件路径,将其指向 /nix/store 中。
  • 另一种是 Bubblewrap,或者基于 Bubblewrap 的 steam-run ,模拟一个符合 FHS 标准的运行环境。顾名思义, steam-run 主要针对的是 Steam 游戏平台以及它上面的游戏,但它也可以用于其它闭源软件。
  • Nix 包管理器会在一个隔离的环境中进行软件打包,你可以粗略地理解成一个断网,限制权限,只允许访问固定路径的 Docker 容器。在编译过程中,访问外部路径或者联网的尝试全部会失败,只能使用 Nix 编译脚本中事先指定的依赖。因此,打包出来的程序将完全不依赖其它文件。
  • 坏消息是:

  • 开发者不一定比打包者更懂 Linux。开发者可能会在代码和编译脚本里写死各种路径,做出各种只符合 FHS 标准的假设。此时就需要你手动写补丁,纠正这些路径,让程序可以在 NixOS 上正常编译运行。
  • 一旦你遇到了不能使用现成函数的情况,包括下列情况,你就得做好「一杯茶,一包烟,一个 Bug 调一天」的准备:
  • 开发者使用了一些奇怪的源码目录结构(例如 osdlyrics ),或者非标准的编译方式
  • 程序主动检测运行环境(例如 UOS 版微信客户端)
  • 程序主动检测对它本身的修改(例如 SVP 视频补帧软件)
  • 我在几个月前将日常使用的发行版从 Arch Linux 换成了 NixOS,在使用过程中打了很多 NixOS 软件包。本文将从简单的打包开始一步步推进,介绍 NixOS 打包的方法,遇到的常见问题以及应对策略。

    首先,强烈建议你安装好 NixOS 操作系统,并在 NixOS 上进行打包。

  • 虽然在非 NixOS 的操作系统上也可以用 Nix 包管理器打包软件,但打包出的软件在运行过程中可能还会残留有对 FHS 标准目录的依赖,从而导致它们无法正常在 NixOS 上使用。当然,如果你打包只是自用,只考虑自己的运行环境,那可以忽略这条。
  • 此外,要在非 NixOS 的操作系统上安装 Nix 打包的软件,你需要使用 Home Manager ,一个通过 Nix 语言的配置文件来管理你的 Home 目录下的软件配置文件的工具。你需要自行研究,或者查阅其它人的相关文章。
  • 使用 NUR 的打包模版

    NUR 是 Nix 的由用户自行管理的软件仓库,类似于 Arch Linux 的 AUR。NUR 提供了一份现成的 Nix 仓库模版,你可以方便地统一添加、管理自己的软件包。

    在 GitHub 上,访问 nur-packages-template ,点击「Use this template」用这个模版建立一个仓库。之后,你可以将所有软件包统一保存在你新建的仓库。

    如果要将自己的软件包发布到 NUR,你需要向 NUR 的主仓库 发起 Pull Request,将你自己的仓库地址加进去。但即使你不发 Pull Request,也完全可以直接使用自己的仓库。

    然后,把你的仓库 Clone 下来。

    对于不使用 Nix Flake 的用户,运行以下命令可以对 example-package 这个模版自带的示例软件包进行打包:

    nix-build -A example-package

    对于使用 Flake 的用户,运行以下命令:

    nix flake update # 可选,将 flake.lock 中的 Nixpkgs 等仓库更新到最新版
    nix build ".#example-package"

    然后,在你的 NixOS 配置中添加自己的仓库。

    对于不使用 Nix Flake 的用户,在 configuration.nix 中添加如下定义:

    nixpkgs.config.packageOverrides = pkgs: {
      myRepo = import (builtins.fetchTarball "https://github.com/nix-community/nur-packages-template/archive/master.tar.gz") {
        inherit pkgs;
    

    https://github.com/nix-community/nur-packages-template 替换成你的仓库地址。

    这样操作后,你就能用类似于 pkgs.myRepo.example-package 的方式使用你打的包了。

    对于使用 Nix Flake 的用户,在 flake.nix 中的 inputs 一节中添加如下定义:

    inputs = {
      # ...
      myRepo = {
        url = "github:nix-community/nur-packages-template";
        inputs.nixpkgs.follows = "nixpkgs";
      # ...
    

    nix-community/nur-packages-template 替换成你的仓库地址。

    然后,在 flake.nix 中的 output 一节,你的 nixosConfigurations 定义中,为每个系统添加一个 module:

    outputs = { self, nixpkgs, ... }@inputs: {
      nixosConfigurations."nixos" = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          # 在 modules 的开头添加下面这几行
            nixpkgs.overlays = [
              (final: prev: {
                myRepo = inputs.myRepo.packages."${prev.system}";
          # 在 modules 的开头添加上面这几行
          ./configuration.nix
    

    这样操作后,你就能用类似于 pkgs.myRepo.example-package 的方式使用你打的包了。

    直接在 NixOS 配置文件中添加软件包

    当然,你也可以不使用 NUR 的模版,而是直接把打包定义和 NixOS 的配置文件放在一起。

    假设你有这样一个打包定义,保存成 example-package.nix:(来自 https://github.com/nix-community/nur-packages-template/blob/master/pkgs/example-package/default.nix

    { stdenv }:
    stdenv.mkDerivation rec {
      name = "example-package-${version}";
      version = "1.0";
      src = ./.;
      buildPhase = "echo echo Hello World > example";
      installPhase = "install -Dm755 example $out";
    

    你可以在 configuration.nix 中使用 pkgs.callPackage 函数来调用它:

    { config, pkgs, ... }:
      # 直接使用这个包
      environment.systemPackages = [
        (pkgs.callPackage ./example-package.nix { })
      # 或者将这个包先定义成一个常量
      environment.systemPackages = let
        examplePackage = pkgs.callPackage ./example-package.nix { };
        examplePackage
    

    如果你要单独尝试构建这个软件包,你可以使用以下命令:

    nix-build -E 'with import <nixpkgs> {}; callPackage ./example-package.nix {}'

    虽然你可以直接调用 Nix 包管理器内置的 builtins.derivation 函数进行打包,但我们一般用更为方便的 stdenv.mkDerivation 函数来生成一个 Nix 包管理器的打包定义。相比于 builtins.derivationstdenv.mkDerivation 将打包过程分成了 7 个步骤(Phase):

    解压(Unpack phase)

    在这一步中,stdenv.mkDerivation 会自动解压 src 参数指定的源码包。例如如果你的源码包是 .tar.gz 格式的,就会自动调用 tar xf

    stdenv.mkDerivation 不能识别所有压缩格式,例如 .zip 就不行,需要手动指定解压命令:

    nativeBuildInputs = [ unzip ];
    unpackPhase = ''
      unzip $src
    

    stdenv.mkDerivation 要求源码包的顶层是一个文件夹,解压完成后会自动 cd

    配置(Configure phase)

  • 这一步相当于运行 ./configure 或者 cmakestdenv.mkDerivation 会自动检测打包方案并调用相应命令,或者当相应配置文件不存在时,自动跳过这一步。
  • 需要注意的是,要调用 cmake,你需要额外加一行 nativeBuildInputs = [ cmake ]; 把 CMake 加入打包环境中。
  • 你可以用 configureFlags 或者 cmakeFlags 添加配置参数,例如启用/禁用软件的功能。
  • 编译(Build phase)

  • 这一步相当于运行 make。你可以用 makeFlags 添加传给 make 的参数。
  • 测试(Check phase)

  • 这一步会运行源码中自带的测试用例,以保证软件功能正确。
  • 你可以用 doCheck = false; 禁用这一步。
  • 安装(Install phase)

  • 这一步相当于运行 make install,将编译结果复制到 Nix store 的相应文件夹中。
  • 整个构建过程是在临时文件夹中,而不是 Nix store 中进行的,因此需要这一步将文件复制过去。
  • 当你手动指定安装命令时,目标路径存在变量 $out 中。$out 可以是存放有文件的文件夹,也可以直接是一个文件。
  • 额外修补(Fixup phase)

  • 这一步会对 Nix store 中的结果做一些清理,例如去除调试符号等。
  • Autopatchelf Hook,一个自动替换闭源软件 .so 的路径的 Hook,就是在这一步运行的。
  • 你可以用 dontFixup = true; 禁用这一步。
  • 每一个步骤都可以手动指定对应的命令,或者在原有命令之前或之后额外增加命令。以安装这一步为例:

    preInstall = ''
      echo 这里指定在安装步骤之前运行的命令
    installPhase = ''
      # 运行 preInstall 的命令。默认的 installPhase 自带了下面这一行,但当你指定整个步骤的命令时,就需要自己加上,否则 preInstall 不会运行
      runHook preInstall
      echo 这里指定安装步骤的所有命令
      # 运行 postInstall 的命令,同理
      runHook postInstall
    postInstall = ''
      echo 这里指定在安装步骤之后运行的命令
    

    只看这些步骤的解释可能有些抽象,因此接下来我会给出一些实例,并给出详细解释。此外,我的实例中还会涉及 Nixpkgs 提供的对于几种常用编程语言的专用打包函数,例如 Python 的 buildPythonPackage,Go 的 buildGoModule 等等。这些实例都来自我的 NUR 软件源

    实例:开源软件

    开源软件的打包往往都比较容易,因为在打包过程中,Nix 包管理器会调整好环境变量,让编译器找到存放在 Nix store 中其它路径的库文件,所以生成的二进制文件都会链接到 Nix store 的库文件中,不依赖 /usr 等路径下的其它文件,可以直接在 NixOS 上使用,此外,即使开源软件中出现路径写死等情况,你在打包过程中也可以写一个补丁,把路径修改掉,从而让它能在 NixOS 下正常工作。

    简单:LibOQS(C++,CMake,自动化构建)

    首先我们来看一个最简单的例子:LibOQS。LibOQS 提供了多种后量子加密算法的实现,可以用来给 OpenSSL 或 BoringSSL 提供后量子加密支持

    LibOQS 使用 CMake 构建,并且本身没有任何依赖,因此基本上所有工作都可以由 stdenv.mkDerivations 自动完成,我们只需要为 CMake 指定几个额外的参数:

    # 当你使用 pkgs.callPackage 函数时,这里的参数会用 Nixpkgs 的软件包和函数自动填充(如果有对应的话)
    { lib
    , stdenv
    , fetchFromGitHub
    , cmake
    , ...
    } @ args:
    stdenv.mkDerivation rec {
      # 指定包名和版本
      pname = "liboqs";
      version = "0.7.1";
      # 从 GitHub 下载源代码
      src = fetchFromGitHub ({
        owner = "open-quantum-safe";
        repo = "liboqs";
        # 对应的 commit 或者 tag,注意 fetchFromGitHub 不能跟随 branch!
        rev = "0.7.1";
        # 下载 git submodules,绝大部分软件包没有这个
        fetchSubmodules = false;
        # 这里的 SHA256 校验码不会算怎么办?先注释掉,然后构建这个软件包,Nix 会报错,并提示你正确的校验码
        sha256 = "sha256-m20M4+3zsH40hTpMJG9cyIjXp0xcCUBS+cCiRVLXFqM=";
      # 并行编译,大幅加快打包速度,默认是启用的。对于极少数并行编译会失败的软件包,才需要禁用。
      enableParallelBuilding = true;
      # 如果基于 CMake 的软件包在打包时出现了奇怪的错误,可以尝试启用此选项
      # 此选项禁用了对 CMake 软件包的一些自动修正
      dontFixCmake = true;
      # 将 CMake 加入编译环境,用来生成 Makefile
      nativeBuildInputs = [ cmake ];
      # 传给 CMake 的配置参数,控制 liboqs 的功能
      cmakeFlags = [
        "-DBUILD_SHARED_LIBS=ON"
        "-DOQS_BUILD_ONLY_LIB=1"
        "-DOQS_USE_OPENSSL=OFF"
        "-DOQS_DIST_BUILD=ON"
      # stdenv.mkDerivation 自动帮你完成其余的步骤
    

    然后运行下面这行命令,Nix 包管理器就会自动构建这个软件包,并把输出链接到当前目录的 results

    nix-build -E 'with import <nixpkgs> {}; callPackage ./liboqs.nix {}'

    中等:openssl-oqs-provider(C,增加依赖)

    有了 LibOQS,我们可以再打包一个 OpenSSL OQS Provider,一个 OpenSSL 3.0 的加解密引擎,可以把后量子加密算法加入 OpenSSL 3.0 中

    { lib
    , stdenv
    , fetchFromGitHub
    , cmake
    , liboqs
    , openssl_3_0
    , python3
    , ...
    } @ args:
    stdenv.mkDerivation rec {
      pname = "openssl-oqs-provider";
      version = "ec60cde5cc894814016f821a1162fe1a4b888a75";
      src = fetchFromGitHub ({
        owner = "open-quantum-safe";
        repo = "oqs-provider";
        rev = "ec60cde5cc894814016f821a1162fe1a4b888a75";
        fetchSubmodules = false;
        sha256 = "sha256-NyT5CpQeclSJ0b4Qr4McAJXwKgy6SWiUijkAgu6TTNM=";
      enableParallelBuilding = true;
      dontFixCmake = true;
      # nativeBuildInputs 指定的是只有在构建时用到,运行时不会用到的软件包
      # 例如这里的用来生成 Makefile 的 CMake,和用来生成配置文件的 Python
      nativeBuildInputs = [
        cmake
        # 向打包环境加入 Python 和这几个包,preConfigure 中的命令需要用到
        (python3.withPackages (p: with p; [ jinja2 pyyaml tabulate ]))
      # buildInputs 指定的是运行时也会用到的软件包
      buildInputs = [
        liboqs
        openssl_3_0
      # 在配置步骤(Configure phase)之前运行的命令,用来启用所有的后量子加密算法
      preConfigure = ''
        cp ${sources.openssl-oqs.src}/oqs-template/generate.yml oqs-template/generate.yml
        sed -i "s/enable: false/enable: true/g" oqs-template/generate.yml
        LIBOQS_SRC_DIR=${sources.liboqs.src} python oqs-template/generate.py
      cmakeFlags = [ "-DCMAKE_BUILD_TYPE=Release" ];
      # 手动指定安装命令,把 oqsprovider.so 复制到 $out/lib 文件夹下
      # 一般来说可执行文件放在 $out/bin,库文件放在 $out/lib,菜单图标等放在 $out/share
      # 但并非强制,你在 $out 下随便放都可以,只不过在其它地方调用会麻烦一些
      installPhase = ''
        mkdir -p $out/lib
        install -m755 oqsprov/oqsprovider.so "$out/lib"
    

    这个包主要用来展示 nativeBuildInputsbuildInputs 的区别:

  • nativeBuildInputs 只有在构建时用到,一般用来生成一些配置文件或者编译脚本。在交叉编译(给其它架构的设备编译软件)时,nativeBuildInputs 的架构会和运行编译的设备相同,而不是和目标设备相同。例如用 x86 电脑给 ARM 树莓派编译时,nativeBuildInputs 的架构会是 x86。
  • buildInputs 在构建和最终运行软件时都会用到。所有的依赖库都会放到这里。这些依赖的架构和目标设备相同,例如 openssl-oqs-provider 依赖的 liboqs 必然和它是同一架构的(都是 x86 或者都是 ARM)。
  • 困难:OSDLyrics(Python 和 C++,两轮构建)

    接下来我们来看 OSDLyrics,一个桌面歌词软件。这个包表面上看起来很好打,官方给出的编译命令就是下面几行:

    ./autogen.sh
    ./configure --prefix=/usr PYTHON=/usr/bin/python3
    sudo make install

    但是编译命令里出现了 Python,这就比较麻烦了。OSDLyrics 由 Python 和 C++ 两部分组成,其中 C++ 部分会调用 Python 的库。因此,官方的编译脚本会把 OSDLyrics 的 Python 模块安装到 Python 的 site-packages 文件夹中。但是在 Nix 中,对于 OSDLyrics 这个软件包来说,Python 的安装目录是只读的,自然无法安装这个模块。

    因此我们需要先给 Python 模块部分单独打个包:

    { python3Packages
    , fetchFromGitHub
    , writeText
    , ...
    python3Packages.buildPythonPackage rec {
      pname = "osdlyrics";
      version = "0.5.10";
      src = fetchFromGitHub ({
        owner = "osdlyrics";
        repo = "osdlyrics";
        rev = "0.5.10";
        fetchSubmodules = false;
        sha256 = "sha256-x9gIT1JkfPIc4RmmQJLv9rOG2WqAftoTK5uiRlS65zU=";
      configurePhase =
          # 原软件包的 Python 模块部分不符合 PIP 的打包格式,需要手动加入这两个配置文件
          setupPy = writeText "setup.py" ''
            from setuptools import setup, find_packages
            setup(
              name='${pname}',
              version='${version}',
              packages=['osdlyrics', 'osdlyrics/dbusext'],
          initPy = writeText "__init__.py" ''
            PROGRAM_NAME = 'OSD Lyrics'
            PACKAGE_NAME = '${pname}'
            PACKAGE_VERSION = '${version}'
        # 把 Python 模块的文件夹改名并加入配置文件,以符合 PIP 规范
          ln -s ${setupPy} setup.py
          mv python osdlyrics
          ln -s ${initPy} osdlyrics/__init__.py
      # 禁用测试,原软件包中没有单元测试
      doCheck = false;
    

    然后把这个模块加入 OSDLyrics 最终使用的 Python 环境:

    { python3Packages
    , fetchFromGitHub
    , writeText
    , python3
    , ...
      osdlyricsPython = python3Packages.buildPythonPackage rec {
        # ...
      # 下面列出的包都是 OSDLyrics 要用到的
      python = python3.withPackages (p: with p; [
        chardet
        dbus-python
        future
        osdlyricsPython
        pycurl
        pygobject3
    # ...

    最终才能打包它的 C++ 部分:

    { ... }:
    # ...
    stdenv.mkDerivation rec {
      pname = "osdlyrics";
      version = "0.5.10";
      src = fetchFromGitHub ({
        owner = "osdlyrics";
        repo = "osdlyrics";
        rev = "0.5.10";
        fetchSubmodules = false;
        sha256 = "sha256-x9gIT1JkfPIc4RmmQJLv9rOG2WqAftoTK5uiRlS65zU=";
      nativeBuildInputs = [
        # 自动运行 autoconf,也就是 autogen.sh 做的事
        autoreconfHook
        # 生成语言文件的工具
        intltool
        # pkgconfig 被 autoconf 系列配置脚本用来查找依赖
        pkg-config
      # C++ 部分用到的依赖
      buildInputs = [
        dbus-glib
        libnotify
        # 注意这个 Python 是我们上面定义的,加了几个模块的版本
        python
      # 解决一些编译错误
      postPatch = ''
        sed -i 's/-Werror//g' configure.ac
      # autoreconfHook 会在构建步骤中加入一个 autoreconf phase,也有对应的前置/后置命令 Hook
      preAutoreconf = ''
        export AUTOPOINT=intltoolize
      # 指定用我们的加了模块的 Python
      makeFlags = [ "PYTHON=${python}/bin/python" ];
      # 删除结果中的 Python 模块部分(因为已经打包过了)
      postInstall = ''
        rm -rf $out/lib/python*
    

    最终完整的定义如下:

    { stdenv
    , lib
    , fetchFromGitHub
    , writeText
    , python3Packages
      # nativeBuildInputs
    , autoreconfHook
    , intltool
    , pkg-config
      # buildInputs
    , dbus-glib
    , gtk2
    , libnotify
    , python3
    , ...
    } @ args:
      pname = "osdlyrics";
      version = "0.5.10";
      src = fetchFromGitHub ({
        owner = "osdlyrics";
        repo = "osdlyrics";
        rev = "0.5.10";
        fetchSubmodules = false;
        sha256 = "sha256-x9gIT1JkfPIc4RmmQJLv9rOG2WqAftoTK5uiRlS65zU=";
      osdlyricsPython = python3Packages.buildPythonPackage rec {
        inherit pname version src;
        configurePhase =
            setupPy = writeText "setup.py" ''
              from setuptools import setup, find_packages
              setup(
                name='${pname}',
                version='${version}',
                packages=['osdlyrics', 'osdlyrics/dbusext'],
            initPy = writeText "__init__.py" ''
              PROGRAM_NAME = 'OSD Lyrics'
              PACKAGE_NAME = '${pname}'
              PACKAGE_VERSION = '${version}'
            ln -s ${setupPy} setup.py
            mv python osdlyrics
            ln -s ${initPy} osdlyrics/__init__.py
        doCheck = false;
      python = python3.withPackages (p: with p; [
        chardet
        dbus-python
        future
        osdlyricsPython
        pycurl
        pygobject3
    stdenv.mkDerivation rec {
      inherit pname version src;
      nativeBuildInputs = [
        autoreconfHook
        intltool
        pkg-config
      buildInputs = [
        dbus-glib
        libnotify
        python
      postPatch = ''
        sed -i 's/-Werror//g' configure.ac
      preAutoreconf = ''
        export AUTOPOINT=intltoolize
      makeFlags = [ "PYTHON=${python}/bin/python" ];
      postInstall = ''
        rm -rf $out/lib/python*
    

    实例:闭源软件(以及以二进制形式分发的软件)

    比起开源软件,给闭源软件打包就比较困难了。这些闭源软件往往只提供二进制文件,而这些二进制文件往往是提供给传统的、使用 FHS 标准目录结构的 Linux 发行版的,例如 CentOS、Debian、Ubuntu 等。由于我们没有源代码,我们只能想办法在二进制文件上动手术,在二进制文件中查找 FHS 标准路径,并把它们全部替换成 Nix store 的路径。

    幸运的是,针对不同的情况,Nixpkgs 提供了好几种方案,让多数的闭源软件都能打包成功。

    简单:Bilibili-linux(解压 DEB 包,Electron)

    首先我们看一个简单的情况:基于 Electron 的软件。这里以 Bilibili-linux 为例,它是基于哔哩哔哩官方的桌面客户端移植到 Linux 系统的版本

    虽然 Electron 软件相比传统的基于 GTK 或 Qt 的桌面软件耗电大,占用空间多,而且会让每台电脑中都装上十来个 Chromium,让它的市场占有率飙升到 1000% 以上,但它的移植便捷性不容忽视。Bilibili-linux 这个客户端是使用纯 Javascript 实现的,软件包里除了 Electron 之外,没有任何其它的二进制文件。因此我们可以取出它的 Javascript 代码,然后直接用系统的 Electron 运行。

    { stdenv
    , fetchurl
    , electron
    , lib
    , makeWrapper
    , ...
    } @ args:
    ################################################################################
    # Mostly based on bilibili-bin package from AUR:
    # https://aur.archlinux.org/packages/bilibili-bin
    ################################################################################
    stdenv.mkDerivation rec {
      pname = "bilibili";
      version = "1.2.1-1";
      src = fetchurl {
        url = "https://github.com/msojocs/bilibili-linux/releases/download/v1.2.1-1/io.github.msojocs.bilibili_1.2.1-1_amd64.deb";
        sha256 = "sha256-t/igezm0ipkOkKION8qTYGK9f6qI3c4iPuS/wWrMywQ=";
      # 解压 DEB 包
      unpackPhase = ''
        ar x ${src}
        tar xf data.tar.xz
      # makeWrapper 可以自动生成一个调用其它命令的命令(也就是 wrapper),并且可以在原命令上修改参数、环境变量等
      buildInputs = [ makeWrapper ];
      installPhase = ''
        mkdir -p $out/bin
        # 替换菜单项目(desktop 文件)中的路径
        cp -r usr/share $out/share
        sed -i "s|Exec=.*|Exec=$out/bin/bilibili|" $out/share/applications/*.desktop
        # 复制出客户端的 Javascript 部分,其它的不要了
        cp -r opt/apps/io.github.msojocs.bilibili/files/bin/app $out/opt
        # 生成 bilibili 命令,运行这个命令时会调用 electron 加载客户端的 Javascript 包($out/opt/app.asar)
        makeWrapper ${electron}/bin/electron $out/bin/bilibili \
          --argv0 "bilibili" \
          --add-flags "$out/opt/app.asar"
    

    中等:DingTalk(自动 Patch 二进制,查找依赖)

    当然,不是所有闭源软件都用的是 Electron 方案。对于有二进制文件的闭源软件,我们就需要在二进制文件上动刀了,把它的依赖库文件全部改成 Nix store 里的库。Nixpkgs 提供了一个方便的工具 autoPatchelfHook,它会搜索软件包里的所有二进制,并修改所有的依赖路径,当有依赖路径没被满足时会自动报错,方便调试。

    我们这次用的例子是 DingTalk,钉钉的 Linux 客户端,它使用 GTK 作为界面框架。由于我们一开始不知道钉钉有什么依赖,我们先编写一个大致的打包模版:

    { stdenv
    , fetchurl
    , autoPatchelfHook
    , makeWrapper
    , lib
    , callPackage
    , ...
    } @ args:
    ################################################################################
    # Mostly based on dingtalk-bin package from AUR:
    # https://aur.archlinux.org/packages/dingtalk-bin
    ################################################################################
    stdenv.mkDerivation rec {
      pname = "dingtalk";
      version = "1.4.0.20425";
      src = fetchurl {
        url = "https://dtapp-pub.dingtalk.com/dingtalk-desktop/xc_dingtalk_update/linux_deb/Release/com.alibabainc.dingtalk_${version}_amd64.deb";
        sha256 = "sha256-UKkFuuFK/Ae+XIWbPYYsqwS/FOJfOqm9e1i18JB8UfA=";
      # autoPatchelfHook 可以自动修改二进制文件
      nativeBuildInputs = [ autoPatchelfHook makeWrapper ];
      unpackPhase = ''
        ar x ${src}
        tar xf data.tar.xz
        mv opt/apps/com.alibabainc.dingtalk/files/version version
        mv opt/apps/com.alibabainc.dingtalk/files/*-Release.* release
        # 删除一些可以用系统库替代的库文件,和没用的 exe 等文件
        rm -rf release/Resources/{i18n/tool/*.exe,qss/mac}
        rm -f release/{*.a,*.la,*.prl}
        rm -f release/dingtalk_updater
        rm -f release/libgtk-x11-2.0.so.*
        rm -f release/libm.so.*
      installPhase = ''
        mkdir -p $out
        mv version $out/
        # 有些库文件必须使用钉钉自带的版本
        mv release $out/lib
        # 这里的 desktop 文件和图标是从 AUR 拿的
        mkdir -p $out/share/applications $out/share/pixmaps
        ln -s ${./dingtalk.desktop} $out/share/applications/dingtalk.desktop
        ln -s ${./dingtalk.png} $out/share/pixmaps/dingtalk.png
    

    然后尝试打包。不出所料,报错了:

    # ...
    > auto-patchelf failed to find all the required dependencies.
    > Add the missing dependencies to --libs or use `--ignore-missing="foo.so.1 bar.so etc.so"`.
    For full logs, run 'nix log /nix/store/gm3d0jm6l19ypcz6vfmv5hmx8d9iygr1-dingtalk-1.4.0.20425.drv'.

    我们运行上面这行命令查看完整的日志:

    # ...
    error: auto-patchelf could not satisfy dependency libX11.so.6 wanted by /nix/store/w179pb9w545rwnhvv0kkcjvra0gv82sp-dingtalk-1.4.0.20425/lib/cefclient
    error: auto-patchelf could not satisfy dependency libgtk-x11-2.0.so.0 wanted by /nix/store/w179pb9w545rwnhvv0kkcjvra0gv82sp-dingtalk-1.4.0.20425/lib/cefclie
    error: auto-patchelf could not satisfy dependency libgdk_pixbuf-2.0.so.0 wanted by /nix/store/w179pb9w545rwnhvv0kkcjvra0gv82sp-dingtalk-1.4.0.20425/lib/cefc
    lient
    error: auto-patchelf could not satisfy dependency libgobject-2.0.so.0 wanted by /nix/store/w179pb9w545rwnhvv0kkcjvra0gv82sp-dingtalk-1.4.0.20425/lib/cefclie
    error: auto-patchelf could not satisfy dependency libglib-2.0.so.0 wanted by /nix/store/w179pb9w545rwnhvv0kkcjvra0gv82sp-dingtalk-1.4.0.20425/lib/cefclient
    # ...

    autoPatchelfHook 已经列出了所有缺失的库文件,接下来,我们需要一个一个查找这些库文件对应的软件包,并把它们加入软件包的依赖 buildInputs 中。你可以根据自己的经验在 NixOS Search 上查找软件包,也可以使用 nix-index,一个根据文件名搜包的工具,来加快查找速度。

    最后添加完后,DingTalk 包的定义是这样的:

    { stdenv
    , fetchurl
    , autoPatchelfHook
    , makeWrapper
    , lib
    , callPackage
      # DingTalk dependencies
    , alsa-lib
    , at-spi2-atk
    , at-spi2-core
    , cairo
    , cups
    , dbus
    , e2fsprogs
    , gdk-pixbuf
    , glib
    , gnutls
    , graphite2
    , gtk2
    , krb5
    , libdrm
    , libgcrypt
    , libGLU
    , libpulseaudio
    , libthai
    , libxkbcommon
    , mesa_drivers
    , nspr
    , nss
    , rtmpdump
    , udev
    , util-linux
    , xorg
    , ...
    } @ args:
    ################################################################################
    # Mostly based on dingtalk-bin package from AUR:
    # https://aur.archlinux.org/packages/dingtalk-bin
    ################################################################################
      version = "1.4.0.20425";
      # 钉钉依赖旧版本的 OpenLDAP,openldap-2_4.nix 这个定义可以在我的 NUR 找到
      openldap = callPackage ./openldap-2_4.nix { };
      libraries = [
        alsa-lib
        at-spi2-atk
        at-spi2-core
        cairo
        e2fsprogs
        gdk-pixbuf
        gnutls
        graphite2
        libdrm
        libgcrypt
        libGLU
        libpulseaudio
        libthai
        libxkbcommon
        mesa_drivers
        openldap
        rtmpdump
        util-linux
        xorg.libICE
        xorg.libSM
        xorg.libX11
        xorg.libxcb
        xorg.libXcomposite
        xorg.libXcursor
        xorg.libXdamage
        xorg.libXext
        xorg.libXfixes
        xorg.libXi
        xorg.libXinerama
        xorg.libXmu
        xorg.libXrandr
        xorg.libXrender
        xorg.libXScrnSaver
        xorg.libXt
        xorg.libXtst
    stdenv.mkDerivation rec {
      pname = "dingtalk";
      inherit version;
      src = fetchurl {
        url = "https://dtapp-pub.dingtalk.com/dingtalk-desktop/xc_dingtalk_update/linux_deb/Release/com.alibabainc.dingtalk_${version}_amd64.deb";
        sha256 = "sha256-UKkFuuFK/Ae+XIWbPYYsqwS/FOJfOqm9e1i18JB8UfA=";
      nativeBuildInputs = [ autoPatchelfHook makeWrapper ];
      buildInputs = libraries;
      unpackPhase = ''
        ar x ${src}
        tar xf data.tar.xz
        mv opt/apps/com.alibabainc.dingtalk/files/version version
        mv opt/apps/com.alibabainc.dingtalk/files/*-Release.* release
        # Cleanup
        rm -rf release/Resources/{i18n/tool/*.exe,qss/mac}
        rm -f release/{*.a,*.la,*.prl}
        rm -f release/dingtalk_updater
        rm -f release/libgtk-x11-2.0.so.*
        rm -f release/libm.so.*
      installPhase = ''
        mkdir -p $out
        mv version $out/
        # Move libraries
        # DingTalk relies on (some of) the exact libraries it ships with
        mv release $out/lib
        # Entrypoint
        mkdir -p $out/bin
        # 因为钉钉客户端会在运行过程中动态加载库文件,所以要把所有的依赖项加入 LD_LIBRARY_PATH,让钉钉客户端能找到
        makeWrapper $out/lib/com.alibabainc.dingtalk $out/bin/dingtalk \
          --argv0 "com.alibabainc.dingtalk" \
          --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath libraries}"
        # App Menu
        mkdir -p $out/share/applications $out/share/pixmaps
        ln -s ${./dingtalk.desktop} $out/share/applications/dingtalk.desktop
        ln -s ${./dingtalk.png} $out/share/pixmaps/dingtalk.png
    

    困难:SVP(程序检测自身完整性,Bubblewrap)

    以钉钉客户端为例的闭源软件虽然打包麻烦,需要手动查找所有的依赖库,反复测试,但至少软件本身不会给你下绊子。有些闭源软件为了防止破解,会检测自身的完整性,只要自己的二进制文件被修改就拒绝启动,例如 SVP 视频补帧软件

    对于这些软件,autoPatchelfHook 自然用不了了。因此我们只能换成另一种办法:生成一个符合 FHS 标准的虚拟环境,把所有的库文件放在虚拟环境中对应的路径,然后在虚拟环境中启动软件。最常用的创建虚拟环境的软件是 Bubblewrap,它原本的用途是把软件放在沙盒中,阻止它读取敏感数据,但这个沙盒正好也可以是我们要用的虚拟环境。

    我们直接来看 SVP 的打包定义:

    { stdenv
    , bubblewrap
    , fetchurl
      # SVP 的所有依赖
    , ffmpeg
    , glibc
    , gnome
    , lib
    , libmediainfo
    , libsForQt5
    , libusb1
    , lsof
    , makeWrapper
    , mpv-unwrapped
      # NVIDIA 驱动,SVP 需要调用其中的一个库来支持 N 卡光流加速
      # 在 N 卡系统上需要用户手动 override 成自己的驱动版本
      # 在非 N 卡系统上可以设置成 null
    , nvidia_x11 ? null
    , ocl-icd
    , p7zip
    , patchelf
    , vapoursynth
    , wrapMpv
    , writeShellScript
    , writeText
    , xdg-utils
    , xorg
    , ...
    ################################################################################
    # Based on svp package from AUR:
    # https://aur.archlinux.org/packages/svp
    ################################################################################
      # 打包一个加了 N 卡光流库,并开启 Vapoursynth 视频处理引擎的 MPV
      mpvForSVP = wrapMpv
        (mpv-unwrapped.override {
          vapoursynthSupport = true;
          extraMakeWrapperArgs = lib.optionals (nvidia_x11 != null) [
            "--prefix"
            "LD_LIBRARY_PATH"
            "${lib.makeLibraryPath [ nvidia_x11 ]}"
      # SVP 主程序的依赖
      libPath = lib.makeLibraryPath [
        libsForQt5.qtbase
        libsForQt5.qtdeclarative
        libsForQt5.qtscript
        libsForQt5.qtsvg
        libmediainfo
        libusb1
        xorg.libX11
        stdenv.cc.cc.lib
        ocl-icd
        vapoursynth
      # SVP 查找二进制程序的路径(即 PATH 环境变量)
      execPath = lib.makeBinPath [
        ffmpeg.bin
        gnome.zenity
        xdg-utils
      svp-dist = stdenv.mkDerivation rec {
        pname = "svp-dist";
        version = "4.5.210";
        src = fetchurl {
          url = "https://www.svp-team.com/files/svp4-linux.${version}-1.tar.bz2";
          sha256 = "10q8r401wg81vanwxd7v07qrh3w70gdhgv5vmvymai0flndm63cl";
        nativeBuildInputs = [ p7zip patchelf ];
        # 禁用修补步骤(Fixup phase),它会修改 SVP 二进制文件,导致完整性校验报错
        dontFixup = true;
        # 解压、安装步骤来自 AUR:https://aur.archlinux.org/packages/svp-bin
        unpackPhase = ''
          tar xf ${src}
        buildPhase = ''
          mkdir installer
          LANG=C grep --only-matching --byte-offset --binary --text  $'7z\xBC\xAF\x27\x1C' "svp4-linux-64.run" |
            cut -f1 -d: |
            while read ofs; do dd if="svp4-linux-64.run" bs=1M iflag=skip_bytes status=none skip=$ofs of="installer/bin-$ofs.7z"; done
        installPhase = ''
          mkdir -p $out/opt
          for f in "installer/"*.7z; do
            7z -bd -bb0 -y x -o"$out/opt/" "$f" || true
          for SIZE in 32 48 64 128; do
            mkdir -p "$out/share/icons/hicolor/''${SIZE}x''${SIZE}/apps"
            mv "$out/opt/svp-manager4-''${SIZE}.png" "$out/share/icons/hicolor/''${SIZE}x''${SIZE}/apps/svp-manager4.png"
          rm -f $out/opt/{add,remove}-menuitem.sh
      # 创建一个使用 Bubblewrap 的启动脚本
      startScript = writeShellScript "SVPManager" ''
        # 除了这些路径以外,其它的根目录下的路径都映射进虚拟环境
        # 这里的有些路径不是完全不映射,而是在下面有更细粒度的映射配置
        blacklist=(/nix /dev /usr /lib /lib64 /proc)
        declare -a auto_mounts
        # loop through all directories in the root
        for dir in /*; do
          # if it is a directory and it is not in the blacklist
          if [[ -d "$dir" ]] && [[ ! "''${blacklist[@]}" =~ "$dir" ]]; then
            # add it to the mount list
            auto_mounts+=(--bind "$dir" "$dir")
        # Bubblewrap 启动脚本
        cmd=(
          ${bubblewrap}/bin/bwrap
          # /dev 需要特殊的映射方式
          --dev-bind /dev /dev
          # 在虚拟环境中也切换到当前文件夹
          --chdir "$(pwd)"
          # Bubblewrap 退出时杀掉虚拟环境里的所有进程
          --die-with-parent
          # /nix 目录只读
          --ro-bind /nix /nix
          # /proc 需要特殊的映射方式
          --proc /proc
          # 把 Glibc 放到 /lib 和 /lib64,让 SVP 加载
          --bind ${glibc}/lib /lib
          --bind ${glibc}/lib /lib64
          # 一些 SVP 需要用到的命令,SVP 固定去 /usr/bin 查找这些命令
          --bind /usr/bin/env /usr/bin/env
          --bind ${ffmpeg.bin}/bin/ffmpeg /usr/bin/ffmpeg
          --bind ${lsof}/bin/lsof /usr/bin/lsof
          # 配置环境变量,包括查找命令和库的路径
          --setenv PATH "${execPath}:''${PATH}"
          --setenv LD_LIBRARY_PATH "${libPath}:''${LD_LIBRARY_PATH}"
          # 把 SVP 专用的 MPV 播放器映射过来
          --symlink ${mpvForSVP}/bin/mpv /usr/bin/mpv
          # 映射其它根目录下的路径
          "''${auto_mounts[@]}"
          # 虚拟环境启动后运行 SVP 主程序
          ${svp-dist}/opt/SVPManager "$@"
        exec "''${cmd[@]}"
      # SVP 菜单项
      desktopFile = writeText "svp-manager4.desktop" ''
        [Desktop Entry]
        Version=1.0
        Encoding=UTF-8
        Name=SVP 4 Linux
        GenericName=Real time frame interpolation
        Type=Application
        Categories=Multimedia;AudioVideo;Player;Video;
        MimeType=video/x-msvideo;video/x-matroska;video/webm;video/mpeg;video/mp4;
        Terminal=false
        StartupNotify=true
        Exec=${startScript} %f
        Icon=svp-manager4.png
    # 创建一个简单的包,只包含启动脚本和菜单项
    stdenv.mkDerivation {
      pname = "svp";
      inherit (svp-dist) version;
      phases = [ "installPhase" ];
      installPhase = ''
        mkdir -p $out/bin $out/share/applications
        ln -s ${startScript} $out/bin/SVPManager
        ln -s ${desktopFile} $out/share/applications/svp-manager4.desktop
        ln -s ${svp-dist}/share/icons $out/share/icons
    

    困难:WeChat-UOS(程序检测运行环境,Steam-run)

    另一个会检测运行环境的是 UOS 版微信客户端。虽然它本身是一个 Electron 应用,打包应该很简单,但是它自带了一个库文件,包含有检测 UOS 系统授权文件的逻辑,检测失败就拒绝你登录。因此,我们依然需要构造一个虚拟环境,把 UOS 的授权文件放到对应的位置,才能正常使用微信。

    这里展示 Nixpkgs 中的一个便捷打包工具:steam-runsteam-run 本身就是调用的 Bubblewrap,但是顾名思义,steam-run 原本是用来运行 Steam 客户端和 Steam 上的游戏的,因此它的默认环境包含了大量常用的库文件,很多闭源软件都能用它跑起来。

    { stdenv
    , fetchurl
    , writeShellScript
    , electron
    , steam
    , lib
    , scrot
    , ...
    } @ args:
    ################################################################################
    # Mostly based on wechat-uos package from AUR:
    # https://aur.archlinux.org/packages/wechat-uos
    ################################################################################
      version = "2.1.4";
      # UOS 授权文件,从 AUR 下载:https://aur.archlinux.org/packages/wechat-uos
      license = stdenv.mkDerivation rec {
        pname = "wechat-uos-license";
        version = "0.0.1";
        src = ./license.tar.gz;
        installPhase = ''
          mkdir -p $out
          cp -r etc var $out/
      # 微信软件包,和 B 站客户端一样只保留 Javascript 部分和几个需要的库
      resource = stdenv.mkDerivation rec {
        pname = "wechat-uos-resource";
        inherit version;
        src = fetchurl {
          url = "https://home-store-packages.uniontech.com/appstore/pool/appstore/c/com.tencent.weixin/com.tencent.weixin_${version}_amd64.deb";
          sha256 = "sha256-V74m+dFK9/f0QoHfvIjk7hyIil6FpV9HGkPqwJLvQhM=";
        unpackPhase = ''
          ar x ${src}
        installPhase = ''
          mkdir -p $out
          tar xf data.tar.xz -C $out
          mv $out/usr/* $out/
          mv $out/opt/apps/com.tencent.weixin/files/weixin/resources/app $out/lib/wechat-uos
          chmod 0644 $out/lib/license/libuosdevicea.so
          rm -rf $out/opt $out/usr
          # use system scrot
          pushd $out/lib/wechat-uos/packages/main/dist/
          sed -i 's|__dirname,"bin","scrot"|"${scrot}/bin/"|g' index.js
      # 生成一个 Steam-run 虚拟环境,这里包含了 UOS 授权文件和微信软件包
      steam-run = (steam.override {
        extraPkgs = p: [ license resource ];
        runtimeOnly = true;
      }).run;
      # 微信启动脚本
      startScript = writeShellScript "wechat-uos" ''
        # 目前版本的微信在 NixOS 上无法显示托盘图标,如果关掉窗口有可能就再也找不到了
        # 因此如果微信运行着,就直接把它杀掉,这样就可以重新启动微信了
        wechat_pid=`pidof wechat-uos`
        if test $wechat_pid; then
            kill -9 $wechat_pid
        # 用 Steam-run 在虚拟环境中启动微信
        ${steam-run}/bin/steam-run \
          ${electron}/bin/electron \
          ${resource}/lib/wechat-uos
    # 创建一个简单的包,只包含启动脚本和菜单项
    stdenv.mkDerivation {
      pname = "wechat-uos";
      inherit version;
      phases = [ "installPhase" ];
      installPhase = ''
        mkdir -p $out/bin $out/share/applications
        ln -s ${startScript} $out/bin/wechat-uos
        ln -s ${./wechat-uos.desktop} $out/share/applications/wechat-uos.desktop
        ln -s ${resource}/share/icons $out/share/icons
    

    steam-run 虽然好用,但因为它为了支持大量的 Steam 游戏默认引入了大量的库文件,如果只为了运行一些简单的程序,不免有些大材小用。因此我建议,对于简单的软件尽量用 Bubblewrap 手动打包,对于复杂的软件再用 steam-run

    实例:特殊软件包

    最后,我演示几种特殊软件的打包。

    字体:Hoyo-Glyphs

    NixOS 中的字体也是一个个软件包,只要把 TTF 文件放进软件包的 $out/share/fonts/opentype 文件夹就可以了。

    Hoyo-Glyphs 演示,它是一个由米哈游游戏爱好者创建的字体项目,模仿了米哈游的原神、星穹铁道、绝区零等游戏内的架空文字

    { stdenvNoCC
    , lib
    , fetchFromGitHub
    , ...
    } @ args:
    # stdenvNoCC 是一个没有编译器的打包环境,毕竟我们打包字体也用不到编译器
    stdenvNoCC.mkDerivation rec {
      pname = "hoyo-glyphs";
      version = "b2bf17cd3d9637fbf55c23bf46fe380e4f7e0739";
      src = fetchFromGitHub ({
        owner = "SpeedyOrc-C";
        repo = "Hoyo-Glyphs";
        rev = "b2bf17cd3d9637fbf55c23bf46fe380e4f7e0739";
        fetchSubmodules = false;
        sha256 = "sha256-7Jx/7z3QxAi7lsV3JFwUDWJUpaKOmfZyGKL3MUrUopw=";
      # 查找所有的 otf 字体文件,复制到 $out/share/fonts/opentype 目录下
      # 这样做是因为 hoyo-glyphs 项目中字体散布在多个文件夹下
      installPhase = ''
        mkdir -p $out/share/fonts/opentype/
        cp font/**/*.otf $out/share/fonts/opentype/
    

    最后把这个软件包加入 NixOS 的字体配置(或者 Home-Manager 的字体配置),就可以使用了:

    hoyo-glyphs = pkgs.callPackage ./hoyo-glyphs.nix { }; fonts.fonts = [ hoyo-glyphs

    Go 软件包:Konnect

    接下来我演示一下 Go 软件包的打包。Nixpkgs 提供了 buildGoModule 函数,可以几乎全自动地给 Go 语言软件打包。但是 buildGoModule 存在一个问题:由于 Go 语言程序需要联网下载 vendor 目录下的依赖,因此 buildGoModule 会计算整个 vendor 目录的校验码,这个校验码需要在打包时手动给出。

    校验码不会算怎么办?老方法,先注释掉(或者随便改几个字),然后构建这个软件包,Nix 会报错,并提示你正确的校验码。

    这里我演示的软件是 Konnect,一个 OpenID 单点登录服务,支持 LDAP 后端

    { fetchFromGitHub
    , buildGoModule
    buildGoModule rec {
      pname = "konnect";
      version = "v0.34.0";
      src = fetchFromGitHub ({
        owner = "Kopano-dev";
        repo = "konnect";
        rev = "v0.34.0";
        fetchSubmodules = false;
        sha256 = "sha256-y7SD+czD/jK/m0LbFq7qGjwJgBIXfTNrdsA3pzgD2xE=";
      vendorSha256 = "sha256-ZrwFUZDTbJx5qvloVOa5qK1ykKNkUn1hjfz0xf+8sWk=";
    

    你不需要指定任何的编译命令,buildGoModule 会自动完成一切。

    类似的,Python,NodeJS,Rust 等多种语言的项目都有它们对应的打包函数,具体用法可以在 NixOS Wiki 查找

    但是不包括 Java,因为 Java 常用的 Maven 构建系统不支持把依赖固定在某一个版本,因此两次编译的依赖可能会发生变化,违反了 Nix 的初衷。

    内核:linux-xanmod-lantian

    最后,我演示一下如何自定义 Linux 内核。Nixpkgs 照例提供了方便的 buildLinux 函数:

    { pkgs
    , stdenv
    , lib
    , fetchFromGitHub
    , buildLinux
    , ...
    } @ args:
      version = "5.17.14";
      release = "1";
    buildLinux {
      inherit stdenv lib version;
      src = fetchFromGitHub {
        owner = "xanmod";
        repo = "linux";
        rev = "${version}-xanmod${release}";
        sha256 = "sha256-OutD9Z/4LMT1cNmpq5fHaJZzU6iMDoj2N8GXFvXkECY=";
      # 指定内核模块文件夹的名称,我修改了 CONFIG_LOCALVERSION,因此这里要一同修改
      modDirVersion = "${version}-xanmod${release}-lantian";
      # 从 config.nix 中加载配置
      structuredExtraConfig = import ./config.nix args;
      # 在内核上打的补丁,我这里打了 Nixpkgs 自带的两个补丁,和 patches 目录下的所有补丁(自动检测)
      kernelPatches = [
        pkgs.kernelPatches.bridge_stp_helper
        pkgs.kernelPatches.request_key_helper
      ] ++ (builtins.map
        (name: {
          inherit name;
          patch = ./patches + "/${name}";
        (builtins.attrNames (builtins.readDir ./patches)));
      # 只允许给 x86_64 平台打包
      extraMeta.broken = !stdenv.hostPlatform.isx86_64;
    

    config.nix 存放你自定义的配置,Nixpkgs 会在 NixOS 默认内核配置的基础上应用你的修改。

    { lib, ... }:
    with lib.kernel;
      # 指定一个字符串:使用 freeform
      LOCALVERSION = freeform "-lantian";
      # 指定一个数字:也是使用 freeform,把数字写成字符串
      LOG_BUF_SHIFT = freeform "12";
      # 编译成模块:使用 module
      # 如果和默认配置冲突,可以用 lib.mkForce 强制应用
      TCP_CONG_CUBIC = lib.mkForce module;
      # 编译进内核:使用 yes
      TCP_CONG_BBR = yes;
      # 禁用:使用 no
      CRYPTO_842 = no;
    

    软件打包一向是困难的,在打包过程中,你往往需要考虑软件的所有依赖,并且调整参数反复尝试。相比于其它发行版,NixOS(以及 Nixpkgs)的打包看起来复杂,但实际上是比较容易的:

  • 大量的重复工作被以函数的形式自动化;
  • 打包环境与主系统隔离,不用担心系统上的残留库文件产生冲突,也不用担心少指定依赖导致其他人无法使用。
  • 本文中我展示了常见的几种打包情况,包括开源软件和闭源软件。但因为我展示的样本很少,无法覆盖到你会遇到的所有情况,因此更多时候还是需要你去自行查阅资料:

  • NixOS Wiki 上有多种常见编程语言的打包教程,以及一些特殊情况的介绍(例如 Qt)。
  • Nixpkgs 本身就是一个大型的软件包仓库,存放了 8 万多个软件包的定义,也可以作为参考。
  •