↓ 跳过正文

NixOS(七):软件应该装在哪里——系统、用户与项目环境

·4606 字·10 分钟
锦李本鲤
作者
锦李本鲤
光锥之内,皆为命运。
NixOS系列 - 这篇文章属于一个选集。
§ 7: 本文

缘起
#

前面几篇介绍了通过environment.systemPackages安装软件的方法。在NixOS中,除了这种方法,还可以使用Home Manager、nix shell、nix run和nix develop等工具来安装或使用软件。它们都能提供软件环境,但适用的场景有所不同。

之前在Python环境管理方式总结中,我介绍过不同环境管理工具的使用方法。NixOS的软件管理也有类似的问题:如果没有统一的使用方式,系统、用户和项目中的软件环境就容易混在一起。

因此,这里总结一下NixOS中几种常用的软件安装与环境管理方式,介绍它们的用途和使用方法,最后给出一些选择建议。让AI帮忙安装软件时,也可以根据用途说明应该修改系统配置、用户配置,还是项目的开发环境。

前置条件
#

  • 已有可以正常构建的NixOS flake配置;
  • 了解NixOS模块的基本用法,可以参考第六篇;
  • 使用Home Manager的部分需要已经完成相应集成。

下面的配置都是简化示例,不包含私人配置。项目开发环境部分会使用一个独立的示例目录。

软件安装与环境管理方式分类
#

根据软件的使用范围,可以分为以下几类:

  1. 系统软件包:通过NixOS配置管理,供这台电脑上的用户使用。
  2. 用户软件包:通过Home Manager管理,适合个人工具和程序设置。
  3. 临时软件环境:通过nix shell或nix run使用,适合临时运行某个工具。
  4. 项目开发环境:通过项目的devShell管理,适合解释器、编译器和项目依赖。

这些方式可以配合使用。例如,系统中保留常用工具,同时为不同项目提供各自的开发环境。

系统软件包管理
#

安装系统软件包
#

如果希望电脑上的用户都能使用某些常用命令,可以放进NixOS模块:

1
2
3
4
5
6
7
8
{ pkgs, ... }:
{
  environment.systemPackages = [
    pkgs.curl
    pkgs.jq
    pkgs.ripgrep
  ];
}

这只是模块中的相关部分,原有配置不用删掉。ripgrep提供的命令叫rg,包名和命令名不一定相同。应用配置之后,系统环境里就能找到这些工具。

公共文件放哪些包,还是要看是否每台电脑都需要。几台电脑都用的诊断工具可以放在公共模块,只有一台电脑需要的软件就放在对应主机配置里,具体拆分见第三篇。

建议这里只保留需要长期使用的工具。对于偶尔使用的软件,可以先通过临时环境运行,避免系统软件包列表过于庞杂。

启用系统服务
#

例如想启用SSH服务,需要的是:

1
services.openssh.enable = true;

对应模块会准备软件和服务配置。注意,只把pkgs.openssh加入软件包列表,并不会启用SSH服务。如果需要让其他电脑连接本机,还需要启用服务,并检查认证和访问设置。

有些桌面程序也有programs.*模块,除了装包还会处理系统集成。需要这类功能时,先查相应模块,再决定是否直接加包。NixOS手册的软件包管理章节介绍了相关方法。

使用Home Manager管理用户软件
#

Home Manager可以管理用户的软件和设置。例如在已有的Home Manager模块里写:

1
2
3
4
5
6
7
8
9
{ pkgs, ... }:
{
  home.packages = [
    pkgs.jq
    pkgs.ripgrep
  ];

  programs.fzf.enable = true;
}

home.packages把包加入这个用户的环境。programs.fzf.enable则使用Home Manager的fzf模块,除了软件本身,还可以配合已启用的Shell模块提供集成。这里的选项属于Home Manager,要放进用户模块,不能直接抄到普通NixOS模块里。

这里的jq和前面的系统软件包例子重复,是为了展示两种安装方式,实际使用时可以选择其中一种。对于与个人Shell、编辑器设置相关的工具,使用Home Manager可以把软件和配置一起管理。

