Nix入坑笔记
对于经常使用计算机工作的人(尤其是程序员)来说,工作设备上往往会积聚大量的文档、软件以及配置文件;如果我们需要在多台不同设备间切换,或者单纯是更换了新电脑,要是可以在不同的设备上同步配置,将会节省我们很多时间;另外假如有时由于某个操作导致系统出现了异常,要如何轻松回退到之前的状态呢?现在市面上有各类云盘工具可以用于备份和同步文件,有版本管理工具可以帮助管理文档版本。那么对于软件呢?软件的配置文件可以备份和同步,但是试想一下如果在一台机器上曾经安装了应用A,而在另一台机器上重新安装A时,A的版本发生了变化,直接使用最新版会导致旧的配置不可用;进而可以试想每个软件都有不同的依赖,如果应用A依赖B的1.0版本,而在新设备上安装的B是最新的2.0版本,这也可能导致程序A无法工作。正如标题所示,在此我要介绍一套能解决以上问题的工具:Nix
。
函数与可复现
更换使用的计算机,我们希望可以在新的机器上复现旧机器的内容,也就是获得和旧机器一样的软件版本、配置信息等;当出现问题需要他人帮助时,我们希望可以控制变量,为帮助者复现一个与我们当前环境最接近的环境。我们曾经接触过什么东西是可以复现的呢?
回顾一下数学中函数的定义:“函数(英語:Function)在数学中为两不为空集的集合间的一种对应关系:输入值集合中的每项元素皆能对应唯一一项输出值集合中的元素。(维基百科)”,例如f(x) = x^2
就是一个函数,对于任意一个输入x,都只能有唯一一个输出,如果一个东西输入为x,输出同时既可以是y,也可以是z,那么这就不是函数。可以发现,一个函数,只要输入不变,输出也一定不会变,也就是说数学上的函数是可以复现结果的,不论外界条件(如时间)如何变化,只有输入是改变结果的唯一渠道,输入不变就可以一直得到不变的结果。
通过上图可以看到,如果x轴是输入而y轴是输出,那么画一条x轴的垂直线,如果它能与曲线拥有超过一个的交点,那么这个图像就不是函数的图像。
以上是数学中的函数,在计算机领域,也存在一个“函数”,但是这两个概念并不相等。考察下面这个Python
语言中的函数:
这种函数被成为纯函数,对于一个确切的age
值,这个函数只会返回一个确切的结果,这种情况下,这个函数相当于数学定义中的函数。再看另一种函数:
这个函数就是不纯的,因为adultAge
可以被更改,而函数依赖这样一个可以被改变的自由变量,因此相同的输入可能获得不同的结果,例如当参数age为19时,函数返回true
,之后adultAge
被修改为20,同样的输入函数会返回false
。这样不纯的函数就不能视为数学上的函数。可以说,在编程语言中,纯函数是可以复现的,而非纯函数不可以。
Nix: 纯函数式的软件包管理工具
Nix是一系列工具的合集,通过一种纯函数式的方式。Nix提供了一个函数式语言来描述软件包,每一个软件包就是Nix语言中的一个表达式,例如下面这个hello
包:
这是一个Nix语言中的函数,也是Nix概念下的“软件包”,Haskell
程序员可能会对此感到很熟悉,Nix中的函数定义很简洁,格式是pattern: body
,pattern是一个模式,如果没有接触过函数式语言的话,可以参考JavaScript
中的解构对象,body是函数体,要想定义包,这个函数需要返回一个derivation
,也就是对包的构建过程的描述。Nix中调用函数不需要括号,也不需要return
,函数体表达式结果就是返回值,采用func param
格式,因此这个hello
包的函数体就是调用了stdenv.mkDerivation
函数返回其结果,其中包含构建该软件包所需的属性。只要输入相同,我们就能得到完全相同的软件版本。一个软件包所需要的全部依赖必须被定义在表达式内,而不能去环境变量、其他目录获取。Nix语言编译后的结果就是表达式所描述的程序包。
既然Nix下的包就是Nix语言的一个表达式,那我们从一个编程语言的表达式的角度来看看Nix包的性质:
- 表达式可以求值,可以认为求值结果就是一个软件包,值可以比较,值不相同,就是包不同
- 软件包的版本、依赖版本、构建过程等必须由表达式描述,更改这些属性,会得到不同的值,也就是不同的包
- 结合1、2,即使如果原本有包A,依赖
Python3.7
,现在我们创建一个依赖Python3.6
的版本,虽然都是可以认为这也是包A,但实质上他们是两个包,因为值不相同 - 因此系统上可以同时出现很多个版本的包A,他们实质上并不相同,其他的包可以依赖不同的包A,从旧的包A派生一个新的包A2.0,不会改变那些依赖旧的包A的其他包,除非修改了其他包的表达式定义
Profile与Channel
Nix工具集中,nix-env
命令用于安装、升级或删除包,它和其他Linux发行版的包管理工具或Mac上的homebrew作用类似,不同之处在于nix-env对系统环境的更改是原子化的,可回滚的。每次通过nix-env
修改用户环境,都会生成一个新的profile,类似于一次Git记录,可以像Git一样,回滚到某一次变更记录上。nix-env --list-generations
命令可以列出所有的版本,可以在其中自由切换,为了节省硬盘空间,也可以使用垃圾回收机制清除不必要的记录。
nix-channel
是一个用来管理Channel的工具,Channel就是一个简单的指向某个Nix表达式集合,或者说:软件包仓库。例如https://nixos.org/channels/nixpkgs-unstable(目前其中包含八万多个包)。
隔离的开发环境
我经常在开发环境中使用Docker
,因为我对开发环境有一些“洁癖”,比如在开发的某个项目需要用到redis,而我在其他地方不是经常使用,那么我会使用Docker镜像来代替全局安装。另外,现代的编程语言包管理工具通常都具有隔离环境的作用,比如nodejs
的npm
,在一个项目下添加依赖react16
,它会被安装一个隔离的环境中,不会影响到另一个项目下使用react17
;Python
的官方包管理工具pip
会把Python包安装到全局,所以做Python开发一般都会使用virtualenv
创建与全局隔离的虚拟环境(顺带推荐一个支持PEP582的Python包管理工具PDM)。nix-shell
就是一个类似virtualenv
的工具。
假如日常系统全局环境使用的Python版本是3.8
,但是想在某个单独的环境里使用Python3.10
,尝试尝试它的模式匹配功能,那么就可以使用命令nix-shell -p python310
,nix-shell会准备需要的依赖,并且自动进入一个配置好的单独shell环境中。
nix-shell内的包不会影响到外界。
NixOS
NixOS是一个基于Nix的Linux发行版。与pacman
等包管理工具不同,Nix本身是跨平台的,可以脱离NixOS使用。事实上是先有Nix才有的NixOS,借用姜文《邪不压正》台词:“就是为了这口醋,才包的这顿饺子”。虽然NixOS也是一个Linux发行版,但是它和常规的GNU/Linux发行版
有一些可能会劝退新手的区别:
- 首先它不支持FHS,所以一些假定系统上存在这些目录的程序可能不能方便地正常工作
- 没有像Ubuntu那样方便的安装工具,不过按照官方手册来装也并不是很费事
所以在尝试使用NixOS之前一定要考虑清楚,最好是虚拟机里先试试再决定是否作为日常使用的系统。
首先是安装,可以参考官方手册,首先是分区,然后mount,最后通过nixos-generate-config
命令生成一个/etc/nixos/configuration.nix
,这个文件定义了整个系统的配置、软件包、硬件、环境变量等,这是一个Nix语言的文件,支持模块化:
以上是部分示例,还是比较简洁易懂的,配置好就可以使用nixos-install
命令安装系统了,之后要修改配置,新增系统包,都可以来修改这个文件,执行nixos-rebuild switch
命令重新编译,就像nix-env
一样,上一个系统状态也会保留,在boot选项里可以选择回到改动之前的版本,可以通过配置gc来定时清理太旧的profile:
可以说整个NixOS就是一个声明式的系统,只要备份好configuration,就可以随时恢复原样,拷贝配置文件就可以在新设备生成一个一样的系统。由于/etc/nixos/*.nix
就是对系统的定义,那么如果系统出了问题,只要拷贝一份配置文件给别人,别人就可以清楚地知道你的系统状态,方便复现问题,定位问题(经常看一些开源项目的issue就会发现,有一些问题就因为使用者环境的复杂,开发者无法复现问题,从而长期得不到解决)。
说完优点再说说缺点,除了之前提到的与常规发行版的区别,再说几个我认为的缺点:
- 用nix-env安装包,不会自动记录在配置里,这可能引起困惑
- 文档相对较少
- 自己创建社区暂时没有的包,需要学习Nix语言(算一个使用门槛上的缺点吧)
对于Nix的介绍就先到这里了,关于一些使用的细节我会发布到个人wiki上以供参考,等到再深入使用一段时间后再来谈谈Nix相关内容吧~