NixOS 系列文章目录:
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 游戏平台以及它上面的游戏,但它也可以用于其它闭源软件。
- 一种是 Autopatchelf,自动修改二进制文件中的库文件路径,将其指向
- 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 { };
in [
examplePackage
];
}
如果你要单独尝试构建这个软件包,你可以使用以下命令:
nix-build -E 'with import <nixpkgs> {}; callPackage ./example-package.nix {}'
打包流程
虽然你可以直接调用 Nix 包管理器内置的 builtins.derivation
函数进行打包,但我们一般用更为方便的 stdenv.mkDerivation
函数来生成一个 Nix 包管理器的打包定义。相比于 builtins.derivation
,stdenv.mkDerivation
将打包过程分成了 7 个步骤(Phase):
-
解压(Unpack phase)
-
在这一步中,
stdenv.mkDerivation
会自动解压src
参数指定的源码包。例如如果你的源码包是.tar.gz
格式的,就会自动调用tar xf
。 -
但
stdenv.mkDerivation
不能识别所有压缩格式,例如.zip
就不行,需要手动指定解压命令:nativeBuildInputs = [ unzip ]; unpackPhase = '' unzip $src '';
-
stdenv.mkDerivation
要求源码包的顶层是一个文件夹,解压完成后会自动cd
进去。
-
-
打补丁(Patch phase)
- 在这一步中,
stdenv.mkDerivation
会按顺序应用patches
列表中的所有补丁。这一步可以用来解决一部分软件和 NixOS 的不兼容问题。
- 在这一步中,
-
配置(Configure phase)
- 这一步相当于运行
./configure
或者cmake
。stdenv.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"
'';
}
这个包主要用来展示 nativeBuildInputs
和 buildInputs
的区别:
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
make
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 =
let
# 原软件包的 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}'
'';
in
# 把 Python 模块的文件夹改名并加入配置文件,以符合 PIP 规范
''
ln -s ${setupPy} setup.py
mv python osdlyrics
ln -s ${initPy} osdlyrics/__init__.py
'';
# 禁用测试,原软件包中没有单元测试
doCheck = false;
}
然后把这个模块加入 OSDLyrics 最终使用的 Python 环境:
{ python3Packages
, fetchFromGitHub
, writeText
, python3
, ...
}:
let
osdlyricsPython = python3Packages.buildPythonPackage rec {
# ...
};
# 下面列出的包都是 OSDLyrics 要用到的
python = python3.withPackages (p: with p; [
chardet
dbus-python
future
mpd2
osdlyricsPython
pycurl
pygobject3
]);
in
# ...
最终才能打包它的 C++ 部分:
{ ... }:
let
# ...
in
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
gtk2
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:
let
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 =
let
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}'
'';
in
''
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
mpd2
osdlyricsPython
pycurl
pygobject3
]);
in
stdenv.mkDerivation rec {
inherit pname version src;
nativeBuildInputs = [
autoreconfHook
intltool
pkg-config
];
buildInputs = [
dbus-glib
gtk2
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
nt
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
nt
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
################################################################################
let
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
cups
dbus
e2fsprogs
gdk-pixbuf
glib
gnutls
graphite2
gtk2
krb5
libdrm
libgcrypt
libGLU
libpulseaudio
libthai
libxkbcommon
mesa_drivers
nspr
nss
openldap
rtmpdump
udev
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
];
in
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
################################################################################
let
# 打包一个加了 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
lsof
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
done
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"
done
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")
fi
done
# 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
'';
in
# 创建一个简单的包,只包含启动脚本和菜单项
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-run
。steam-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
################################################################################
let
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
popd
'';
};
# 生成一个 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
fi
# 用 Steam-run 在虚拟环境中启动微信
${steam-run}/bin/steam-run \
${electron}/bin/electron \
${resource}/lib/wechat-uos
'';
in
# 创建一个简单的包,只包含启动脚本和菜单项
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 的字体配置),就可以使用了:
let
hoyo-glyphs = pkgs.callPackage ./hoyo-glyphs.nix { };
in
{
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:
let
version = "5.17.14";
release = "1";
in
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 万多个软件包的定义,也可以作为参考。
- NUR 是 Nix 用户个人管理的软件包仓库,类似于 AUR。
本文所有的打包样例都来自我的 NUR 仓库。