Published on

新编译器与旧 conda 环境

Authors
  • Name

python 的环境管理真是狗屎不如


TL;DR

解决方法是令编译 pip 包时所用的编译器与当前环境下的 Python 所用的编译器具有统一版本。

  • Python 所用编译器版本可直接在 REPL 中查看,如:
      python3 -u
    Python 3.8.12 (default, Oct 12 2021, 13:49:34)
    [GCC 7.5.0] :: Anaconda, Inc. on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>>
    
    此处的 Python 是用 GCC 7.5.0 编译的。
  • CCCXX 指定要使用的编译器版本,如:
    CC=/usr/bin/gcc-7 CXX=/usr/bin/g++-7 python3 -u setup.py install
    

问题

一个月前需要用一个来自某学术代码仓库的 CUDA 轮子,这个轮子写得还是不错的,但是需要用 setup.py 安装进用 conda 开的虚拟环境里。可能因为系统里装的 gcc 版本太新(GCC 11),在 python 脚本中使用这个包时会在 import 那一行报错 undefined symbol:

ImportError: /path/to/libxxxx.so: undefined symbol: _ZNSt15__exception_ptr13exception_ptr9_M_addrefEv

一种粗暴的解决办法

第一次需要安装这个包时也遇到了这样的报错,当时因为在使用的另一份代码有 gcc-9 的依赖,系统里有安装 gcc-9,所以直接用了这个旧版本的编译器安装:

$ CC=gcc-9 CXX=gcc-9 python3 setup.py install

安装后再去 import 就没问题了。

从头开始

为什么要从头开始呢,因为最近重装了一下系统[1],所有之前知道为啥装的和不知道为啥装的包都被清洗一空,这之中当然包括那个 gcc-9。这次希望不要再装一次 gcc-9 了,因为装一次这玩意需要 CPU 的 16 个核心共同沸腾一下午(下次一定保存好打好的包🥲),我觉得不值得这么折腾[2]。另外因为记得 anaconda 不止提供 python 环境管理,以前也从 conda 安装过一些动态库之类的东西,所以猜想也可以在 conda 自己的环境里完成这个包的编译安装。

编译

经过简单的搜索[3],需要在 conda 环境里装 gcc_linux-64。安装后会在 conda 环境根目录的 /bin 下安装若干可执行文件,其中包括编译器x86_64-conda-linux-gnu-gcc

$ "$CONDA_PREFIX/bin/x86_64-conda-linux-gnu-gcc" --version
x86_64-conda-linux-gnu-gcc (crosstool-NG 1.24.0.133_b0863d8_dirty) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

可以看到这里的 gcc 大版本号也是 9[4]so far so good!我们给 setup.py 指定这个编译器来编译一次:

$ CC=x86_64-conda-linux-gnu-gcc CXX=x86_64-conda-linux-gnu-gcc \
    python3 setup.py install

这里遇到个报错说找不到 cc1plus。再次经过简单的搜索[5][6],发现需要另外在 conda 环境里安装 gxx_linux-64。安装后再次用上面方法指定的编译器运行 setup.pyninja 就开始编译了。我要装的包的源码里共有 14 个文件需要编译,ninja 令人满意地从 [1/14] 一直跑到了[14/14]

链接

我的双手已经离开了键盘,窗外的阳光照在我的后背上,我以为问题已经解决了。

但是世上哪有这么好的事情。

ninja 停止工作了,它告诉我:

.../compiler_compat/ld: cannot find /usr/lib64/libpthread_nonshared.a

Hmm,链接错误。于是去 /usr/lib64fd 了一通,果然并没有找到这个东西:

$ fd libpthread /usr/lib64
/usr/lib64/libpthread.a
/usr/lib64/libpthread-2.33.so
/usr/lib64/musl/lib/libpthread.a
/usr/lib64/libpthread.so.0
/usr/lib64/libpthread.so

关于如何对 ld 进行 debug 我也从来没有了解过。经过简单的搜索[7],了解到可以用 LD_DEBUG=all 或给 ld 添加 --verbose flag 进行 debug。先用 LD_DEBUG=all 重新尝试编译了一下,终端立刻被无穷的 debug 信息刷屏了,其中还有各种空行,这样对 debug 完全没有帮助。于是转而使用第二个方法,给 ld 添加 --verbose flag。因为不会通过在 setup.py 里加编译参数让 ld 吃到这个 flag,我直接(暴力地)把刚才报错的 $CONDA_PREFIX/compiler_compat/ld 改成了个脚本:

#!/usr/bin/env bash
x86_64-conda-linux-gnu-ld "$@" --verbose

再来一次吧。这次 ld 在讲人话了,最后几行的报错看起来是这样子:

attempt to open .../bin/../x86_64-conda-linux-gnu/sysroot/lib/../lib/libpthread.so failed
attempt to open .../bin/../x86_64-conda-linux-gnu/sysroot/lib/../lib/libpthread.a failed
attempt to open .../bin/../x86_64-conda-linux-gnu/sysroot/usr/lib/../lib/libpthread.so succeeded
opened script file .../bin/../x86_64-conda-linux-gnu/sysroot/usr/lib/../lib/libpthread.so
.../bin/../x86_64-conda-linux-gnu/sysroot/usr/lib/../lib/libpthread.so
opened script file .../bin/../x86_64-conda-linux-gnu/sysroot/usr/lib/../lib/libpthread.so
attempt to open /lib64/libpthread.so.0 succeeded
/lib64/libpthread.so.0
attempt to open /usr/lib64/libpthread_nonshared.a failed
attempt to open /usr/lib64/libpthread_nonshared.a failed
attempt to open /usr/lib64/libpthread_nonshared.a failed
attempt to open /usr/lib64/libpthread_nonshared.a failed
.../bin/x86_64-conda-linux-gnu-ld: cannot find /usr/lib64/libpthread_nonshared.a

