跳转至

software

反向代理的 Partial Transfer 问题

反向代理已经是无处不在,但是如果反向代理没有根据使用场景调优,或者出现了一些异常,可能会带来不好的用户体验,并且现象十分奇怪,例如访问某 GitLab 实例的时候,偶尔会出现页面加载不完整的情况。

这些问题困扰了我们很久,到最后才发现,原来问题在反向代理上。下面就来回顾一下事情的经过。

包管理器打包命令速查

随着 Linux 使用逐渐深入,开始尝试参与到一些发行版/包管理器的维护当中。在此记录一下打包相关命令,方便自己速查。

Clang 如何支持 CUDA 程序

前言

编译 CUDA 程序的主要工具是 NVIDIA 提供的闭源编译器 NVCC,但实际上,NVCC 是基于 LLVM 开发的(来源:NVIDIA CUDA Compiler),NVIDIA 也把 NVCC 其中一部分逻辑贡献给了 LLVM 上游,使得 Clang 也可以在 CUDA 的配合下编译 CUDA 程序。这篇博客尝试研究 Clang/LLVM 如何实现 CUDA 程序的编译,主要是 Clang 前端部分,后端部分,也就是从 LLVM IR 到 NVPTX 的这一步还没有进行深入的研究。

Podman 和 Docker Rootless 实践

最近在配置公用机器的环境,需求是很多用户需要使用 docker,但是众所周知,有 docker 权限就等于有了 root 权限,因此正好想尝试一下现在的 Rootless 容器化方案,例如 docket rootless 和 podman。

SPEC CPU 2006 性能测试

背景

最近在网上看到龙芯 3A6000 的 SPEC CPU 2006 性能评测数据,想着自己也可以在手上的一些平台上测一测,把测试的过程记录在本文。

VFIO - Virtual Function I/O

背景

VFIO 是 Linux 内核中的一个功能,目的是把 PCIe 设备暴露给用户态的程序,进而可以暴露给虚拟机内的系统,也就是常说的虚拟机 PCIe 直通。为了保证安全性,VFIO 还会配置好 IOMMU,保证用户态程序无法利用设备的 DMA 访问到其他地址空间的数据。

本文探讨 VFIO 暴露的用户态 API 以及如何在用户态中使用 VFIO 直接控制 PCIe 设备。

从 libvirtd 迁移到 Proxmox VE

背景

之前用 libvirtd + virt-manager 做 Linux 上的虚拟化,好处是比较轻量级,但是远程控制起来比较麻烦,要么通过 RDP 访问 virt-manager 的 UI,要么就用 cockpit 在网页里去配置虚拟机。此时就会比较怀念 VMware ESXi 的网页,但是 ESXi 装完以后,宿主机就很不自由了,很多东西没法自定义。最后就想到在 Debian 上装一个 Proxmox VE,希望得到一个比较好的中间态。

Tar 文件格式

本文的内容已经整合到知识库中。

背景

最近在解压 tar.gz 文件的时候,发现如果用 unar 解压,就会出现文件名截断到 100 个字节的问题,而如果用 gnu tar 解压,文件名就是正常的,因此深入研究了一下 Tar 的文件格式。实际上,这是因为早期 tar 格式设计的时候,就设定了路径最长 100 字节的限制,后来的扩展解决了这个问题,但是 unar 没能正确地识别扩展,导致解压路径出错。

使用 Docker 部署 OpenLDAP

OpenLDAP 是一个开源的用户系统实现,主要支持 LDAP 协议,可以给其他系统提供用户认证。下面讨论了如何在 Docker 中部署 OpenLDAP。

链接器的工作原理

背景

最近和同学讨论一些比较复杂的链接问题,遇到一些比较复杂的情况,因此复习一遍链接器的工作原理,在这里总结工作原理和常见的问题。

How a Linux 6.2.13 BUG stops Vivado from recognizing FPGA

中文版本

TLDR

In short, the commit introduced by Linux 6.2.13:

commit 0d30989fe9a176565d360376d4bc2ea1c61cbbac
Author: Liam R. Howlett <Liam.Howlett@oracle.com>
Date:   Fri Apr 14 14:59:19 2023 -0400

    mm/mmap: regression fix for unmapped_area{_topdown}

    commit 58c5d0d6d522112577c7eeb71d382ea642ed7be4 upstream.

    The maple tree limits the gap returned to a window that specifically fits
    what was asked.  This may not be optimal in the case of switching search
    directions or a gap that does not satisfy the requested space for other
    reasons.  Fix the search by retrying the operation and limiting the search
    window in the rare occasion that a conflict occurs.

    Link: https://lkml.kernel.org/r/20230414185919.4175572-1-Liam.Howlett@oracle.com
    Fixes: 3499a13168da ("mm/mmap: use maple tree for unmapped_area{_topdown}")
    Signed-off-by: Liam R. Howlett <Liam.Howlett@oracle.com>
    Reported-by: Rick Edgecombe <rick.p.edgecombe@intel.com>
    Cc: <stable@vger.kernel.org>
    Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
    Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>

While fixing a BUG, a new BUG is introduced, causing MAP_32BIT to fail to work sometimes, and Xilinx's Digilent driver uses this parameter, causing mmap to fail and unable to recognize the FPGA.

The new BUG has been fixed in [PATCH v2] maple_tree: Make maple state reusable after mas_empty_area().

Linux 6.2.13 引入的 BUG 导致 Vivado 无法识别 FPGA

English version

TLDR

简单来说,Linux 6.2.13 引入的 commit:

commit 0d30989fe9a176565d360376d4bc2ea1c61cbbac
Author: Liam R. Howlett <Liam.Howlett@oracle.com>
Date:   Fri Apr 14 14:59:19 2023 -0400

    mm/mmap: regression fix for unmapped_area{_topdown}

    commit 58c5d0d6d522112577c7eeb71d382ea642ed7be4 upstream.

    The maple tree limits the gap returned to a window that specifically fits
    what was asked.  This may not be optimal in the case of switching search
    directions or a gap that does not satisfy the requested space for other
    reasons.  Fix the search by retrying the operation and limiting the search
    window in the rare occasion that a conflict occurs.

    Link: https://lkml.kernel.org/r/20230414185919.4175572-1-Liam.Howlett@oracle.com
    Fixes: 3499a13168da ("mm/mmap: use maple tree for unmapped_area{_topdown}")
    Signed-off-by: Liam R. Howlett <Liam.Howlett@oracle.com>
    Reported-by: Rick Edgecombe <rick.p.edgecombe@intel.com>
    Cc: <stable@vger.kernel.org>
    Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
    Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>

