16 分钟
Nix 高级话题之 profile
version: nix-2.23.2
简述
Nix profile (用户环境, user environments) 是 Nix 实现不同用户拥有不同的环境,实现环境回滚的底层机制。
和 Nix profile 有关命令的主要由 nix-env、 nix-collect-garbage、 nix-channel。
本文将介绍 Nix profile 的原理以及 nix-env、 nix-collect-garbage 详细用法和示例。
(nix-channel 本文不做介绍,下一篇专门讨论)
原理
基本结构和相关命令
nix 在使用
sh <(curl -L https://nixos.org/nix/install) --no-daemon
安装 Nix 时,会在用户的 shell profile (~/.profile
) 中注入类似如下语句。if [ -e ~/.nix-profile/etc/profile.d/nix.sh ]; then . ~/.nix-profile/etc/profile.d/nix.sh; fi # added by Nix installer
这个脚本主要设置了
PATH
、MANPATH
、XDG_DATA_DIRS
环境变量,让命令,man 可以识别 nix 安装的包。执行
nix-env -iA nixpkgs.hello
安装一个包后,观察情况。其中
~/.nix-profile
是一个软链,单用户模式,该软链指向~/.local/state/nix/profiles/profile
,而~/.local/state/nix/profiles/profile
也是一个软链,指向同目录的profile-1-link
,而最终~/.local/state/nix/profiles/profile-1-link
指向 nix store 中的一个 user-environments 目录,如/nix/store/197xfcwzc2xk6wkjyblc37grnpc3k4xk-user-environment
。示意如下:~/.nix-profile -> ~/.local/state/nix/profiles/profile -> ~/.local/state/nix/profiles/profile-2-link -> /nix/store/h1m8pdwqh0vj4xq6jr5cwlb21z9rprgb-user-environment
~/.nix-profile
是整个 nix profile 的入口文件,nix-env --switch-profile
命令的职责就是改变这个软链的指向,详见下文。~/.local/state/nix/profiles
是单用户模式默认的 user profiles 的存储目录,包含一堆软链,nix-env --install|--remove|--rollback|--switch-generation
、nix-channel
等命令均会操作该目录,示意如下:channels -> channels-1-link channels-1-link -> /nix/store/197xfcwzc2xk6wkjyblc37grnpc3k4xk-user-environment profile -> profile-2-link profile-1-link -> /nix/store/mndsg7lyka8k7bsh3dxmrpk8rzcbkbr1-user-environment profile-2-link -> /nix/store/h1m8pdwqh0vj4xq6jr5cwlb21z9rprgb-user-environment
nix-env --install|--remove
完成安装卸载后,会生成一个新的/nix/store/xxx-user-environment
,然后创建一个~/.local/state/nix/profiles/profile-x-link
软链,然后更新~/.local/state/nix/profiles/profile
软链的指向。nix-env --rollback|--switch-generation
执行后,将更新~/.local/state/nix/profiles/profile
的指向。nix-channel
命令和nix-env
类似,会更新~/.local/state/nix/profiles/channels
相关的文件,详见下一篇文章。
最终
~/.nix-profile
将指向/nix/store/xxx-user-environment
,该目录就是 nix 存储用户环境的路径。其目录和常规 Linux 的/usr/local
结构类似,示例如下:/nix/store/h1m8pdwqh0vj4xq6jr5cwlb21z9rprgb-user-environment ├── bin │ ├── hello -> /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1/bin/hello │ ├── nix -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix │ ├── nix-build -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-build │ ├── nix-channel -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-channel │ ├── nix-collect-garbage -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-collect-garbage │ ├── nix-copy-closure -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-copy-closure │ ├── nix-daemon -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-daemon │ ├── nix-env -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-env │ ├── nix-hash -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-hash │ ├── nix-instantiate -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-instantiate │ ├── nix-prefetch-url -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-prefetch-url │ ├── nix-shell -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-shell │ └── nix-store -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/bin/nix-store ├── etc -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/etc ├── lib -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib ├── libexec -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/libexec ├── manifest.nix -> /nix/store/kbhbbkqbyq1ii3y5hclzsbzir82v87js-env-manifest.nix └── share ├── bash-completion -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/bash-completion ├── fish -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/fish ├── info -> /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1/share/info ├── locale -> /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1/share/locale ├── man └── zsh -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/zsh
- 安装的 hello 可执行文件在
bin/
下存在一个软链。 ~/.profile
中 source 的~/.nix-profile/etc/profile.d/nix.sh
最终指向的就是该目录的etc/profile.d/nix.sh
下。manifest.nix
是一个 nix 代码文件,数据结构为derivation[]
(derivation 数组), 记录了这个 profile 装的包列表的一些元信息,nix-env --query --installed
数据来源就是该文件,通过nix-instantiate --strict --eval manifest.nix --json
可以以 json 格式,获取到安装的包存储路径,从而可以获取更多信息,示例如下:nix-env -q --installed --json --out-path --drv-path --description --meta nix-instantiate --strict --eval ~/.nix-profile/manifest.nix --json # ["/nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1","/nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2"] nix-store --query --deriver /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1 # /nix/store/c9v5d926q8d2cdb0jq6k3ybnxqdl3nb2-hello-2.12.1.drv nix derivation show /nix/store/c9v5d926q8d2cdb0jq6k3ybnxqdl3nb2-hello-2.12.1.drv --extra-experimental-features nix-command nix derivation show /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1 --extra-experimental-features nix-command # 略
- 安装的 hello 可执行文件在
derivation outputs 和 profile
# 安装 gcc 标准编译器(nix wrapper)
nix-env -iA nixpkgs.gcc13
# 观察 profiles 情况
tree -L 2 ~/.nix-profile/
# /home/rectcircle/.nix-profile/
# ├── bin
# │ ├── addr2line -> /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/addr2line
# │ ├── ar -> /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/ar
# │ ├── as -> /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/as
# │ ├── c++ -> /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/c++
# │ ├── cc -> /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/cc
# │ ├── c++filt -> /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/c++filt
# │ ├── cpp -> /nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0/bin/cpp
# │ ├── ...
# │ └── ...
# ├── etc -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/etc
# ├── lib -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib
# ├── libexec -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/libexec
# ├── manifest.nix -> /nix/store/0p5qf6srlcxp30vcyr9j7jcj4qhpjh9g-env-manifest.nix
# └── share
# ├── bash-completion -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/bash-completion
# ├── fish -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/fish
# ├── info -> /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1/share/info
# ├── locale -> /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1/share/locale
# ├── man
# └── zsh -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/share/zsh
nix-env -q --installed --out-path
# gcc-wrapper-13.3.0 man=/nix/store/l3nxj0c8zippk5aijmkndn0zh6j7h55s-gcc-wrapper-13.3.0-man;/nix/store/zansxqviinfh345skvpy5f0z58snr229-gcc-wrapper-13.3.0
# hello-2.12.1 /nix/store/r8mfs49cp5q9l0q8zj2ab78h7gx2chfb-hello-2.12.1
# nix-2.23.2 /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2
tree -L 3 /nix/store/l3nxj0c8zippk5aijmkndn0zh6j7h55s-gcc-wrapper-13.3.0-man
# /nix/store/l3nxj0c8zippk5aijmkndn0zh6j7h55s-gcc-wrapper-13.3.0-man
# └── share
# └── man
# ├── man1
# └── man7
可以看出,derivation outputs 目录 out
和 man
的 bin/
、share/man
都被正确的安装到了 ~/.nix-profile/
目录(默认是否安装到 profiles 中,是由 meta.outputsToInstall
属性控制的,默认应该是 out
)。
nix-env -iA nixpkgs.libgcc
tree -L 2 ~/.nix-profile/
# 输出关键点如下
# /home/rectcircle/.nix-profile/
# ├── bin
# ├── ...
# ├── lib
# │ ├── libboost_context.so -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib/libboost_context.so
# │ ├── ...
# │ ├── libgcc_s.so -> /nix/store/pd8xxiyn2xi21fgg9qm7r0qghsk8715k-gcc-13.3.0-libgcc/lib/libgcc_s.so
# │ ├── libgcc_s.so.1 -> /nix/store/pd8xxiyn2xi21fgg9qm7r0qghsk8715k-gcc-13.3.0-libgcc/lib/libgcc_s.so.1
# │ ├── libnixcmd.so -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib/libnixcmd.so
# │ └── ...
# └── ...
可以看出,lib 目录变成了 nix 的 lib 以及 libgcc 的 lib 的聚合体。
最后,安装一下其他常见的 lib 库。
nix-env -iA nixpkgs.libz
nix-env -iA nixpkgs.libxcrypt
tree -L 2 ~/.nix-profile/
# 输出关键点如下
# /home/rectcircle/.nix-profile/
# ├── bin
# ├── ...
# ├── include
# │ ├── c++ -> /nix/store/zc0nsv23pakbafngjy32kvhfzb16as43-gcc-13.3.0/include/c++
# │ └── crypt.h -> /nix/store/gk0hrl9rngz3lfrnisql0h4xm65p036z-libxcrypt-4.4.36/include/crypt.h
# ├── lib
# │ ├── ...
# │ ├── libcrypt.so -> /nix/store/gk0hrl9rngz3lfrnisql0h4xm65p036z-libxcrypt-4.4.36/lib/libcrypt.so
# │ ├── libcrypt.so.2 -> /nix/store/gk0hrl9rngz3lfrnisql0h4xm65p036z-libxcrypt-4.4.36/lib/libcrypt.so.2
# │ ├── libcrypt.so.2.0.0 -> /nix/store/gk0hrl9rngz3lfrnisql0h4xm65p036z-libxcrypt-4.4.36/lib/libcrypt.so.2.0.0
# │ ├── ...
# │ ├── libz.so -> /nix/store/xnpg0ssr0hjrz8srf3saviy69w38rkhd-libz-1.2.8.2015.12.26-unstable-2018-03-31/lib/libz.so
# │ ├── libz.so.1 -> /nix/store/xnpg0ssr0hjrz8srf3saviy69w38rkhd-libz-1.2.8.2015.12.26-unstable-2018-03-31/lib/libz.so.1
# │ ├── libz.so.1.2.8 -> /nix/store/xnpg0ssr0hjrz8srf3saviy69w38rkhd-libz-1.2.8.2015.12.26-unstable-2018-03-31/lib/libz.so.1.2.8
# │ └── ...
# └── ...
可以看出,这些包的 include、lib 都安装到了正确的位置。
再验证一下已经安装了 gcc 的情况下再安装 clang。
nix-env -iA nixpkgs.clang_16
# 报错
# error: Unable to build profile. There is a conflict for the following files:
# /nix/store/g2f50c20wy9ca6nd46d449v5gbzx4rwy-clang-wrapper-16.0.6/bin/addr2line
# /nix/store/piz0jc0js7xnnka355n2yw07zj7p2hgq-gcc-wrapper-14.1.0/bin/addr2line
# error: builder for '/nix/store/1wqinfagy9lxqlanszck2fh5rsbkpxy4-user-environment.drv' failed with exit code 1
可以看出,如果两个包存在同名的二进制,将提示冲突。
最后再安装 libmysqlclient,来观察一下 outputs 包含 dev 的场景:
nix-env -iA nixpkgs.libmysqlclient
# ...
# copying path '/nix/store/p81agrmnhd2mm8hraqa52j3hl344bsjk-mariadb-connector-c-3.3.5' from 'https://cache.nixos.org' ...
# copying path '/nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev' from 'https://cache.nixos.org' ...
nix-env -q --installed --out-path
# ...
# mariadb-connector-c-3.3.5 /nix/store/p81agrmnhd2mm8hraqa52j3hl344bsjk-mariadb-connector-c-3.3.5
# ...
ls -al ~/.nix-profile/lib/
# ...
# mariadb -> /nix/store/p81agrmnhd2mm8hraqa52j3hl344bsjk-mariadb-connector-c-3.3.5/lib/mariadb
# ...
which mariadb_config
# mariadb_config not found
ls -al /nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev/bin
# mariadb_config
# mysql_config -> mariadb_config
ls -al ~/.nix-profile/bin/mariadb_config
# ls: cannot access '$HOME/.nix-profile/bin/mariadb_config': No such file or directory
cat ~/.nix-profile/manifest.nix
# [ { meta = { ...; outputsToInstall = [ "out" ]; ... }; name = "mariadb-connector-c-3.3.5"; ...; outputs = [ "out" ]; ... } ...]
nix-shell --pure -p libmysqlclient --run env | grep PATH
# stdenv=/nix/store/d3dzfy4amjl826fb8j00qp1d9887h7hm-stdenv-linux
# buildInputs=/nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev
# PATH=...:/nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev/bin:...
nix derivation show nixpkgs#libmysqlclient --extra-experimental-features 'nix-command flakes'
# {
# "/nix/store/jnwpkqz0qx9cx7ljirsks5s4b5lxhmz7-mariadb-connector-c-3.3.5.drv": {
# //...
# "name": "mariadb-connector-c-3.3.5",
# "inputDrvs": {
# //...
# "/nix/store/kjniqf3ladgc55nh4h41vrcwp3z7426b-zlib-1.3.1.drv": {
# "dynamicOutputs": {},
# "outputs": [
# "dev"
# ]
# },
# //...
# }
# "outputs": {
# "dev": {
# "path": "/nix/store/lzjh7kfbwhcslywmas0288w1k5k8zh93-mariadb-connector-c-3.3.5-dev"
# },
# "out": {
# "path": "/nix/store/118ayny4nv1d687bgi4js46b40wg4md2-mariadb-connector-c-3.3.5"
# }
# },
# //...
# }
# }
nix-instantiate --eval --expr 'let pkgs = import <nixpkgs> {}; in pkgs.libmysqlclient.outputs'
# [ "out" "dev" ]
nix-instantiate --eval --expr 'let pkgs = import <nixpkgs> {}; in pkgs.zlib.outputs'
# [ "out" "dev" "static" ]
可以发现 libmysqlclient derivation outputs 是 [ "out" "dev" ]
,但 nix-env --install
的 dev 目录的 mariadb_config
可执行文件并没有安装到 profiles 里面,也就是说安装的 out
输出。
libmysqlclient 这个包配置的 outputs 是 [ "out" "dev" ]
,当执行 nix-shell
shell 是,配置到 PATH 里的是 /nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev/bin
,说明 nix-shell
引用的是 dev
输出。
如果想安装 dev 目录到 profile 中,需要强制指定 nix-env -iA nixpkgs.libmysqlclient.dev nixpkgs.libmysqlclient.out
都安装(特别提醒: nix 似乎有 bug 一旦下面命令执行, profile 就损坏了!因此建议直接使用 nix-shell)。
nix-env -iA nixpkgs.libmysqlclient.dev nixpkgs.libmysqlclient.out
nix-env -q --installed --out-path
# ...
# mariadb-connector-c-3.3.5 dev=/nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev
# mariadb-connector-c-3.3.5 /nix/store/p81agrmnhd2mm8hraqa52j3hl344bsjk-mariadb-connector-c-3.3.5
# ...
ls -al ~/.nix-profile/lib/
# ...
# mariadb -> /nix/store/p81agrmnhd2mm8hraqa52j3hl344bsjk-mariadb-connector-c-3.3.5/lib/mariadb
# ...
which mariadb_config
# /home/cloudide/.nix-profile/bin/mariadb_config
ls -al ~/.nix-profile/bin/mariadb_config
# $HOME/.nix-profile/bin/mariadb_config -> /nix/store/3j0l731cns49pzsffl3pfqini5yf4sqh-mariadb-connector-c-3.3.5-dev/bin/mariadb_config
nix-env -iA nixpkgs.zlib
# 报错
# error: this derivation has bad 'meta.outputsToInstall'
cat ~/.nix-profile/manifest.nix
# [ { meta = { ...; outputsToInstall = [ "dev" ]; ... }; name = "mariadb-connector-c-3.3.5"; ...; outputs = [ "out" ]; ... } ...]
总结,在执行 nix-env --install
时:
- nixpkgs 声明的 derivation 都有一个
meta.outputsToInstall
属性(一般情况下为out
或bin
),会将其指向的子目录都软链到 ~/.nix-profile/ 中。如果裸使用derivation
,没有配置meta.outputsToInstall
,nix-env 会安装所有的 outputs。 多个包的 outputs 的子目录会进行合并,合并是递归的进行:如果安装的包的 outputs 的子目录没有没有重复的,则直接创建一个软链指向到这个子目录。如果存在存在重复的,则在中创建这个目录,然后创建软链。
# 安装了 nix,只有 nix 的 output 目录有 lib 目录,此时 lib 为: lib -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib # 安装了 nix 和 libgcc,这两个目录都有 lib 目录 lib ├── libboost_context.so -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib/libboost_context.so ├── ... ├── libgcc_s.so -> /nix/store/pd8xxiyn2xi21fgg9qm7r0qghsk8715k-gcc-13.3.0-libgcc/lib/libgcc_s.so ├── libgcc_s.so.1 -> /nix/store/pd8xxiyn2xi21fgg9qm7r0qghsk8715k-gcc-13.3.0-libgcc/lib/libgcc_s.so.1 ├── libnixcmd.so -> /nix/store/af39xch7s21s36bd3j8gjssmcbhgm42y-nix-2.23.2/lib/libnixcmd.so └── ...
如果最终存在冲突(比如:gcc 和 clang 都需要安装 bin/addr2line),同时安装,将报错。有两点说明如下:
- 默认情况, nix-env 安装的 pname 相同包时,旧的 pname 的包将被删除,并安装这个新的 pname 包(未找到相关文档,实测如此),执行
nix-env --install
时,可使用--preserve-installed
阻止该行为,行为切换为报错冲突。 - derivation 有一个
meta.priority
属性(文档),如果两个包的优先级相同,nix-env 安装存在冲突时,就会报错,如果安装一个优先级更高的包存在冲突时,这个包会覆盖之前安装的优先级地的包。meta.priority
的默认值为5
。另外,也可以通过nix-env --set-flag priority 数字
调整已安装的包的优先级。
- 默认情况, nix-env 安装的 pname 相同包时,旧的 pname 的包将被删除,并安装这个新的 pname 包(未找到相关文档,实测如此),执行
nix-env --install
支持指定安装特定的 outputs,格式形如nixpkgs.libmysqlclient.dev
,但是,这样会破坏掉 profile ,导致后续安装任何的包都报错。原因是生成的manifest.nix
中meta.outputsToInstall
属性的值不包含在outputs
属性中。由于 nix 的包都是 nixpkgs 维护的,而关于 outputs 目录, nixpkgs 有如下如下约定:
- 如果 outputs 有多个输出,
out
目录一般放到最前面,例如[ "out" "dev" ]
。 meta.outputsToInstall
默认值规则为:如果 outputs 存在 bin 目录,则添加 bin;如果存在 out 目录,则添加 out;否则添加 outputs 的第一个。最后,如果存在 man,一定会 append man (详见:源码) 。- nixpkgs 的包维护者,可以按需选择
outputs
中的目录添加到meta.outputsToInstall
中,一般情况下 dev 目录一般不会加到这个属性中。 - 使用 nix-shell 或 nix-build 包的依赖是通过
nixpkgs.lib.stdenv.mkDerivation
的 buildInputs 声明时,如果这个依赖 outputs 包含 dev 时,实际依赖的是 dev 而非 out 目录。源码详见:make-derivation.nix 和 attrsets.nix。 - 关于 outputs 更多参见: Nixpkgs Reference Manual - Multiple-output packages ,博客 How to Learn Nix, Part 29: Derivations in detail,setenv.sh。
- 如果 outputs 有多个输出,
C 库 和 profile
从上文可以看出,lib 和 include 已经正确的设置到 profile 中了,但是和 bin 不同。如果使用的不是 NixOS,而是在传统 Linux (如 debian) 中使用 Nix。上面的 profile 中的 lib include 将不会设置到系统中,因为如果设置了,会和系统的 lib 冲突,造成严重问题。
因此,如果想用 nix 管理 C/C++ 项目的依赖(即 include 头文件 和 lib 动态链接库 so),需要使用 Nix shell 声明依赖,并启动一个 shell,这个 shell 里面会设置 NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu=1
等 NIX_CFLAGS_COMPILE
以及 NIX_LDFLAGS
(详见源码: setup.sh,setup-hook.sh。
然后使用 wrapper 的 C/C++ 编译器(如 nixpkgs.gcc13
、 nixpkgs.clang_16
),这样 C/C++ 编译器才能使用正确的识别 Nix 安装依赖。
详见: Nix Wiki - C。
nix-env 命令详解
nix 通过 nix-env 命令来实现对 profile 的管理,本部分将详细介绍该命令的各种能力和细节。
nix-env --delete-generations
删除 profile 的历史版本,示例如下:
nix-env delete-generations 1 2 3
删除 profile 的 1、2、3 版本nix-env --delete-generations old
删除除了当前版本之外的所有的版本。nix-env delete-generations 30d
删除 30 天之前的版本。nix-env delete-generations 5+
保留当前版本之前的 5 个版本以及大于当前版本的版本,删除其他的版本。
nix-env --install
安装一个或多个包 (derivation) 到 profile 中,语法如下:
nix-env {--install | -i} args… [{--prebuilt-only | -b}] [{--attr | -A}] [--from-expression] [-E] [--from-profile path] [--preserve-installed | -P] [--remove-all | -r]
安装包的各种写法如下:
# 最常见的写法(通过属性名安装 nixpkgs 的包)
nix-env -iA nixpkgs.python312
nix-env --install --attr nixpkgs.python312
# 根据 derivation 的 name 安装(这种方式需要评估所有 nixpkgs 的包,性能很差,不推荐用在 nixpkgs 的包的安装)
nix-env --install python3-3.12.3
# -A 和 -f 结合 channel
nix-env -iA nixpkgs.python312 -f ~/.nix-defexpr/
nix-env -iA nixpkgs.python312 -f ~/.nix-defexpr/channels
nix-env -iA python312 -f ~/.nix-defexpr/channels/nixpkgs
# -f 自定义 nix 表达式
# -f 文件,最终是一个 derivation
cat > /tmp/single-python.nix <<EOF
let pkgs = import <nixpkgs> {}; in pkgs.python312
EOF
nix-env -i -f /tmp/single-python.nix
# -f 文件,最终是一个 derivation 列表
cat > /tmp/list-python.nix <<EOF
let pkgs = import <nixpkgs> {}; in [pkgs.python312]
EOF
nix-env -i -f /tmp/list-python.nix
# -f 文件,最终是一个属性集
cat > /tmp/attrset-python.nix <<EOF
let pkgs = import <nixpkgs> {}; in { python312 = pkgs.python312; }
EOF
nix-env -iA python312 -f /tmp/attrset-python.nix
# -f 文件最终函数声明为 a: derivation ,这里的 a 是一个属性集。
cat > /tmp/func-single-python.nix <<EOF
{ pkgs ? import <nixpkgs> {} }: pkgs.python312
EOF
# 实测,改为如下是不行的
# _: let pkgs = import <nixpkgs> {}; in pkgs.python312
nix-env -i -f /tmp/func-single-python.nix
# -f 文件最终函数声明为 a: derivation列表,这里的 a 是一个属性集。
cat > /tmp/func-list-python.nix <<EOF
{ pkgs ? import <nixpkgs> {} }: pkgs.python312
EOF
nix-env -i -f /tmp/func-list-python.nix
# -f 文件最终函数声明为 a: {} 这里的 a 是一个属性集。 (和 nixpkgs 原理相同)
cat > /tmp/func-attrset-python.nix <<EOF
{ pkgs ? import <nixpkgs> {} }: { python312 = pkgs.python312; }
EOF
nix-env -i -f /tmp/func-attrset-python.nix '.*' # 方式 0:可以通过 .* 安装属性集中的所有包。
nix-env -iA python312 -f /tmp/func-attrset-python.nix # 方式 1
nix-env -iA python312 --arg pkgs 'import <nixpkgs> {}' -f /tmp/func-attrset-python.nix # 方式 2: 验证覆盖函数参数
nix-env -i -E 'a: let mypkgs = a{}; in mypkgs.python312' -f /tmp/func-attrset-python.nix # 方式 3: 使用表达式参数
# 从表达式安装,这个表达式的必须是一个函数,声明为:
# a: derivation { ... }
# 假设这个函数名为 f ,调用方式分为如下两种情况:
# 1. 不传递 -f 参数或者 -f 参数是一个 channel 的 user-environment 时:f { _combineChannels = [ ]; nixpkgs = import <nixpkgs>; }
# 2. -f 参数传递的是一个包含 default.nix 的目录或压缩包下载链接,或者一个 .nix 源代码文件时:let a = import -f参数值; in f a
nix-env --install --from-expression 'a: let pkgs = a.nixpkgs{}; in pkgs.python312'
nix-env --install --from-expression 'a: let pkgs = a.nixpkgs{}; in pkgs.python312' -f ~/.nix-defexpr/channels
nix-env --install --from-expression 'nixpkgs: let pkgs = nixpkgs{}; in pkgs.python312' -f ~/.nix-defexpr/channels/nixpkgs/
# 直接通过 store path 或 store derivation path 安装。
nix-env -i $(nix-instantiate --expr 'let pkgs = import <nixpkgs> {}; in pkgs.python312')
默认情况, nix-env 安装的 pname 相同包时,旧的 pname 的包将被删除,并安装这个新的 pname 包,使用 --preserve-installed
参数检测这种情况并直接报错,示例如下:
nix-env -iA nixpkgs.python311
nix-env -iA nixpkgs.python312 --preserve-installed
# 报错: error: Unable to build profile. There is a conflict for the following files:
nix-env -iA nixpkgs.python312
# replacing old 'python3-3.11.9'
# installing 'python3-3.12.3'
nix-env --query --installed
# 只会输出 python3-3.12.3,不会输出 python3.11
从其他 profile 中安装,可用于 copy 其他用户的 profile。
nix-env -iA python312 --from-profile /nix/store/xxx-user-environment
将包安装到其他的 nix-profile。
mkdir -p /tmp/myprofiles
nix-env -i -A nixpkgs.python312 nixpkgs.nix --profile /tmp/myprofiles/profile
ls -al /tmp/myprofiles
# profile -> profile-1-link
# profile-1-link -> /nix/store/3hb6nr0n05a2iwbxm0i50968mw2dd220-user-environment
重要参数总结,以及其他参数说明如下:
--prebuilt-only
/-b
只从 substitute 中安装与构建的包,永不从源码构建。--remove-all
/-r
删除所有其他已安装的包,再执行安装,相当于首先运行nix-env --uninstall '.*'
,只不过一切都发生在单个事务中。--file
/-f
从哪里获取 nix 表达式,有两种情况:- 不填,默认为 nix-channel 维护的 (Nix 表达式搜索路径)
~/.nix-defexpr/channels/
,详见后文 nix-channel 。 - 可以是本地 nix 源代码文件,包含 default.nix 的目录或压缩包下载链接(如 github archive),要求其表达式类型推导最终的类型定义可以是如下六种情况:
derivation
[]derivation
derivation 列表{}
属性集。a: derivation
函数,其中 a 是属性集。a: derivation[]
函数,其中 a 是属性集。a: {}
函数,其中 a 是属性集。- 以上 a 的一般写法为
{ pkgs ? import <nixpkgs> {} }: ...
包含默认值,如需自定义,可以使用--arg
指定。
- 不填,默认为 nix-channel 维护的 (Nix 表达式搜索路径)
--from-expression
/-E
从表达式安装,这个表达式的必须是一个函数,声明为:--attr
/-A
可以指定多个,用来指定要安装的包,这里的写法和--file
参数有关。只有--file
/-f
的最终评估值是一个属性集时,才能使用该参数。--profile
安装到指定的 profile 必须是一个软链的路径。--dry-run
模拟执行。-I
path,指定包搜索路径,可多次给出,也可以通过NIX_PATH
环境变量配置,-I 优先级高于环境变量,默认为~/.nix-defexpr/channels/
,主要再如下场景使用:- nix 表达式语言的
<nixpkgs>
语法。 nix-env --attr nixpkgs.python312
语法。
如上吗,底层都是用
builtins.findFile
,原理是查找对应的目录,且该目录包含default.nix
。- nix 表达式语言的
nix-env --list-generations
列出当前 profile 的所有版本。
nix-env --query
查询包(derivation)信息,按 name
排序,语法如下:
nix-env {--query | -q} names… [--installed | --available | -a] [{--status | -s}] [{--attr-path | -P}] [--no-name] [{--compare-versions | -c}] [--system] [--drv-path] [--out-path] [--description] [--meta] [--xml] [--json] [{--prebuilt-only | -b}] [{--attr | -A} attribute-path]
有如下两种查询目标选择:
--installed
查询已安装的包,该选项是默认的。--available
或-a
查询 channel 中,可用的包的信息。
指定查询结构输出格式,默认为文本,可通过 --xml
、--json
指定输出为 xml 或 json 格式。
nix-env --query --xml
# <?xml version='1.0' encoding='utf-8'?>
# <items>
# <item attrPath="1" name="nix-2.24.0pre20240627_b44909ac" outputName="" pname="nix" system="x86_64-linux" version="2.24>
# <output name="man" />
# <output name="out" />
# </item>
# ...
# </items>
nix-env --query --json
# {
# "0": {
# "name": "python3-3.12.3",
# "outputName": "",
# "outputs": {
# "out": null
# },
# "pname": "python3",
# "system": "x86_64-linux",
# "version": "3.12.3"
# },
# ...
# }
其他选项说明如下:
--prebuilt-only
或-b
选项,只查询那些能从 substitute 中安装的包,永不从源码构建(主要和--available
配合使用,主要为了过滤出那些可以快速安装的包)。--status
或-s
选项,查询包的状态,状态包含 3 个字符I
已安装到当前 profile。P
已经保存到 store。S
是否在 substitute 中有可用的预购建的产物。
示例如下:
nix-env --query --status #IPS nix-2.24.0pre20240627_b44909ac #IPS python3-3.12.3
--attr-path
或-P
选项,(仅能和--available
一起使用)打印属性路径。--no-name
选项,不打印name
。--compare-versions
或-c
选项,查询已安装的包的版本和 channel 中可用的包的版本的差异,主要用于检测是否有新版本,示例如下:nix-env --query --compare-versions # nix-2.24.0pre20240627_b44909ac = 2.24.0pre20240627_b44909ac # python3-3.12.3 < 3.13.0b3
--system
选项,打印包的 system 字段。--drv-path
选项,打印 store derivation 路径。--out-path
选项,打印 derivation 的输出路径。--description
选项,打印 derivation 的meta.description
属性。meta
选项,打印 derivation 的meta
属性,该选项只能和--xml
和--json
一起使用。
nix-env --rollback
将当前 profile 回滚到上一版本。
nix-env --set-flag
修改已安装的包的 meta 下的属性,语法如下:
nix-env --set-flag name value drvnames
目前支持三个:
priority
,修改已安装包的meta.priority
字段,影响安装存在冲突的包时的覆盖关系。nix-env -iA nixpkgs.gcc nix-env -iA nixpkgs.binutils nix-env --query binutils-wrapper --meta --json | grep priority # "priority": 10, # 默认安装将失败 nix-env -iA nixpkgs.gcc # error: Unable to build profile. There is a conflict for the following files: # 报错 # /nix/store/l46fjkzva0bhvy9p2r7p4vi68kr7a1db-binutils-wrapper-2.41/bin/addr2line # /nix/store/mpm3i0sbqc9svfch6a17179fs64dz2kv-gcc-wrapper-13.3.0/bin/addr2line # 修改优先级后重新安装将成功 nix-env --set-flag priority 1 binutils-wrapper nix-env --query binutils-wrapper --meta --json | grep priority # "priority": "1", nix-env -iA nixpkgs.gcc nix-env --query gcc-wrapper-13.3.0 --meta --json | grep priority # "priority": 10,
keep
,可以设置为 true 以防止包被升级或替换。如果您想保留旧版本的软件包,这非常有用。nix-env --set-flag keep true python3-3.12.3 nix-env --query python3-3.12.3 --meta --json | grep keep
active
,可以设置为 false 以禁用该包。也就是说,不会生成包文件的符号链接,但它仍然是配置文件的一部分(因此不会被垃圾收集)。可以将其设置回 true 以重新启用该包。nix-env --install -A nixpkgs.python312 which python # /home/rectcircle/.nix-profile/bin/python nix-env --set-flag active false python3-3.12.3 which python # 找不到
nix-env --set
设置 profile 指向一个特定的 derivation。
nix-env --set drvname
示例如下:
nix-env --set nix
nix-env --query
# 无输出
readlink -f
# /nix/store/9b72q76kfi4v0vm08vrsdllw62wpb1ka-nix-2.24.0pre20240627_b44909ac
nix-env -iA nixpkgs.python312
which nix
# 无输出
可以看出:
--set
参数实际上可以是 pname 也可以是 name。--set
执行 后直接把 profile 指向了 nix 这个包,并没有生成 user environments。- 后续再执行
--install
,上面的--set
的包会丢失。
没有想到这个命令的用途,官方的示例是:
nix-env --profile /nix/var/nix/profiles/browser --set firefox
可能在 NixOS 场景有用吧。
nix-env --switch-generation
将当前 profile 切换到指定的版本。语法如下:
nix-env {--switch-generation | -G} generation
本质上是修改 profile 软链指向软链的指向,即 ~/.nix-profile
指向的文件 ~/.local/state/nix/profiles/profile
指向新的 profile-xxx-link
。
nix-env --switch-profile
切换用户环境 profile,语法如下:
nix-env {--switch-profile | -S} path
~/.nix-profile
是整个 nix profile 的入口文件,该命令的职责就是改变这个软链的指向,详见下文。
nix-env --uninstall
卸载包,语法如下:
nix-env {--uninstall | -e} drvnames…
示例如下:
nix-env --uninstall gcc-wrapper
# uninstalling 'gcc-wrapper-13.3.0'
# building '/nix/store/xmn76h44mpyh7s50slmfaqj1l89cmznw-user-environment.drv'...
nix-env -iA nixpkgs.gcc
nix-env --uninstall gcc-wrapper-13.3.0
# uninstalling 'gcc-wrapper-13.3.0'
# building '/nix/store/xmn76h44mpyh7s50slmfaqj1l89cmznw-user-environment.drv'...
可以看出:
--uninstall
参数实际上可以是 pname 也可以是 name。- 卸载操作也会生成一个新的版本,可用以
--rollback
回滚。
nix-env --upgrade
升级一个包,语法如下:
nix-env {--upgrade | -u} args [--lt | --leq | --eq | --always] [{--prebuilt-only | -b}] [{--attr | -A}] [--from-expression] [-E] [--from-profile path] [--preserve-installed | -P]
可选择如下 4 种升级策略中一种:
--lt
选项,默认值。将包升级到 channel 中 pname 相同的更新的版本,如果 channel 降级了,或版本号不变,则啥也不做。--leq
选项,channel 中同名 pname 的版本号大于等于该包,则升级到 channel 中这个版本。--eq
选项,channel 中同名 pname 的版本号等于该包,则升级到 channel 中这个版本,这种情况主要用来处理这个包本身没有变化,但是依赖有变化的场景。--always
选项,不管 channel 中同名 pname 的版本号是否变化,总是重新升级。
其他参数:
--prebuilt-only
或-b
选项,只升级那些能从 substitute 中安装的包,永不从源码构建。--preserve-installed
或-P
选项,不清楚这个选项的意义,原文如下:Do not remove derivations with a name matching one of the derivations being installed. Usually, trying to have two versions of the same package installed in the same generation of a profile will lead to an error in building the generation, due to file name clashes between the two versions. However, this is not the case for all packages.
示例如下:
# 升级 gcc,指定了属性名(推荐,无需计算所有包,速度很快推荐)。
nix-env --upgrade --attr nixpkgs.gcc
# 升级 gcc,使用 panme (很慢,要计算 channel 中所有的包的 pname,然后匹配,和下面升级所有包速度一样慢)
nix-env --upgrade gcc-wrapper
# 升级所有包(很慢,要计算 channel 中所有的包)。
nix-env --upgrade --always
垃圾回收
Nix 参考手册 - 6.2 Garbage Collection | Nix 参考手册 - 8.4.2 nix-collect-garbage
上文的 nix-env --uninstall
仅仅是生成了一个新的 profiles,并没有真正删除存储对象。通过 nix-collect-garbage
命令可以回收那些不可达的存储对象,这个行为被称作垃圾回收。
nix 会遍历如指定路径的 profiles 所有版本直接引用和间接引用(闭包)的存储对象列表,不再该列表的存储对象就是不可达的。
nix 遍历的 profiles 路径如下:
- 在 profile 章节 说明的目录:
$XDG_STATE_HOME/nix/profiles
普通用户的 profiles,一般情况下为~/.local/state/nix/profiles
(多用户安装模式会遍历所有使用 Nix 的用户路径的该路径)。$NIX_STATE_DIR/profiles/per-user/root
,root 用户的 profiles,一般情况下为/nix/var/nix/profiles/per-user/root
。
- 兼容历史版本的路径(未来可能有变化):
$NIX_STATE_DIR/profiles
和$NIX_STATE_DIR/profiles/per-user
。
在实现上,上述目录都在 /nix/var/nix/gcroots/
目录下存在软链。在执行垃圾回收时,就是根据该目录进行查找的。
- 观察
/nix/var/nix/gcroots/auto
,可以看到大量指向~/.local/state/nix/profiles/profile-xxx-link
的软链。 - 如果想让某个包,某个 profiles 依赖的包,不被垃圾回收,可以在
/nix/var/nix/gcroots/
目录创建软链。 nix-store
、nix-instantiate
等命令存在一个--add-root path
选项,可以将某个存储对象添加到/nix/var/nix/gcroots/auto
目录下 (本质上nix-env
也是通过这个命令来不被垃圾回收的)。nix-store --add-root /home/eelco/bla/result --realise ... # $ ls -l /nix/var/nix/gcroots/auto # lrwxrwxrwx 1 ... 2005-03-13 21:10 dn54lcypm8f8... -> /home/eelco/bla/result $ ls -l /home/eelco/bla/result # lrwxrwxrwx 1 ... 2005-03-13 21:10 /home/eelco/bla/result -> /nix/store/1r11343n6qd4...-f-spot-0.0.10
nix-collect-garbage
命令,语法如下:
nix-collect-garbage [--delete-old] [-d] [--delete-older-than period] [--max-freed bytes] [--dry-run]
选项说明如下:
- 不加任何参数,等价于
nix-store --gc
。 --delete-old
或-d
,删除所有用户的 profiles 历史,等价于针对每个 profiles 先执行nix-env --delete-generations old
,再执行nix-store --gc
。--delete-older-than period
,等价于针对每个 profiles 先执行nix-env --delete-generations <period>
,再执行nix-store --gc
。
已知问题
报错 error: this derivation has bad ‘meta.outputsToInstall’
# 安装 dig (attribute 是 dig, name 是 bind),可以安装成功
nix-env -iA nixpkgs.dig
# 再安装任意一个包,会报错
nix-env -iA nixpkgs.ruby
# error: this derivation has bad 'meta.outputsToInstall'
此时观察 ~/.nix-profile/manifest.nix
中元信息内容如下:
{
name = "bind-9.18.28";
meta = {
mainProgram = "dig";
name = "bind-9.18.28";
outputsToInstall = [ "out" "dnsutils" "host" ];
position = "/nix/store/7z7lzz187w5in6lplpxrqrzqh215sklb-nixpkgs/nixpkgs/pkgs/servers/dns/bind/default.nix:125"; unfree = false;
# ...
};
outputs = [ "dnsutils" ];
dnsutils = { outPath = "/nix/store/04wa3k4m2qdfnd56605hhrw91zihqycg-bind-9.18.28-dnsutils"; };
outPath = "/nix/store/04wa3k4m2qdfnd56605hhrw91zihqycg-bind-9.18.28-dnsutils";
# ...
}
# ...
想临时修复这个问题,就是使用包名 nixpkgs.bind
重新安装或者卸载 bind
。
# 安装 bind (attribute 是 bind, name 是 bind)
nix-env -iA nixpkgs.bind
# 或卸载 bind: nix-env --uninstall bind
# 后续安装任何包将不会报错
此时,再观察 ~/.nix-profile/manifest.nix
中元信息内容如下:
{
name = "bind-9.18.28";
meta = {
name = "bind-9.18.28";
outputsToInstall = [ "out" "dnsutils" "host" ];
position = "/nix/store/7z7lzz187w5in6lplpxrqrzqh215sklb-nixpkgs/nixpkgs/pkgs/servers/dns/bind/default.nix:125";
## ...
};
outputs = [ "dnsutils" "host" "out" ];
dnsutils = { outPath = "/nix/store/04wa3k4m2qdfnd56605hhrw91zihqycg-bind-9.18.28-dnsutils"; };
host = { outPath = "/nix/store/pwn4dbl6606b3mpcyasks0mlqwjijsyh-bind-9.18.28-host"; };
out = { outPath = "/nix/store/krw6p2q85a4ca6dvpvfvaxgy99r4xjjh-bind-9.18.28"; };
outPath = "/nix/store/krw6p2q85a4ca6dvpvfvaxgy99r4xjjh-bind-9.18.28";
# ...
}
两者区别在于: 使用 nixpkgs.dig
安装时,meta.outputsToInstall 中有 [ "out" "dnsutils" "host" ]
, outputs 中只有 [ "dnsutils" ]
,找不到。
在 Nixpkgs 中,源码如下:
相关 Issue 如下: