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语言中的函数:

def isAdult(age: int) -> bool:
    return age > 18

这种函数被成为纯函数,对于一个确切的age值,这个函数只会返回一个确切的结果,这种情况下,这个函数相当于数学定义中的函数。再看另一种函数:

adultAge = 18

def isAdult(age: int) -> bool:
    return age > adultAge

这个函数就是不纯的,因为adultAge可以被更改,而函数依赖这样一个可以被改变的自由变量,因此相同的输入可能获得不同的结果,例如当参数age为19时,函数返回true,之后adultAge被修改为20,同样的输入函数会返回false。这样不纯的函数就不能视为数学上的函数。可以说,在编程语言中,纯函数是可以复现的,而非纯函数不可以。

Nix: 纯函数式的软件包管理工具

Nix是一系列工具的合集,通过一种纯函数式的方式。Nix提供了一个函数式语言来描述软件包,每一个软件包就是Nix语言中的一个表达式,例如下面这个hello包:

{ lib
, stdenv
, fetchurl
, testVersion
, hello
}:

stdenv.mkDerivation rec {
  pname = "hello";
  version = "2.10";

  src = fetchurl {
    url = "mirror://gnu/hello/${pname}-${version}.tar.gz";
    sha256 = "0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i";
  };

  doCheck = true;

  passthru.tests.version =
    testVersion { package = hello; };

  meta = with lib; {
    description = "A program that produces a familiar, friendly greeting";
    longDescription = ''
      GNU Hello is a program that prints "Hello, world!" when you run it.
      It is fully customizable.
    '';
    homepage = "https://www.gnu.org/software/hello/manual/";
    changelog = "https://git.savannah.gnu.org/cgit/hello.git/plain/NEWS?h=v${version}";
    license = licenses.gpl3Plus;
    maintainers = [ maintainers.eelco ];
    platforms = platforms.all;
  };
}

这是一个Nix语言中的函数,也是Nix概念下的“软件包”,Haskell程序员可能会对此感到很熟悉,Nix中的函数定义很简洁,格式是pattern: body,pattern是一个模式,如果没有接触过函数式语言的话,可以参考JavaScript中的解构对象,body是函数体,要想定义包,这个函数需要返回一个derivation,也就是对包的构建过程的描述。Nix中调用函数不需要括号,也不需要return,函数体表达式结果就是返回值,采用func param格式,因此这个hello包的函数体就是调用了stdenv.mkDerivation函数返回其结果,其中包含构建该软件包所需的属性。只要输入相同,我们就能得到完全相同的软件版本。一个软件包所需要的全部依赖必须被定义在表达式内,而不能去环境变量、其他目录获取。Nix语言编译后的结果就是表达式所描述的程序包。

既然Nix下的包就是Nix语言的一个表达式,那我们从一个编程语言的表达式的角度来看看Nix包的性质:

  1. 表达式可以求值,可以认为求值结果就是一个软件包,值可以比较,值不相同,就是包不同
  2. 软件包的版本、依赖版本、构建过程等必须由表达式描述,更改这些属性,会得到不同的值,也就是不同的包
  3. 结合1、2,即使如果原本有包A,依赖Python3.7,现在我们创建一个依赖Python3.6的版本,虽然都是可以认为这也是包A,但实质上他们是两个包,因为值不相同
  4. 因此系统上可以同时出现很多个版本的包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镜像来代替全局安装。另外,现代的编程语言包管理工具通常都具有隔离环境的作用,比如nodejsnpm,在一个项目下添加依赖react16,它会被安装一个隔离的环境中,不会影响到另一个项目下使用react17Python的官方包管理工具pip会把Python包安装到全局,所以做Python开发一般都会使用virtualenv创建与全局隔离的虚拟环境(顺带推荐一个支持PEP582的Python包管理工具PDM)。nix-shell就是一个类似virtualenv的工具。

假如日常系统全局环境使用的Python版本是3.8,但是想在某个单独的环境里使用Python3.10,尝试尝试它的模式匹配功能,那么就可以使用命令nix-shell -p python310,nix-shell会准备需要的依赖,并且自动进入一个配置好的单独shell环境中。

nix-shell

nix-shell内的包不会影响到外界。

NixOS

NixOS是一个基于Nix的Linux发行版。与pacman等包管理工具不同,Nix本身是跨平台的,可以脱离NixOS使用。事实上是先有Nix才有的NixOS,借用姜文《邪不压正》台词:“就是为了这口醋,才包的这顿饺子”。虽然NixOS也是一个Linux发行版,但是它和常规的GNU/Linux发行版有一些可能会劝退新手的区别:

  1. 首先它不支持FHS,所以一些假定系统上存在这些目录的程序可能不能方便地正常工作
  2. 没有像Ubuntu那样方便的安装工具,不过按照官方手册来装也并不是很费事

所以在尝试使用NixOS之前一定要考虑清楚,最好是虚拟机里先试试再决定是否作为日常使用的系统。

首先是安装,可以参考官方手册,首先是分区,然后mount,最后通过nixos-generate-config命令生成一个/etc/nixos/configuration.nix,这个文件定义了整个系统的配置、软件包、硬件、环境变量等,这是一个Nix语言的文件,支持模块化:

{ config, pkgs, ... }:

{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
      ./home.nix
    ];

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  environment.systemPackages = with pkgs; [
    vimHugeX
    python3Full
    nodejs
    wget
    firefox
  ];
}

以上是部分示例,还是比较简洁易懂的,配置好就可以使用nixos-install命令安装系统了,之后要修改配置,新增系统包,都可以来修改这个文件,执行nixos-rebuild switch命令重新编译,就像nix-env一样,上一个系统状态也会保留,在boot选项里可以选择回到改动之前的版本,可以通过配置gc来定时清理太旧的profile:

nix.autoOptimiseStore = true;
nix.gc = {
  automatic = true;
  dates = "weekly";
  options = "--delete-older-than 10d";
};

可以说整个NixOS就是一个声明式的系统,只要备份好configuration,就可以随时恢复原样,拷贝配置文件就可以在新设备生成一个一样的系统。由于/etc/nixos/*.nix就是对系统的定义,那么如果系统出了问题,只要拷贝一份配置文件给别人,别人就可以清楚地知道你的系统状态,方便复现问题,定位问题(经常看一些开源项目的issue就会发现,有一些问题就因为使用者环境的复杂,开发者无法复现问题,从而长期得不到解决)。

说完优点再说说缺点,除了之前提到的与常规发行版的区别,再说几个我认为的缺点:

  1. 用nix-env安装包,不会自动记录在配置里,这可能引起困惑
  2. 文档相对较少
  3. 自己创建社区暂时没有的包,需要学习Nix语言(算一个使用门槛上的缺点吧)

对于Nix的介绍就先到这里了,关于一些使用的细节我会发布到个人wiki上以供参考,等到再深入使用一段时间后再来谈谈Nix相关内容吧~

EOF
Github
Copyright © 2020-2024 Elliot