入坑 Emacs 块一年半了,入坑 NixOS 也有几个月了。入坑 NixOS 的第一时间
我就尝试用 Nix 管理 Emacs 的包,对于 melpa 中的包来说,这是十分简单的,
但是对于像 Emacs Application Framework 这种,不在 melpa 中,没有现成的
Nix 包(居然没人打包?),还伴有 Python 和 NodeJS 之类的复杂依赖的包,
就十分困难了。所以我一开始只能在 NixOS 中配置 Python 和 NodeJS 环境,
然后 Git clone EAF 仓库,手动构建。但是这并不是“Nix way”,不符合了声明
式配置的初衷,不够优雅。因此我最近花了“一点”时间学习并实践,途中遇到了
许多疑惑与困难;在此不得不感叹目前网上关于 Nix 打包的讨论是真的少,中
文内容更少(悲)。于是我打算分享一下我写 Nix 程序并 debug 的经验,而且
我没找到一个合适的 NixOS 中文社区,考虑了一下发现还是 Emacs 社区最合适。
本文旨在让完全不了解 Nix 的友人们能快速地理解 Nix 语言、derivation 的
工作原理以及 debug 方法。本人资历尚浅,提出的方法未必是(大概率不是)
“最佳实践”,也无法保证没有知识性错误,还请各位批判性地阅读。
参考文献/推荐阅读:
7 个赞
首先我们极其精简地介绍一下 Nix 最基础的部分语法。
属性集:使用大括号包围,内容为零个或多个键-值对,每个键-值对结尾需要分
号。示例:{ a = 1; b = 2; }
列表:使用中括号包围,内容为零个或多个项,项之间用空格(或换行)分隔。
示例:[ 1 2 ]
抽象(通常叫函数定义,但我更喜欢 lambda 演算中的叫法):使用冒号分隔形
参和函数体(返回值),形参为单个符号或属性集匹配符。示例:x: x + 1
,
{ x, y }: x + y
应用(通常叫函数调用):函数 参数,如果函数或参数最外面是括号,也可以
不要空格直接将二者挨在一起。示例:(x: x + 1) 5
, ({ x, y }: x + y) { x = 3; y = 4}
下一节介绍在 NixOS 中安装、配置软件的基础操作。
要在 NixOS 中安装软件,首先在官网 NixOS Search 上搜
索选项,例如搜索 firefox,发现有选项programs.firefox.enable
,在配置
文件中设置其为 true 即可安装。并且通常除了 enable 之外,还会搜索到其他
选项,例如programs.firefox.autoConfigFiles
,就可以顺便把软件的配置工
作也给完成了。
如果没有选项,那就在 NixOS Search (其实这和上面那
个不是同一个超链接)上搜索包,例如搜索 ripgrep,发现没有选项,但是有包,
那么就在配置文件中加上environment.systemPackages = [ pkgs.ripgrep ]
即可安装,但是配置就没有选项那么方便。
如果都没有,那就自己打包吧(悲)。
还有一点要注意一下,我们谈到的 Emacs 的包,并不是独立运行的软件,而是
Emacs 的插件,虽然插件也是包,也能搜索到(例如 emacsPackages.magit),
但是将其放在environment.systemPackages
中是没有效果的,正确做法是用
withPackages
函数去“修改”原始软件,形如emacs-gtk.withPackages (epkgs: [ epkgs.magit ])
,这里的参数是一个函数,参数我命名为 epkgs,
可以认为它相当于 pkgs.emacsPackages,所以函数返回列表中直接使用
epkgs.magit,一般不写 pkgs.emacsPackages.magit。下一步再将修改后的
emacs-gtk 放入environment.systemPackages
。有时候会有相关选项方便我们
操作,例如home-manager 提供了programs.emacs.extraPackages
选项,将
epkgs: [ epkgs.magit ]
放到这里即可。
下一节将介绍为 Nix 打包的基础知识。
在 Nix 中,打包就是制作 derivation,所谓 derivation 就是描述一个包的构
建过程,实践中通常使用 Nix 自带的各种mkDerivation
函数,例如
stdenv.mkDerivation
。
Nix 构建一个包的大致思路是:将“源”放到一个“临时路径”(然后可能要“打补
丁”),然后“配置”与“构建”(编译就在这里完成),接着“安装”到“最终路径”
(最后可能还要“修补”一下)。
下一节将举例说明这个构建的流程。
rime-japanese 是 rime 输入法的一个插件,在 NixOS 官网可以搜到它的包,
这个包足够简单,适合作为入门教材。在官网中,我们点击这个包下的 source
按钮查看其 derivation 的源码,发现是这样的:
{
lib,
stdenvNoCC,
fetchFromGitHub,
}:
stdenvNoCC.mkDerivation {
pname = "rime-japanese";
version = "0-unstable-2023-08-02";
src = fetchFromGitHub {
owner = "gkovacs";
repo = "rime-japanese";
rev = "4c1e65135459175136f380e90ba52acb40fdfb2d";
hash = "sha256-/mIIyCu8V95ArKo/vIS3qAiD8InUmk8fAF/wejxRxGw=";
};
installPhase = ''
runHook preInstall
install -D japanese.*.yaml -t $out/share/rime-data/
runHook postInstall
'';
meta = {
description = "Layout for typing in Japanese with RIME";
homepage = "https://github.com/gkovacs/rime-japanese";
# Awaiting upstream response (gkovacs/rime-japanese#6)
# Packages are assumed unfree unless explicitly indicated otherwise
license = lib.licenses.unfree;
maintainers = with lib.maintainers; [ pluiedev ];
platforms = lib.platforms.all;
};
}
源码链接:
整个文件是一个抽象,它的参数是一个属性集,并且需要有lib
、
stdenvNoCC
、fetchFromGitHub
这三个字段;它的返回值是一个应用,将一
个很长的属性集应用到stdenvNoCC.mkDerivation
上,这正是上文提到的众多
mkDerivation
函数中的一个,而stdenvNoCC
是指去除 gcc 等编译工具后的
标准环境,考虑到这个 derivation 根本不需要编译构建,只是复制文件,也就
用不着 gcc,因此使用这个环境是合理的。
接下来我们分析占据了一大半篇幅的那个属性集。
pname 和 version 字段顾名思义,指定包名称和版本,最终在/nix/store/
中
生成的仓库名称会是<hash>-<pname>-<version>
,理论上这两个字段可以随便
写,但是指定合适的名字可以方便我们找到仓库,检查其中的内容是否正确。
src 字段指定构建过程中的“源”,它的值是一个应用,将一个属性集应用到
fetchFromGitHub
函数上,意思是根据这个属性集的信息,从GitHub 上拉取源
码;属性集指定了所有者,仓库名,版本号和哈希值,哈希值可以先不管,到时
候报错了就会提示正确的哈希值的(乐)。
installPhase 字段指定构建过程中的“安装”步骤。我们没有指定“打补丁”、“构
建”等步骤,Nix 就会按默认的来,在stdenvNoCC
中,可以认为它除了把源码
放到临时路径之外啥也没干。installPhase 的值是一个字符串(Nix 特有的多
行字符串语法,使用两个单引号包裹),内容是一个 bash 脚本,指定要干什么
事情;其中runHook preInstall
和runHook postInstall
是运行钩子,如果
在installPhase 之外,还写了 preInstall 和/或 postInstall 字段,那么它就
会调用对应的 bash 脚本,但这里没有指定,那就没效果;唯一有效果的
是install -D japanese.*.yaml -t $out/share/rime-data/
语句,运行
install 指令,将临时路径中形如 japanese.*.yaml 的文件安装到最终仓库路
径下的 share/rime-data/ 文件夹。在这个 bash 脚本里,“当前工作路径” cwd
会是临时路径,而环境变量 out 会是最终路径。至于为什么放到
share/rime-data 文件夹,这个到时候再说。
最后 meta 字段指定了一些“元信息”,例如开源协议、维护者等,如果要将
derivation 发布就必须指定,但它对包的构建其实没有什么影响,自己用时可
以不要。
下一节将介绍如何使用 derivation。
3 个赞
支持,如果有更 nix 的方式来管理 eaf 的话就太棒了
rua
7
nix 我感觉是真的难学,文档又乱又少,很多还没文档,有的文档还落后版本
2 个赞
奇怪,nixpkgs 里大多的包好像是自动生成的,就介绍一下常用的几个函数就可以上手大多数情况了。
其实像我这样的菜鸟,完全可以用 doomemacs。
我也不太明白为什么有人说难,你大概学个入门,慢慢用 nixpkgs,看更新日志和论坛什么的,自然而然就会好起来的。
首先要说明一点:将 rime-japanese 激活的方法比较独特,参考
Package request: rime-japanese · Issue #295527 · NixOS/nixpkgs · GitHub ,以下为一个示例:
i18n.inputMethod.fcitx5.addons = [
(pkgs.fcitx5-rime.override {
rimeDataPkgs = [
pkgs.rime-data
pkgs.rime-japanese
];
})
];
核心是fcitx5-rime.override
函数,它会修改 fcitx5-rime 包中的部分参数,
修改的内容由后面的属性集决定。在这里我们修改了 rimeDataPkgs 参数,它是
一个列表,原本只有 rime-data 那一项(可以在fcitx5-rime的 derivation 中
看到),我们加上了 rime-japanese 包,使得 rime 引擎能找到相关配置文件。
进入正题:如果 rime-japanese 是我们自己写的 derivation,要如何激活它呢?
通常做法是使用callPackage
函数。我们可以做一个实验:把上文中所展示的
derivation 复制到一个 test.nix 文件中,将这个文件放到 NixOS 的配置文件
所在的文件夹中,然后在配置文件中写这样一段代码:
i18n.inputMethod.fcitx5.addons = [
(pkgs.fcitx5-rime.override {
rimeDataPkgs = [
pkgs.rime-data
(pkgs.callPackage ./test.nix { })
];
})
];
这样就激活了我们复制下来的 rime-japanese derivation 了!
callPackage
函数接收第一个参数是 derivation 的文件路径,第二个参数是
传给 derivation 的参数,这里是一个空属性集;这是因为callPackage
会自
动注入类似lib
、stdenvNoCC
之类的参数,所以不用手动指定了;当然手动
指定有更高的优先级,并且对于那些callPackage
不能自动注入的参数,必须
手动指定(至于到底能自动注入哪些参数,或者说一个 derivation 究竟需要什
么参数,我的建议是仿照别人写的 derivation,看看他们使用了什么参数)。
下一节我们将尝试自己写一个 derivation。