其中这行看起来很可疑:

opened script file .../bin/../x86_64-conda-linux-gnu/sysroot/usr/lib/../lib/libpthread.so

为啥是 script file?我一直以为 .so 结尾的都是二进制动态库。去看看这个是什么东西:

$ cat $CONDA_PREFIX/bin/../x86_64-conda-linux-gnu/sysroot/usr/lib/../lib/libpthread.so
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( /lib64/libpthread.so.0 /usr/lib64/libpthread_nonshared.a )

GNU ld script.

看起来非常 legit,在最后一行看到了之前找不到的动态库/usr/lib64/libpthread_nonshared.a。考虑到这个 GNU ld script 文件位于这个 $CONDA_PREFIX/x86_64-conda-linux-gnu/sysroot/... 下,它想找的文件可能就在这个 sysroot 路径下。

$ cd $CONDA_PREFIX/x86_64-conda-linux-gnu/sysroot/usr/lib64
$ fd libpthread
libpthread.so
libpthread_nonshared.a

接下来就是把刚才找到的 GNU ld script 中引用的库的路径修改为这个绝对路径:

$ cat $CONDA_PREFIX/bin/../x86_64-conda-linux-gnu/sysroot/usr/lib/../lib/libpthread.so
/* GNU ld script
   Use the shared library, but some functions are only in
   the static library, so try that secondarily.  */
OUTPUT_FORMAT(elf64-x86-64)
/*GROUP ( /lib64/libpthread.so.0 /usr/lib64/libpthread_nonshared.a )*/
GROUP ( /.../x86_64-conda-linux-gnu/sysroot/lib64/libpthread.so.0 /.../x86_64-conda-linux-gnu/sysroot/usr/lib64/libpthread_nonshared.a )

以上 ... 是被隐藏了的绝对路径,需要修改为真实的路径。

然后回到要装的包源码目录下,指定 x86_64-conda-linux-gnu-gcc 为编译器进行编译:

$ CC=x86_64-conda-linux-gnu-gcc CXX=x86_64-conda-linux-gnu-gcc \
    python3 setup.py install

如此,编译成功,链接成功,且在同一个 conda 环境下可以 import 并运行。

剩余问题

下面的问题我已经不想深究了,今天与 python 环境管理器的斗智斗勇就到此为止吧。

  1. 是哪个包(gcc_linux-64 还是 gxx_linux-64)安装了这个 libpthread_nonshared.a 静态库?
  2. 是不是有办法(比如给一个环境变量指定要使用的 chroot 路径)可以不修改那个 GNU ld script?直接去改这种文件让我觉得绕了弯路。

    Disclaimer:指定 LD_CONFIG_PATHLD_LIBRARY_PATH$CONDA_PREFIX/x86_64-conda-linux-gnu/sysroot/usr/lib64 并没有什么区别,在 man:python(1) 中的ENVIRONMENT VARIABLES节也并没有和 ld 相关的信息,so 上虽然有人说过“要在 python 启动前设置这个变量才行”[8][9][10],但是都并非与 setuppy 安装相关的问题,我对此的实验也告诉我这个方法不 work


  1. 在尝试把实验室的电脑上的文件系统从 ext4 迁移到 btrfs 时,我用 rsync 把整个系统备份到了从师兄那里借来的固态硬盘上,最后抹掉分区、重新迁移回来时发现 rsync 在工作过程中满屏幕的报关于文件权限的错误。为了防止今后用的系统因为这个出什么岔子,干脆直接重装了。当然之前电脑上的数据被清洗了一遍后继续原封不动移到了自己的移动盘上(不直接用自己的盘备份是因为自己的盘是机械硬盘)。至于发生这种权限问题的原因:我也不知道,在迁移另一台电脑的文件系统时用的备份盘上是个 freshly formated btrfs,当时在同样的操作下并没有见到 rsync 报这种权限报错。也许是因为 5.15 内核的 ntfs3 模块本身就不保证权限?或者因为在 mount 这个 ntfs 时没有指定 umask / fmask / dmask?我能想到的避免这种事情的简便方法是远离 ntfs↩︎

  2. 现在我承认,两条路子的折腾程度可能差不多↩︎

  3. https://stackoverflow.com/a/62274859/13482274 ↩︎

  4. 这可能说明了前一次用 gcc-9 成功的经验是歪打正着。 ↩︎

  5. https://stackoverflow.com/a/66373099/13482274 ↩︎

  6. https://github.com/conda/conda-build/issues/3080#issuecomment-467931097 ↩︎

  7. https://stackoverflow.com/a/21647591/13482274 ↩︎

  8. https://stackoverflow.com/questions/856116/changing-ld-library-path-at-runtime-for-ctypes ↩︎

  9. https://stackoverflow.com/questions/23244418/set-ld-library-path-before-importing-in-python ↩︎

  10. https://stackoverflow.com/questions/1099981/why-cant-python-find-shared-objects-that-are-in-directories-in-sys-path ↩︎