修复了 BUG 的同时,引入了新的 BUG,导致 MAP_32BIT 有时无法工作,而 Xilinx 的 Digilent 下载器代码使用了这个参数,导致 mmap 失败,无法识别 FPGA。

新 BUG 在 [PATCH v2] maple_tree: Make maple state reusable after mas_empty_area() 中被修复。

用 Nix 编译 Rust 项目

背景

Rust 项目一般是用 Cargo 管理,但是它的缺点是每个项目都要重新编译一次所有依赖,硬盘空间占用较大,不能跨项目共享编译缓存。调研了一下,有若干基于 Nix 的 Rust 构建工具:

  • cargo2nix: https://github.com/cargo2nix/cargo2nix
  • carnix: 不再更新
  • crane: https://github.com/ipetkov/crane
  • crate2nix: https://github.com/kolloch/crate2nix
  • naersk: https://github.com/nix-community/naersk
  • nocargo: https://github.com/oxalica/nocargo

下面我分别来尝试一下这几个工具的使用。

invalid date 报错与时区的关系

背景

最近在验题的时候,@HarryChen 发现了一个现象:

$ date -d "1919-04-13"
date: invalid date ‘1919-04-13’
$ TZ=UTC date -d "1919-04-13"
Sun Apr 13 00:00:00 UTC 1919

也就是说,这个现象与时区有关,那么为啥 1919-04-13 是一个不合法的日期呢?

时区

实际上,对于某一个时区来说,有的时间是不存在的,最常见的就是夏令时。在 Timezone DB 里可以看到,恰好在 1919 年 4 月 13 日发生了一次 UTC+8 到 UTC+9 的变化,因此零点变成了一点,就变成了不合法的日期。

这个数据,实际上保存在 tzdata 中,可以用 zdump 工具查看:

