开始上手nixos,nix与nixpkgs其实相对还好,都没有什么问题。但这之间遇到了相当多问题。大体分为几点。一个是开发环境,nix-shell的沙盒并不能很方便的对lsp等开发过程中使用的工具提供支持(我们不可能在shell.nix中去添加开发开发用的工具),另外一个大问题是部分第三方包的安装。

从文档开始

这里要特别说明,nixos的文档与其它一些文档相比,有点乱。不像gentoo有一个handbook,但是有一个pills。这个pills是一开始被我忽略的东西,本来以为只是一个简单的教程,但其实不是。如果不读这个pills,直接读manual会发现很多东西对不上。或者说感觉文档阅读起来相当困难。manual分为几个,对于日常使用,主要是nixos,nix与nixpkgs。这三个是必需要读全的,且要交错着读,相交内容比较多。先读pills再去接触文档会比较好。因为文档主一些内容只是一句话带过的。比如参数的override,在没有读pills前,至少我并不知道这个参数是从哪里来的(居然是packages定义函数的参数),而且现在nixpkgs并没有可以对这个参数详细查阅的方法,唯一的方法就是看原代码。

pills是以nix,一点点深入,到构建出一个简单的nixpkgs和nixos中的一些概念,换句话说,nixpkgs中的一些理念,要读pills才能明白。仅仅只读manual,用用系统问题还是不大的,但是如果要做大量工具配置就会出现各种问题了。会发现manual中缺少了很多必要的东西。

环境构建

一般一个编程环境,主要有编辑器、编译器/解释器、依赖包等。

以典型的C为例,编译主要有全局的头文件和库,以及编译器。而python、ruby此类语言则是一个可以导入各类包的环境。不论是那种都大量依赖于环境变量,通过各种环境变量进行查找。解释性语言大多又提供了沙盒工具。可以在不污染全局的情况下进行编译、解释。

开发环境主要依赖各种分析工具,比如lsp server会对各种引用文件等分析,一般工具都对沙盒环境会有支持,但在C方面就会比较差了。需要相当多的配置,会比较麻烦。

依赖,是软件开发中相当重要的一个问题。循环依赖是非常头痛的事,而且也很常见。一般可以通过精心配置和选用依赖来消除。但都比较麻烦。特别是当有多个不同工具,依赖同一个库但版本不同时,这就会造成了大量的冲突,一种情况是用沙盒处理,但并不是所有的工具包都有沙盒,或者就是人工添加路径,这样会很麻烦。典型得像gentoo的ebuild就是使用了一个编译过程中的沙盒和slot系统来处理版本问题。但当同slot一些软件对包的限制非常大时,就会出现比较严重的依赖错误了。

nix/nixpkgs

nix本身是一个语言,用于描述一系列包及其构建。而nixpkgs是基于nix,进行扩展与添加了大量默认包的一套包管理系统。不同于其它包管理,nix最大的优势在于环境隔离。nix的所有包构建会自动隔离,但又不像一些沙盒隔离得非常彻底,可以使得在依赖不被多次重复安装,但又可以隔离环境,同样的软件包在系统中可以存在多个版本。思想很近似于haskell的stack,但不同于stack,stack并不提供一个全局清理方案,唯一的方法就是人工删除每个项目的沙盒。而nix通过link方式,对这个做到了非常好的处理。

nix-shell/nix-build

此二者是nix的子工具,前者提供一个shell环境,后者则是对包进行编译。默认下shell是使用的build环境,特别注意,是编译环境,也就是说,是不包含编译后的东西的。对于python等解释类语言一般仅仅使用nix-shell就足够了。nix-build更多是针对编译类语言的,或者对解释类语言项目进行打包时使用的。

特别注意,nix-build提供的是编译环境沙盒,而不是运行环境沙盒。运行环境沙盒可以使用nix-shell进行构建。具体的可以读阅读文档。

如果需要运行环境隔离,更近一步,还是使用容器更好。当然,nixos本身提供了一个自身的容器,可以更加方便的使用。

NixOS

完全基于nixpkgs构建的系统,不存在任何传统的一些目录,系统大部分目录也是只读的。最激进的就是完全把全局环境移除了。这样就带来了大量问题了。没有全局环境,就意味着所有非nixpkgs的第三方软件包管理,只要有依赖到全局库编译就一定会失败,特别是pypi、gems、cabal这种,而python大部分包在nixpkgs都进行的提供,所以可以轻松安装,但请注意,库与软件是分离的,就是说安装python与python的库,并不能在python中导入库。处理方式就是建立新包来把这几个包的关系建立起来,而对于ruby,官方提供了详细文档进行说明。node则不需要。因为node包并没有编译的过程,也没有在安装过程中依赖到第三方环境。

问题产生

上面这样一说就出现了一个很关键的问题,开发环境。对于使用系统,并不需要开发的各种库,所以没有问题(其实也有问题的,因为有些工具仅在第三方包管理上,只是这类工具也大部分都是开发用的)。对于项目依赖的包而言,使用nix进行构建,不仅提供了环境隔离,而且可以方便处理依赖,这就非常轻松了。但是我们还存在一类工具,它们是帮忙开发使用的,比如lsp,他们本身或许不需要依赖什么包,但运行时,是需要依赖到开发项目的依赖的,来提供解析等。这类工具我们不可能放置到项目文件中,而如果从nix-shell中跑一个emacs,每当环境重建这个emacs也要重开,就非常麻烦了。当然也是有解决方案的(不然我写这文章是干什么的)。另外一个问题上面也提了,就是一些软件仅在第三方包管理中,而我们又不能直接使用这类包管理。

大体归类就是几点:

  1. 开发辅助工具需要使用编译/运行环境中的信息
  2. 第三方包管理
  3. nixpkgs上的包如何自定义,比如neovim加入python支持

解决问题

direnv

nix-shell提供了一种可以导出环境信息的方式,而最直接的使用方法就是通过direnv来完成这个过程的自动化。这样就可以在项目目录中,使用开发环境,又不会污染到全局环境。具体使用可以看direnv的wiki

第三方包管理

在nixpkgs中,文档把这部分写做了package。这个是用来打包的,比较倾向于开发项目的打包,但也可以用来做本地全局环境的构建。比较麻烦的就是我们可能需要人工对这些包进行管理升级(其实就是通过第三方包管理,而不是一个个去修改版本号,多了一个步骤罢了,可以用shell进行自动化)。

这里又会出现问题了。我们是使用overlay?还是直接override?overlay会污染全局环境,所以后者会比较好,且后者仅仅,且这个问题仅仅出现在nixos上,因为对于非nixos系统,我们使用nix主要是用做构建,其它工具可以正常使用对应的包管理(但也可以这样切换到nix上)。不过我更倾向于在其它系统上使用传统的方法。如果全部切到nix了,那为什么不直接用nixos啊。

自定义参数

这个其实nixos文档上有写,但写得很简单,就一个override,但是override里面的值从哪里来呢?overrideAttr又是什么?这些答案分部在nixpkgs与pills里。override是重写的一个函数调用的参数,即我们先对一个函数使用一系列默认值进行调用得到返回值,然后返回值使用override可以把这个调用过程中的部分参数进行修改而得到一个新的返回值(且可以继续override)。这个想法可以说是非常优秀了(但过于依赖nix语言本身set的特性,nix之外使用有点麻烦),而受限于无法方便的查询参数,唯一的方法就是去查源代码了。而源代码中有些参数是在外部进行调用添加的。而不是定义时的默认值,这样又要一层层去找,现在社区正在寻找处理方法中,或许下一个版本就会出现解决方案吧。

参考