Home Manager作为NixOS模块集成时,通常随nixos-rebuild应用;独立使用时,则有自己的home-manager switch。所以“放在用户配置里”不等于“这次一定不用重建系统”,还要看采用了哪种集成方式。安装和使用方式可以参看Home Manager手册。

Home Manager还可以管理配置文件。首次使用时如何处理已有配置文件,以及怎样组织用户配置,将在下一篇中介绍。

临时软件环境
#

使用nix shell
#

假设只想用jq检查一个JSON文件,可以先进入一个有这些工具的Shell:

1
2
3
4
nix shell nixpkgs#jq nixpkgs#ripgrep
jq --version
rg --version
exit

前一条命令准备软件,并在新的Shell中把它们加入PATH。exit退出后,回到之前的Shell环境,不需要为了试用一个工具而修改配置、重建系统。

如果只执行一条命令,也可以不用交互式Shell:

1
nix shell nixpkgs#jq --command jq --version

这里的nixpkgs#jq里,nixpkgs通过flake registry解析到一个输入,#jq选择软件包。它不一定和系统flake.lock里的Nixpkgs是同一个版本。临时试用这样写很方便,想长期复用同一套环境时,就应该把输入锁在项目里。

“临时”指的是这次Shell的环境。下载的软件仍然在Nix store里,之后是否清理要看垃圾回收引用;程序写到家目录或者项目里的文件,也不会因为退出Shell而消失。

使用nix run
#

如果只是想启动一个程序,还可以用:

1
2
nix run nixpkgs#hello
nix run nixpkgs#hello -- --greeting='Hello from Nix'

这里的hello是一个演示程序。第一条命令直接运行它;第二条命令中,--之后的参数会传给hello程序,用于指定输出的文字。

对于软件包,nix run会根据包的元数据和名称选择主要可执行文件,不是随便运行包里任何一个命令。一个包带了好几个程序,或者没有合适的默认入口时,用nix shell ... --command ...明确指定命令反而更直接。详细规则见nix run文档。

注意,nix shell和nix-shell是不同命令。旧教程中常见的nix-shell -p jq和项目中的shell.nix属于另一种用法。这里继续使用本系列的flake方式。

使用devShell管理项目开发环境
#

在开发项目时,往往需要固定的解释器、库和命令行工具。如果每次都手动创建临时环境,就需要重复指定这些依赖,换电脑时也容易遗漏。

这时可以在项目自己的flake里定义开发环境,叫作devShell。它跟系统配置可以使用不同的输入,项目更新依赖也不必顺便更新整台电脑。

创建项目与配置文件
#

这里用一个Python加NumPy的环境,同时提供jq。在一个新的示例目录中开始:

1
2
3
mkdir -p ~/projects/nix-env-demo
cd ~/projects/nix-env-demo
git init

新建flake.nix:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  description = "A small Python development environment";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { nixpkgs, ... }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" ];
      forAllSystems = nixpkgs.lib.genAttrs systems;
    in
    {
      devShells = forAllSystems (system:
        let
          pkgs = nixpkgs.legacyPackages.${system};
        in
        {
          default = pkgs.mkShell {
            packages = [
              pkgs.jq
              (pkgs.python3.withPackages (ps: [ ps.numpy ]))
            ];
          };
        }
      );
    };
}

这是完整的演示flake。如果已有项目已经有flake.nix,就把需要的输出合进去,不要直接覆盖原文件。

这里的systems列出了两种Linux架构,genAttrs为每一种架构生成对应属性。legacyPackages.${system}选择那种架构的软件包集合;名字虽然带legacy,但在flake中这样取包很常见,不代表我们退回了旧式配置。

devShells.<system>.default是默认开发环境,pkgs.mkShell用来准备它。packages列出环境里的工具;python3.withPackages则提供一个已经带NumPy的Python,所以进入环境之后就能直接导入它。这个例子使用的架构和包也要适用于自己的电脑。mkShell的参数可以参看Nixpkgs文档。

创建和使用开发环境
#

保存配置文件后,运行以下命令:

1
2
3
4
git add flake.nix
nix flake lock
git add flake.lock
nix develop

新文件先加入Git索引,Nix才能在这个本地Git flake中读到它。nix flake lock把分支当前对应的输入锁定下来;以后分享项目时,flake.nix和flake.lock要一起提交。分支会继续更新,锁文件则让这个项目继续使用已锁定的版本。

进入开发环境之后试一下:

1
2
3
python -c 'import numpy; print(numpy.__version__)'
jq --version
exit

这里不写死版本号,输出取决于锁定的输入。想只运行一次检查,也可以在项目目录执行:

1
nix develop --command python -c 'import numpy; print(numpy.__version__)'

nix develop准备的是开发/构建环境,不只是把几个程序放到PATH里。对于这里的mkShell,我们主要用它提供工具;遇到编译项目,还可以加入相应库和构建依赖。命令的更多用法见nix develop文档。

它也不是容器。进入环境后仍然能访问自己的文件、网络和原有环境,运行的程序也会真的修改文件。锁住工具版本很有用,但不等于项目的源码、外部数据和所有运行结果都自动固定了。

Python依赖管理
#

上面的withPackages适合依赖已经在Nixpkgs里的小例子。不需要再对这个store中的Python环境执行sudo pip install;store里的文件由Nix管理,也不适合手动往里面装东西。

如果项目原本就用venv、uv或其他工具管理Python依赖,也可以继续保留它们,让Nix提供解释器、编译工具和系统库。不过这些工具另外解析的依赖,仍然需要自己的锁定和安装过程,不能认为有了flake.lock就全部管住了。涉及预编译Python扩展时,还可能需要处理动态库兼容问题。

因此,可以根据项目选择由Nix管理Python包,或者由Nix提供基础环境,再通过Python工具管理依赖。两种方式的锁定范围不同,使用时需要分别记录。

问题解决
#

软件版本与PATH
#

同一个命令可能同时来自系统环境、用户环境和当前开发环境。出问题时,先看看Shell实际上找到了哪个:

1
2
3
4
type -a jq
command -v jq
readlink -f "$(command -v jq)"
jq --version

type -a可以看到多个候选,也能帮忙发现别名或函数;command -v显示当前选中的入口。readlink这条适用于它返回可执行文件路径的情况,可以继续看到对应的store路径。

如果实际版本和预期不同,需要检查PATH和Shell初始化文件。Shell配置可能再次修改PATH,激活Python虚拟环境也可能改变使用的解释器。调整配置之后,可以重新打开终端再检查。

同一个store路径被多处引用,并不会因此把包内容复制几份;不同输入或构建产生的版本,则可能同时存在。这种共存是Nix的优点,但选择了哪个入口还是要查清楚。

推荐的使用方法
#

根据上述几种方式的用途,我推荐按以下方法选择:

用途放在哪里
电脑上各个用户都需要的常用工具environment.systemPackages
系统服务或需要系统集成的功能对应的NixOS模块
个人常用工具和程序设置Home Manager
偶尔用一下的命令nix shell或nix run
项目的解释器、编译器和依赖项目自己的devShell

还有nix profile install这样的安装方法,它会修改用户profile,软件可以跨会话保留,也可以移除。但它不会自动成为配置仓库中的声明。如果已经决定用Home Manager记录个人环境,就没必要再把同一批长期软件分散到另一张安装清单里。

遇到Nixpkgs里没有的软件、需要修改构建参数的包,或者必须按原来的Linux目录布局运行的程序,还可能要写包、使用override/overrideAttrs,或者考虑容器。这类软件需要另外处理构建或运行环境,仍然可以根据用途放入相应的系统、用户或项目配置。上一篇的mkForce处理的是模块选项的优先级,和修改软件包构建不是一回事。

使用AI协助安装时,建议同时说明软件的用途和配置位置。例如,可以要求“给这个项目添加NumPy,并写入项目开发环境”。修改完成后,再进入相应环境,检查软件版本并运行一个简单的测试,确认配置能够正常使用。

下一篇将介绍Home Manager的配置组织方式,以及管理已有dotfiles时的注意事项。

NixOS系列 - 这篇文章属于一个选集。
§ 7: 本文