$ tzdata -v Asia/Shanghai
Asia/Shanghai  Fri Dec 13 20:45:52 1901 UTC = Sat Dec 14 04:45:52 1901 CST isdst=0
Asia/Shanghai  Sat Dec 14 20:45:52 1901 UTC = Sun Dec 15 04:45:52 1901 CST isdst=0
Asia/Shanghai  Sat Apr 12 15:59:59 1919 UTC = Sat Apr 12 23:59:59 1919 CST isdst=0
Asia/Shanghai  Sat Apr 12 16:00:00 1919 UTC = Sun Apr 13 01:00:00 1919 CDT isdst=1
Asia/Shanghai  Tue Sep 30 14:59:59 1919 UTC = Tue Sep 30 23:59:59 1919 CDT isdst=1
Asia/Shanghai  Tue Sep 30 15:00:00 1919 UTC = Tue Sep 30 23:00:00 1919 CST isdst=0
Asia/Shanghai  Fri May 31 15:59:59 1940 UTC = Fri May 31 23:59:59 1940 CST isdst=0
Asia/Shanghai  Fri May 31 16:00:00 1940 UTC = Sat Jun  1 01:00:00 1940 CDT isdst=1
Asia/Shanghai  Sat Oct 12 14:59:59 1940 UTC = Sat Oct 12 23:59:59 1940 CDT isdst=1
Asia/Shanghai  Sat Oct 12 15:00:00 1940 UTC = Sat Oct 12 23:00:00 1940 CST isdst=0
Asia/Shanghai  Fri Mar 14 15:59:59 1941 UTC = Fri Mar 14 23:59:59 1941 CST isdst=0
Asia/Shanghai  Fri Mar 14 16:00:00 1941 UTC = Sat Mar 15 01:00:00 1941 CDT isdst=1
Asia/Shanghai  Sat Nov  1 14:59:59 1941 UTC = Sat Nov  1 23:59:59 1941 CDT isdst=1
Asia/Shanghai  Sat Nov  1 15:00:00 1941 UTC = Sat Nov  1 23:00:00 1941 CST isdst=0
Asia/Shanghai  Fri Jan 30 15:59:59 1942 UTC = Fri Jan 30 23:59:59 1942 CST isdst=0
Asia/Shanghai  Fri Jan 30 16:00:00 1942 UTC = Sat Jan 31 01:00:00 1942 CDT isdst=1
Asia/Shanghai  Sat Sep  1 14:59:59 1945 UTC = Sat Sep  1 23:59:59 1945 CDT isdst=1
Asia/Shanghai  Sat Sep  1 15:00:00 1945 UTC = Sat Sep  1 23:00:00 1945 CST isdst=0
Asia/Shanghai  Tue May 14 15:59:59 1946 UTC = Tue May 14 23:59:59 1946 CST isdst=0
Asia/Shanghai  Tue May 14 16:00:00 1946 UTC = Wed May 15 01:00:00 1946 CDT isdst=1
Asia/Shanghai  Mon Sep 30 14:59:59 1946 UTC = Mon Sep 30 23:59:59 1946 CDT isdst=1
Asia/Shanghai  Mon Sep 30 15:00:00 1946 UTC = Mon Sep 30 23:00:00 1946 CST isdst=0
Asia/Shanghai  Mon Apr 14 15:59:59 1947 UTC = Mon Apr 14 23:59:59 1947 CST isdst=0
Asia/Shanghai  Mon Apr 14 16:00:00 1947 UTC = Tue Apr 15 01:00:00 1947 CDT isdst=1
Asia/Shanghai  Fri Oct 31 14:59:59 1947 UTC = Fri Oct 31 23:59:59 1947 CDT isdst=1
Asia/Shanghai  Fri Oct 31 15:00:00 1947 UTC = Fri Oct 31 23:00:00 1947 CST isdst=0
Asia/Shanghai  Fri Apr 30 15:59:59 1948 UTC = Fri Apr 30 23:59:59 1948 CST isdst=0
Asia/Shanghai  Fri Apr 30 16:00:00 1948 UTC = Sat May  1 01:00:00 1948 CDT isdst=1
Asia/Shanghai  Thu Sep 30 14:59:59 1948 UTC = Thu Sep 30 23:59:59 1948 CDT isdst=1
Asia/Shanghai  Thu Sep 30 15:00:00 1948 UTC = Thu Sep 30 23:00:00 1948 CST isdst=0
Asia/Shanghai  Sat Apr 30 15:59:59 1949 UTC = Sat Apr 30 23:59:59 1949 CST isdst=0
Asia/Shanghai  Sat Apr 30 16:00:00 1949 UTC = Sun May  1 01:00:00 1949 CDT isdst=1
Asia/Shanghai  Fri May 27 14:59:59 1949 UTC = Fri May 27 23:59:59 1949 CDT isdst=1
Asia/Shanghai  Fri May 27 15:00:00 1949 UTC = Fri May 27 23:00:00 1949 CST isdst=0
Asia/Shanghai  Sat May  3 17:59:59 1986 UTC = Sun May  4 01:59:59 1986 CST isdst=0
Asia/Shanghai  Sat May  3 18:00:00 1986 UTC = Sun May  4 03:00:00 1986 CDT isdst=1
Asia/Shanghai  Sat Sep 13 16:59:59 1986 UTC = Sun Sep 14 01:59:59 1986 CDT isdst=1
Asia/Shanghai  Sat Sep 13 17:00:00 1986 UTC = Sun Sep 14 01:00:00 1986 CST isdst=0
Asia/Shanghai  Sat Apr 11 17:59:59 1987 UTC = Sun Apr 12 01:59:59 1987 CST isdst=0
Asia/Shanghai  Sat Apr 11 18:00:00 1987 UTC = Sun Apr 12 03:00:00 1987 CDT isdst=1
Asia/Shanghai  Sat Sep 12 16:59:59 1987 UTC = Sun Sep 13 01:59:59 1987 CDT isdst=1
Asia/Shanghai  Sat Sep 12 17:00:00 1987 UTC = Sun Sep 13 01:00:00 1987 CST isdst=0
Asia/Shanghai  Sat Apr 16 17:59:59 1988 UTC = Sun Apr 17 01:59:59 1988 CST isdst=0
Asia/Shanghai  Sat Apr 16 18:00:00 1988 UTC = Sun Apr 17 03:00:00 1988 CDT isdst=1
Asia/Shanghai  Sat Sep 10 16:59:59 1988 UTC = Sun Sep 11 01:59:59 1988 CDT isdst=1
Asia/Shanghai  Sat Sep 10 17:00:00 1988 UTC = Sun Sep 11 01:00:00 1988 CST isdst=0
Asia/Shanghai  Sat Apr 15 17:59:59 1989 UTC = Sun Apr 16 01:59:59 1989 CST isdst=0
Asia/Shanghai  Sat Apr 15 18:00:00 1989 UTC = Sun Apr 16 03:00:00 1989 CDT isdst=1
Asia/Shanghai  Sat Sep 16 16:59:59 1989 UTC = Sun Sep 17 01:59:59 1989 CDT isdst=1
Asia/Shanghai  Sat Sep 16 17:00:00 1989 UTC = Sun Sep 17 01:00:00 1989 CST isdst=0
Asia/Shanghai  Sat Apr 14 17:59:59 1990 UTC = Sun Apr 15 01:59:59 1990 CST isdst=0
Asia/Shanghai  Sat Apr 14 18:00:00 1990 UTC = Sun Apr 15 03:00:00 1990 CDT isdst=1
Asia/Shanghai  Sat Sep 15 16:59:59 1990 UTC = Sun Sep 16 01:59:59 1990 CDT isdst=1
Asia/Shanghai  Sat Sep 15 17:00:00 1990 UTC = Sun Sep 16 01:00:00 1990 CST isdst=0
Asia/Shanghai  Sat Apr 13 17:59:59 1991 UTC = Sun Apr 14 01:59:59 1991 CST isdst=0
Asia/Shanghai  Sat Apr 13 18:00:00 1991 UTC = Sun Apr 14 03:00:00 1991 CDT isdst=1
Asia/Shanghai  Sat Sep 14 16:59:59 1991 UTC = Sun Sep 15 01:59:59 1991 CDT isdst=1
Asia/Shanghai  Sat Sep 14 17:00:00 1991 UTC = Sun Sep 15 01:00:00 1991 CST isdst=0
Asia/Shanghai  Mon Jan 18 03:14:07 2038 UTC = Mon Jan 18 11:14:07 2038 CST isdst=0
Asia/Shanghai  Tue Jan 19 03:14:07 2038 UTC = Tue Jan 19 11:14:07 2038 CST isdst=0

可以看到,它列出来了历史上 Asia/Shanghai 时区的变化历史。具体的历史,可以查看 中国时区

此外,历史上,从儒略历到格里高利历的演变过程,也出现了一段“不存在”的日期,如 Setting October 14 ,1582 fails in java.sql.Date

Archive 中 COMMON 符号的链接问题

背景

最近看到一个 issue: irssi 1.4.1 fails to build on darwin arm64,它的现象是,链接的时候会报错:

Undefined symbols for architecture arm64:
  "_current_theme", referenced from:
      _format_get_text_theme in libfe_common_core.a(formats.c.o)
      _format_get_text in libfe_common_core.a(formats.c.o)
      _strip_codes in libfe_common_core.a(formats.c.o)
      _format_send_as_gui_flags in libfe_common_core.a(formats.c.o)
      _window_print_daychange in libfe_common_core.a(fe-windows.c.o)
      _printformat_module_dest_charargs in libfe_common_core.a(printtext.c.o)
      _printformat_module_gui_args in libfe_common_core.a(printtext.c.o)
      ...
  "_default_formats", referenced from:
      _format_find_tag in libfe_common_core.a(formats.c.o)
      _format_get_text_theme_args in libfe_common_core.a(formats.c.o)
      _printformat_module_dest_args in libfe_common_core.a(printtext.c.o)
      _printformat_module_gui_args in libfe_common_core.a(printtext.c.o)
ld: symbol(s) not found for architecture arm64

代码 themes.c 定义了这两个全局变量:

THEME_REC *current_theme;
GHashTable *default_formats;

并且 themes.c 编译出来的 themes.c.o 也在 archive 文件中:

$ ar t src/fe-common/core/libfe_common_core.a | grep themes.c.o
themes.c.o

并且 themes.c.o 也定义了这两个符号:

$ objdump -t src/fe-common/core/libfe_common_core.a.p/themes.c.o | grep COM
0000000000000008         01 COM    00 0300 _current_theme
0000000000000008         01 COM    00 0300 _default_formats
0000000000000008         01 COM    00 0300 _themes

那么,问题在哪呢?看起来,链接的时候提供了 libfe_common_core.a 的参数,并且 .a 里面也有 themes.c.o,我们要找的符号也有定义,那么为什么会出现 Undefined symbols 的问题呢?

答案出在 COMMON 符号上。

COMMON 符号

COMMON 符号的原因和原理,详细可以见 MaskRay 的博客 All about COMMON symbols,里面从链接器的角度很详细地讲述了这个问题。

简单来说,COMMON 符号的引入是为了和 Fortran 进行互操作。它在 C 中对应了没有初始化语句的全局变量。实际上到最后,还是会保存到 .bss 段中,默认清零。所以:

int common_symbol;
int not_common_symbol = 0;

两个语句最终结果是类似的,只不过第一个是 COMMON Symbol,第二个就是普通的 GLOBAL Symbol。

这看起来和 Undefined symbols 错误还是没有关系。问题在哪?

Archive

静态库通常是以 Archive 的方式给出,后缀是 .a。它实际上是一堆 .o 打包的集合,外加一个索引,即单独保存一个表,保存了每个 .o 定义了哪些符号。这样的好处是找符号的时候,不用遍历 .o,而是直接在索引里面找相关的符号。

为了创建一个 Archive,Linux 上可以用 ar 命令:

ar cr libxxx.a a.o b.o c.o

其中 c 表示 create,r 表示插入(和覆盖)。

macOS 上要用 libtool -static 来创建 Archive:

libtool -static libxxx.a a.o b.o c.o

否则链接的时候会报错:

ld: warning: ignoring file libxxx.a, building for macOS-arm64 but attempting to link with file built for unknown-unsupported file format

然后用 ar t 命令就可以看 Archive 有哪些内容:

$ ar t libxxx.a
a.o
b.o
c.o

可以用 nm --print-armap 命令查看 Archive 的索引:

$ nm --print-armap libxxx.a
Archive index:
symbol1 in a.o
symbol2 in b.o
symbol3 in c.o

a.o:
0000000000000000 T symbol1

所以我们已经了解了 Archive 的情况:它是多个 .o 文件的集合,并且实现了索引。链接的时候,会通过索引来找 .o,而不是遍历所有的 .o 文件。

链接问题

那么,回到一开始的链接问题,既然我们已经确认了,.o 文件中定义了符号,并且这个 .o 也确实在 .a 文件中,那就只剩下最后一个可能了:索引里面没有这个符号。

nm --print-armap 命令尝试,发现上面的 _default_formats_current_theme 只在对应的 .o 中有定义,在 Archive index 部分是没有的。

网友 @ailin-nemui 指出了这个问题,并且提供了一个链接:OS X linker unable to find symbols from a C file which only contains variables。它讲了很重要的一点,是 macOS 的 ar/ranlib/libtool 版本默认情况下不会为 COMMON 符号创建索引。所以,解决方案也很明确了:

  1. 第一种 不要创建 COMMON 符号:添加编译选项 -fno-common,这个选项在比较新的编译器里都是默认了
  2. 第二种 为 COMMON 符号创建索引:用 libtool -static -c 命令,其中 -c 选项就是打开为 COMMON 符号创建索引
  3. 第三种 修改代码:给全局变量设置一个初始化值

这样,这个问题就得到了妥善的解决。

附录

下面是 macOS libtool manpage 中写的相关文档:

-c     Include common symbols as definitions with respect to the table of contents.  This is seldom the intended behavior for linking  from
          a library, as it forces the linking of a library member just because it uses an uninitialized global that is undefined at that point
          in the linking.  This option is included only because this was the original behavior of ranlib.  This option is not the default.

NVIDIA 驱动和 CUDA 安装速查

背景

最近在 Ubuntu 上配置 NVIDIA 驱动和 CUDA 环境的次数比较多,在此总结一下整个流程,作为教程供大家学习。

配置 NVIDIA APT 源

Ubuntu 源有自带的 NVIDIA 驱动版本,但这里我们要使用 NVIDIA 的 APT 源。首先,我们要访问 https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=20.04&target_type=deb_network,在网页中选择我们的系统,例如:

  1. Operating System: Linux
  2. Architecture: x86_64
  3. Distribution: Ubuntu
  4. Version: 20.04
  5. Installer Type: deb (network)

此时,下面就会显示一些命令,复制下来执行:

wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-keyring_1.0-1_all.deb
sudo dpkg -i cuda-keyring_1.0-1_all.deb
sudo apt-get update

最后一步的 sudo apt-get -y install cuda 可以不着急安装,我们在后面再来讨论 CUDA 版本的问题。

NVIDIA 驱动

配置好源以后,接下来,我们就要安装 NVIDIA 驱动了。首先,我们要选取一个 NVIDIA 版本,选择的标准如下:

  1. 驱动版本需要支持所使用的显卡
  2. 驱动版本需要支持所使用的 CUDA 版本

这些信息在网络上都可以查到,也可以参考 NVIDIA 驱动和 CUDA 版本信息速查

假如我们已经选择了要安装 470.129.06 版本,那么,我们接下来要确认一下 NVIDIA 的 APT 源的版本名称:

sudo apt show -a nvidia-driver-470

在输出的结果中搜索 470.129.06,找到 Version: 470.129.06-0ubuntu1,下面写了 APT-Sources: https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64 Packages,这就说明这个版本是从 NVIDIA 的 APT 源来的。

所以,我们要用 470.129.06-0ubuntu1,而不是 470.129.06-0ubuntu0.20.04.1,后者是 Ubuntu 源自带的,我们要用前者。

接下来,指定版本安装驱动:

sudo apt install nvidia-driver-470=470.129.06-0ubuntu1

如果系统里已经安装了其他版本的 nvidia 驱动,可能会出现冲突。这时候,只需要把冲突的包也写在要安装的包里即可,例如:

sudo apt install nvidia-utils-470=470.129.06-0ubuntu1 cuda-drivers=470.129.06-1 cuda-drivers-470=470.129.06-1 nvidia-driver-470=470.129.06-0ubuntu1 libnvidia-gl-470=470.129.06-0ubuntu1 libnvidia-compute-470=470.129.06-0ubuntu1 libnvidia-decode-470=470.129.06-0ubuntu1 libnvidia-encode-470=470.129.06-0ubuntu1 libnvidia-ifr1-470=470.129.06-0ubuntu1 libnvidia-fbc1-470=470.129.06-0ubuntu1 libnvidia-common-470=470.129.06-0ubuntu1 nvidia-kernel-source-470=470.129.06-0ubuntu1 nvidia-dkms-470=470.129.06-0ubuntu1 nvidia-kernel-common-470=470.129.06-0ubuntu1 libnvidia-extra-470=470.129.06-0ubuntu1 nvidia-compute-utils-470=470.129.06-0ubuntu1 xserver-xorg-video-nvidia-470=470.129.06-0ubuntu1 libnvidia-cfg1-470=470.129.06-0ubuntu1 nvidia-settings=470.129.06-0ubuntu1 libxnvctrl0=470.129.06-0ubuntu1 nvidia-modprobe=470.129.06-0ubuntu1

最终,我们要保证,系统里面所有 nvidia 驱动相关的包都是同一个版本:

$ sudo apt list --installed | grep nvidia
libnvidia-cfg1-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed,automatic]
libnvidia-common-470/unknown,now 470.129.06-0ubuntu1 all [installed,automatic]
libnvidia-compute-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed]
libnvidia-decode-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed]
libnvidia-encode-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed]
libnvidia-extra-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed,automatic]
libnvidia-fbc1-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed]
libnvidia-gl-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed]
libnvidia-ifr1-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed]
nvidia-compute-utils-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed,automatic]
nvidia-dkms-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed]
nvidia-driver-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed]
nvidia-fabricmanager-470/unknown,now 470.129.06-1 amd64 [installed,automatic]
nvidia-kernel-common-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed,automatic]
nvidia-kernel-source-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed,automatic]
nvidia-modprobe/unknown,now 470.129.06-0ubuntu1 amd64 [installed,upgradable to: 515.48.07-0ubuntu1]
nvidia-settings/unknown,now 470.129.06-0ubuntu1 amd64 [installed,upgradable to: 515.48.07-0ubuntu1]
nvidia-utils-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed,automatic]
xserver-xorg-video-nvidia-470/unknown,now 470.129.06-0ubuntu1 amd64 [installed,automatic]

接下来,为了防止 apt 升级的时候顺手破坏了一致的版本,我们要把包固定在一个版本里:

sudo apt-mark hold cuda-drivers nvidia-modprobe nvidia-settings libxnvctrl0

如果有其他 nvidia 包说要自动升级,也可以类似地固定住。

CUDA

CUDA 实际上是绿色软件,把整个目录放在任意一个目录,都可以使用。

安装 CUDA 的方式有很多,我们可以用 APT 安装全局的,也可以用 Spack 或者 Anaconda 安装到本地目录。实际上这些安装过程都是把同样的文件复制到不同的地方而已。

如果要安装全局的话,还是推荐用 NVIDIA 的 APT 源,以安装 CUDA 11.1 为例:

sudo apt install cuda-11-1

那么 CUDA 就会安装到 /usr/local/cuda-11.1 目录下。如果想要用 nvcc,我们可以手动把它加到 PATH 环境变量中。

CUDA 是可以多版本共存的,比如你可以把 CUDA 11.1 到 CUDA 11.7 一口气都装了。不过注意,CUDA 对 NVIDIA 驱动有版本要求,所以有一些可能会不满足 APT 的版本要求;同时,CUDA 对编译器版本有要求,所以如果系统还是 Ubuntu 16.04 或者 18.04,赶紧升级吧。

NVIDIA Container Toolkit

安装方法:https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#linux-distributions

命令:

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
  && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list \
  && \
    sudo apt-get update

切换 ConnectX-4 为以太网模式

背景

最近在给机房配置网络,遇到一个需求,就是想要把 ConnectX-4 当成以太网卡用,它既支持 Infiniband,又支持 Ethernet,只不过默认是 Infiniband 模式,所以需要用 mlxconfig 工具来做这个切换。

切换方法

Using mlxconfig 文档中,写了如何切换网卡为 Infiniband 模式:

$ mlxconfig -d /dev/mst/mt4103_pci_cr0 set LINK_TYPE_P1=1 LINK_TYPE_P2=1

Device #1:
----------
Device type:   ConnectX3Pro
PCI device:    /dev/mst/mt4103_pci_cr0
Configurations:        Next Boot        New
  LINK_TYPE_P1         ETH(2)           IB(1)
  LINK_TYPE_P2         ETH(2)           IB(1)

Apply new Configuration? ? (y/n) [n] : y
Applying... Done!
-I- Please reboot machine to load new configurations.

那么,我们只需要反其道而行之,设置模式为 ETH(2) 即可。

MST 安装

要使用 mlxconfig,就需要安装 MFT(Mellanox Firmware Tools)。我们用的是 Debian bookworm,于是要下载 DEB:

wget https://www.mellanox.com/downloads/MFT/mft-4.20.1-14-x86_64-deb.tgz
unar mft-4.20.1-14-x86_64-deb.tgz
cd mft-4.20.1-14-x86_64-deb

UPDATE 2022-10-28: 现在最新版本 mft-4.21.0-99 已经修复了下面出现的编译问题。

wget https://www.mellanox.com/downloads/MFT/mft-4.21.0-99-x86_64-deb.tgz
unar mft-4.21.0-99-x86_64-deb.tgz
cd mft-4.21.0-99-x86_64-deb

尝试用 sudo ./install.sh 安装,发现 dkms 报错。查看日志,发现是因为内核过高(5.18),有函数修改了用法,即要把 pci_unmap_single 的调用改为 dma_unmap_single,并且修改第一个参数,如 linux commit a2e759612e5ff3858856fe97be5245eecb84e29b 指出的那样:

-           pci_unmap_single(dev->pci_dev, dev->dma_props[i].dma_map, DMA_MBOX_SIZE, DMA_BIDIRECTIONAL);
+           dma_unmap_single(&dev->pci_dev->dev, dev->dma_props[i].dma_map, DMA_MBOX_SIZE, DMA_BIDIRECTIONAL);

修改完以后,手动 sudo dkms install kernel-mft-dkms/4.20.1,发现就编译成功了。再手动安装一下 mft 并启动服务:

$ sudo dpkg -i DEBS/mft_4.20.1-14_amd64.deb
$ sudo mst start
Starting MST (Mellanox Software Tools) driver set
Loading MST PCI module - Success
[warn] mst_pciconf is already loaded, skipping
Create devices
Unloading MST PCI module (unused) - Success
$ sudo mst status
MST modules:
------------
    MST PCI module is not loaded
    MST PCI configuration module loaded

MST devices:
------------
/dev/mst/mtxxxx_pciconf0         - PCI configuration cycles access.
                                   domain:bus:dev.fn=0000:xx:xx.0 addr.reg=yy data.reg=zz cr_bar.gw_offset=-1
                                   Chip revision is: 00

既然已经安装好了,最后执行 mlxconfig 即可切换为以太网:

$ sudo mlxconfig -d /dev/mst/mtxxxx_pciconf0 set LINK_TYPE_P1=2 LINK_TYPE_P2=2

Device #1:
----------

Device type:    ConnectX4
Name:           REDACTED
Description:    ConnectX-4 VPI adapter card; FDR IB (56Gb/s) and 40GbE; dual-port QSFP28; PCIe3.0 x8; ROHS R6
Device:         /dev/mst/mtxxxx_pciconf0

Configurations:                              Next Boot       New
         LINK_TYPE_P1                        IB(1)           ETH(2)
         LINK_TYPE_P2                        IB(1)           ETH(2)

 Apply new Configuration? (y/n) [n] : y
Applying... Done!
-I- Please reboot machine to load new configurations.

显示各个配置可能的选项和内容:sudo mlxconfig -d /dev/mst/mtxxxx_pciconf0 show_confs

整个安装流程在仓库 https://github.com/jiegec/mft-debian-bookworm 中用脚本实现。

VMware ESXi

如果要在 ESXi 上把网卡改成以太网模式,可以参考下面的文档:

  • https://docs.nvidia.com/networking/pages/releaseview.action?pageId=15049813
  • https://docs.nvidia.com/networking/plugins/servlet/mobile?contentId=15051769#content/view/15051769

命令(ESXi 7.0U3):

scp *.vib root@esxi:/some/path
esxcli software vib install -v /some/path/MEL_bootbank_mft_4.21.0.703-0.vib
esxcli software vib install -v /some/path/MEL_bootbank_nmst_4.21.0.703-1OEM.703.0.0.18434556.vib
reboot
/opt/mellanox/bin/mst start
/opt/mellanox/bin/mst status -vv
/opt/mellanox/bin/mlxfwmanager --query
/opt/mellanox/bin/mlxconfig -d mt4115_pciconf0 set LINK_TYPE_P1=2 LINK_TYPE_P2=2
reboot

然后就可以看到网卡了。

rsyslog 收集远程日志

背景

最近在运维的时候发现网络设备(如交换机)有一个远程发送日志的功能,即可以通过 syslog udp 协议发送日志到指定的服务器。为此,可以在服务器上运行 rsyslog 并收集日志。

rsyslog 配置

默认的 rsyslog 配置是收集系统本地的配置,因此我们需要编写一个 rsyslog 配置,用于收集远程的日志。

首先复制 /etc/rsyslog.conf/etc/rsyslog-remote.conf,然后修改:

  1. 注释掉 imuxsockimklog 相关的 module 加载
  2. 去掉 imudpimtcp 相关的注释,这样就会监听在相应的端口上
  3. 修改 $WorkDirectory,例如 $WorkDirectory /var/spool/rsyslog-remote,防止与已有的 rsyslog 冲突
  4. 注释 $IncludeConfig,防止引入了不必要的配置
  5. 注释所有已有的 RULES 下面的配置
  6. 添加如下配置:
$template FromIp,"/var/log/rsyslog-remote/%FROMHOST-IP%.log"
*.* ?FromIp

这样,就会按照来源的 IP 地址进行分类,然后都写入到 /var/log/rsyslog-remote/x.x.x.x.log 文件里。

systemd service

最后,写一个 systemd service 让它自动启动:

[Unit]
ConditionPathExists=/etc/rsyslog-remote.conf
Description=Remote Syslog Service

[Service]
Type=simple
PIDFile=/var/run/rsyslogd-remote.pid
ExecStart=/usr/sbin/rsyslogd -n -f /etc/rsyslog-remote.conf -i /var/run/rsyslogd-remote.pid
ExecReload=/bin/kill -HUP $MAINPID

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now rsyslog-remote

这样就实现了远程日志的收集。

logrotate 设置

为了防止日志太多,还需要配置 logrotate。

复制 /etc/logrotate.d/rsyslog/etc/logrotate.d/rsyslog-remote,然后修改开头为 /var/log/rsyslog-remote/*.log 即可,路径和上面对应。注意脚本 /usr/lib/rsyslog/rsyslog-remote 也需要复制一份,然后修改一下 systemd service 名字。

参考文档

Nix Cookbook

背景

最近在尝试 NixOS 和在 macOS 上跑 Nix,下面记录一些我在使用过程中遇到的一些小问题和解决思路。

NixOS

全局配置

NixOS 的全局配置路径:/etc/nixos/configuration.nix/etc/nixos/hardware-configuration.nix

应用更新后的全局配置:

nixos-rebuild switch
# or
nixos-rebuild switch --upgrade

应用 Flakes 配置文件并显示变化:

#!/usr/bin/env python3
import os

user = os.getenv("USER")
home = f"/nix/var/nix/profiles/"
old = home + os.readlink(f"{home}system")
os.system("sudo nixos-rebuild switch --flake .")
new = home + os.readlink(f"{home}system")
os.system(f"nix store diff-closures {old} {new}")

更新大版本

如果要更新 NixOS 21.11 到 22.05:

nix-channel --list
nix-channel --add https://nixos.org/channels/nixos-22.05 nixos
nixos-rebuild switch --upgrade

可以考虑改或者不改 /etc/nixos/configuration.nix 中的 system.stateVersion

常用配置

常用的 NixOS 配置:

# Enable XFCE
services.xserver.enable = true;
services.xserver.desktopManager.xfce.enable = true;

# System wide packages
environment.systemPackages = with pkgs; [
  xxx
];

# Fish shell
programs.fish.enable = true;
users.users.xxx = {
  shell = pkgs.fish;
}

# Command not found
programs.command-not-found.enable = true;

# Steam gaming
nixpkgs.config.allowUnfree = true;
programs.steam.enable = true;

# NOPASSWD for sudo
security.sudo.wheelNeedsPassword = false;

# QEMU guest
services.qemuGuest.enable = true;
services.spice-vdagentd.enable = true;

# XRDP
services.xrdp.enable = true;
services.xrdp.defaultWindowManager = "xfce4-session";
services.xrdp.openFirewall = true;

# OpenSSH server
services.openssh.enable = true;

# Udev rules for Altera USB Blaster
services.udev.packages = with pkgs; [
  usb-blaster-udev-rules
];

VSCode Remote

VSCode Remote 会在远程的机器上运行一个预编译的 nodejs,运行的时候会因为路径问题无法执行。

解决方法在 NixOS Wiki 上有,具体来说,首先,需要安装 nodejs

environment.systemPackages = with pkgs; [
  nodejs-16_x # vscode remote
];

然后,用软链接来覆盖 nodejs:

cd ~/.vscode-server/bin/HASH
ln -sf /run/current-system/sw/bin/node

这样就可以正常使用 VSCode Remote 了。

Home Manager

Home Manager 描述用户默认看到的程序,而 NixOS 的配置是所有用户的。

配置文件

配置文件:~/.config/nixpkgs/home.nix

应用配置文件:

home-manager switch

应用 Flakes 配置文件并显示变化:

#!/usr/bin/env python3
import os

user = os.getenv("USER")
home = f"/nix/var/nix/profiles/per-user/{user}/"
old = home + os.readlink(f"{home}profile")
os.system("home-manager switch --flake .")
new = home + os.readlink(f"{home}profile")
os.system(f"nix store diff-closures {old} {new}")

常用配置

常用的 Home Manager 配置:

# Allow unfree
nixpkgs.config.allowUnfree = true;
nixpkgs.config.allowUnfreePredicate = (pkg: true);

# User wide packages
home.packages = with pkgs; [
  xxx
];

生成 Nix 配置 ~/.config/nix/nix.conf

# Enable flakes & setup TUNA mirror
nix.package = pkgs.nix;
nix.settings = {
  experimental-features = [ "nix-command" "flakes" ];
  substituters = [ "https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store" "https://cache.nixos.org/"];
};

Shell 环境变量和 PATH:

home.sessionVariables = {
  A = "B";
};
home.sessionPath = [
  "$HOME/.local/bin"
];

离线 Home Manager 文档(用 home-manager-help 命令打开):

manual.html.enable = true;

覆盖依赖版本

设置 JVM 程序依赖的 JDK 版本:

# Maven with java 11
home.packages = with pkgs; [
  (maven.override { jdk = jdk11; })
];

# Many packages with the same JDK
home.packages = 
  let java = pkgs.jdk11; in
  with pkgs; [
    (maven.override { jdk = java; })
    (sbt.override { jre = java; })
  ];

具体的参数命名要看 nixpkgs 上对应的包的开头。

配置 direnv

direnv 是一个 shell 插件,它的用途是进入目录的时候,会根据 .envrc 来执行命令,比如自动进入 nix-shell 等。配置:

programs.direnv.enable = true;

然后在工程路径下,编写 .envrc

use_nix

那么,在 shell 进入目录的时候,就会自动获得 nix-shell 的环境变量。

配置 fish

可以在 home manager 配置中编写 fish 配置,这样它会自动生成 ~/.config/fish/config.fish 文件:

programs.fish.enable = true;
programs.fish.shellAliases = {
  a = "b";
};
programs.fish.shellInit = ''
  # Rust
  set -x PATH ~/.cargo/bin $PATH
'';

配置 git

同理,也可以在 home manager 中配置 git:

programs.git.enable = true;
programs.git.lfs.enable = true;
programs.git.userName = "Someone";
programs.git.userEmail = "mail@example.com";
programs.git.extraConfig = {
  core = {
    quotepath = false;
  };
  pull = {
    rebase = false;
  };
};
programs.git.ignores = [
  ".DS_Store"
];

生成的 git 配置在 ~/.config/git/config~/.config/git/ignore

Flakes

Flakes 可以用来把多个系统的 nix 配置写在一个项目中。例如:

{
  description = "Nix configuration";

  inputs = {
    home-manager.url = "github:nix-community/home-manager/release-22.05";
    nixpkgs.url = "github:nixos/nixpkgs/nixos-22.05";
  };

  outputs = { self, nixpkgs, home-manager }:
    {
      nixosConfigurations.xxxx = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./nixos/xxxx/configuration.nix
          home-manager.nixosModules.home-manager
          ./nixos/xxxx/home.nix
        ];
      };
      homeConfigurations.yyyy = home-manager.lib.homeManagerConfiguration {
        configuration = import ./home-manager/yyyy/home.nix;
        system = "aarch64-darwin";
        homeDirectory = "/Users/yyyy";
        username = "yyyy";
        stateVersion = "22.05";
      };
    };
}

然后,要应用上面的配置,运行:

# NixOS
nixos-rebuild switch --flake .
# Home manager
home-manager switch --flake .

这样就可以把若干个系统上的 nix 配置管理在一个仓库中了。

实用工具

nixpkgs-fmt

nixpkgs-fmt 用来格式化 Nix 代码。

search.nixos.org

search.nixos.org 可以搜索 nixpkgs 上的各种包,也可以看到不同平台支持情况。缺点是看不出是否 unfree 和 broken,并且一些 darwin os-specific 的包不会显示。

nix-tree

显示各个 nix derivation 的硬盘占用和依赖关系。

打包

可以很容易地编写 default.nix 来给自己的项目打包。

CMake

对于一个简单的 cmake 程序,可以按照如下的格式编写 default.nix

with import <nixpkgs> {};

stdenv.mkDerivation {
  name = "xyz";
  version = "1.0";

  src = ./.;

  nativeBuildInputs = [
    cmake
  ];

  buildInputs = [
    xxx
    yyy
  ];
}

可以用 nix-build 命令来构建,生成结果会在当前目录下创建一个 result 的软链接,里面就是安装目录。

由于 nix-build 的时候也会创建 build 目录,为了防止冲突,建议开发的时候用其他的名字。

Qt

对于 Qt 项目来说,由于有不同的 Qt 大版本,所以实现的时候稍微复杂一些,要拆成两个文件,首先是 default.nix

with import <nixpkgs> {};

libsForQt5.callPackage ./xxx.nix { }

这里就表示用 qt5 来编译,那么编写 xxx.nix 的时候,传入的 qtbase 等库就是 qt5 的版本:

{ stdenv, qtbase, wrapQtAppsHook, cmake }:

stdenv.mkDerivation {
  name = "xxx";
  version = "1.0";

  src = ./.;

  nativeBuildInputs = [
    cmake
    wrapQtAppsHook # must-have for qt apps
  ];

  buildInputs = [
    qtbase
  ];
}

实际测试中发现,运行的程序可能会报告 Could not initialize GLX 的错误,这个方法可以通过 wrapProgram 添加环境变量解决:

  # https://github.com/NixOS/nixpkgs/issues/66755#issuecomment-657305962
  # Fix "Could not initialize GLX" error
  postInstall = ''
    wrapProgram "$out/bin/xxx" --set QT_XCB_GL_INTEGRATION none
  '';

开发环境

除了打包以外,通常还会在 shell.nix 中定义开发环境需要的包:

{ pkgs ? import <nixpkgs> {}
}:

pkgs.mkShell {
  buildInputs = with pkgs; [
    cmake
  ];
}

然后可以用 nix-shell 来进入开发环境。如果不希望外面的环境变量传递进去,可以用 nix-shell --pure

搜索

按名字搜索一个包:

nix search nixpkgs xxx
nix-env -qaP yyy

Nixpkgs

可以从 TUNA 镜像上先 clone 一份到本地,然后再添加 github 上游作为 remote。

从本地 nixpkgs 安装:

nix-env -f $PWD -iA xxx

从本地 nixpkgs 编译:

nix-build $PWD -A xxx

从本地 nixpkgs 开一个 shell:

nix-shell -I nixpkgs=$PWD -p xxx

Nixpkgs 的分支

Nixpkgs 开发分支主要有三个:

  1. master
  2. staging-next
  3. staging

发 PR 的时候,如果需要重新编译的包比较多,就要往 staging 提交;比较少,就往 staging-next 提交。

CI 会自动把 master 合并到 staging-next,也会把 staging-next 合并到 staging。这样 master 上的改动也会同步到 staging 上。

维护者会定义把 staging 手动合并到 staging-next,然后手动合并 staging-next 到 staging。这个的周期一般是一周多,可以在 pr 里搜索 staging-next。

Hydra 会编译 master 分支和 staging-next 分支上的包,不会编译 staging 分支上的包。同理,binary cache 上前两个分支上有的,而 staging 上没有的。

参考:https://nixos.org/manual/nixpkgs/stable/#submitting-changes-commit-policy

提交贡献

注意事项:

  1. 升级一些比较老的写法,例如 mkDerivation -> stdenv.mkDerivation,Qt 的 hook
  2. 引入 patch 的时候,建议先向上游提 PR,如果合并了,就直接用上游的 commit;如果没有合并,退而求其次可以用 pr 的 patch;如果没有提 PR 的渠道,或者上游的 commit 无法应用到当前的版本,或者这个 patch 没有普适性,再写本地的 patch;注释里要写打 patch 的原因和相关的 issue 链接,什么时候不再需要这个 patch,并且起个名字
  3. 不知道 SHA256 的时候,可以注释掉或者随便写一个,这样 nix build 的时候会重新下载,然后把正确的显示出来
  4. 对于有命令的包,可以添加 testVersion 测试
  5. 长时间没有 review 的 pr,可以在 discourse 上回复帖子。
  6. 更新之前,可以搜索一下,有没有相关的 issue 或者 pr;如果有 issue,新建 pr 的时候要提一下

一些常见的问题:

  1. 编译器打开 -fno-common 后,可能会导致一些链接问题
  2. Darwin 上的 clang 没有打开 LTO,也没有打开 Universal 支持
  3. AArch64 Darwin 上的 gfortran 的 stack protector 不工作,需要把 hardening 关掉
  4. 当编译报错是 -Werror 导致的时候,按照 warning 类型在 NIX_CFLAGS_COMPILE 中添加 -Wno-error=warning-type
  5. configure 版本较老,需要引入 autoreconfHook

阅读文档:https://github.com/NixOS/nixpkgs/blob/master/doc/contributing/quick-start.chapter.mdhttps://github.com/NixOS/nixpkgs/blob/master/doc/contributing/coding-conventions.chapter.md

VSCode

可以安装 https://github.com/nix-community/vscode-nix-ide/ 插件,配合 rnix-lsp 来使用。

杂项

可以用 nix copy 命令在不同机器的 store 之间复制文件,见 nix copy - copy paths between Nix stores