最近都在折腾 Python ,免不了要安装几个不同版本的执行环境。Python 的主程 序可以选择使用静态链接或者动态链接——默认是静态链接的;本来静态链接的 Python 也没什么问题,但是在别的程序里嵌入 Python 的时候用动态链接就会比 较方便。

我的系统里有其他版本的 Python ,所以我希望每个版本都有自己的 prefix , 这样不同版本之间就不会有干扰。但是动态链接的 Python 使用自定义安装路径 时有个问题: Python 的主程序找不到 libpython*.so ,因为这个 so 不在标准 的 linker 搜索路径里。

Linker 寻找动态库的顺序

既然问题是 linker 找不到动态库,我们就先看看它寻找的过程。我这用的是 Linux ,默认用的是 GNU linker ,而根据 manpage , GNU linker 的搜索 顺序是酱的:

  1. 搜索 ELF 文件内 .dynamic 区段的 RPATH 字段指定的目录;
  2. 搜索环境变量 LD_LIBRARY_PATH 指定的目录;
  3. 搜索 ELF 文件内 .dynamic 区段的 RUNPATH 字段指定的目录;
  4. 搜索 /etc/ld.so.cache 文件中的索引——这个索引是由 ldconfig 根据 /etc/ld.so.conf 的内容生成的;
  5. 搜索默认目录 /lib 和 /usr/lib (又或者 /lib64 和 /usr/lib64 )。

一般的程序或者库文件都是安装到 /lib 或者 /usr/lib (又或者 lib64 )下, 不然就是在 /etc/ld.so.conf 里添加路径,所以 linker 肯定能找到。而我 习惯将 Python 执行环境安装到 $HOME 下,在全局配置里加入我的用户目录似 乎怪怪的,而每次执行程序都要加上 LD_LIBRARY_PATH 又很麻烦而且可能会对 其他程序造成影响……剩下的选项就只有 1 和 3 了。

设置 RPATH

这是一个 ld 命令的选项,只需要在用 gcc 编译的时候像这样加入 --rpath

1
gcc -L/foo/lib/location -lfoo -Wl,--rpath='/foo/lib/location' source.c -o binary

可以用 readelf 检查 RPATH 的值是否正确:

1
readelf -d binary

输出是这样的:

1
2
3
4
5
6
7
8
Dynamic section at offset 0xe08 contains 26 entries:
Tag        Type                         Name/Value
0x0000000000000001 (NEEDED)             Shared library: [libfoo.so]
0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
0x000000000000000f (RPATH)              Library rpath: [/foo/lib/location]
0x000000000000000c (INIT)               0x400548
0x000000000000000d (FINI)               0x400744
....

可见 Library rpath 的值已经被设置为 /foo/lib/location 了。这时如果执行 binary 的话, linker 就会先到 /foo/lib/location 寻找 libfoo.

另外,对于已经编译好的二进制文件,可以用一个叫 patchelf 的小工具修改 RPATH.

在 RPATH 中使用相对路径

上面我们设置的 RPATH 是绝对路径,如果我们想要打包一个“绿色版”的程序,扔到 任何位置都能执行呢?那当然是把所有动态库和可执行文件打包到一起,然后将 RPATH 设置为相对路径了。不幸的是,动态连接器在解析 RPATH 中相对路径的时 候,并不是以可执行文件所在目录为准,而是以当前目录为准的。我们总不能在 每次执行程序之前都 cd 到程序目录去吧?

为此, GNU linker 提供了针对 RPATH 的变量替换。例如,要指定相对于可执行 文件的路径,可以这样干:

1
gcc -L/foo/lib/location -lfoo -Wl,--rpath='$ORIGIN/../lib' source.c -o binary

执行 binary 时,这里的 $ORIGIN 变量会被 linker 替换为 binary 所 在的路径;如果将 binary 和动态库一起移动到别的位置再执行, linker 也能 通过相对位置找到对应的动态库。

在各种编译环境下设置 RPATH

Configure 脚本

回到 Python 动态链接的问题上, cpython 编译使用 configure 脚本,所以可以 用 LDFLAGS 传递 linker 选项:

1
2
export PY_PREFIX=$HOME/py/py35
./configure --prefix=$PY_PREFIX --enable-shared LDFLAGS="-Wl,--rpath='\$\$ORIGIN/../lib'"

这里的 --rpath 选项会被写入到 Makefile 里,所以 $ORIGIN 前面要再加一个 $ 以 进行转义.

CMake

cmake 命令提供了专门的变量 CMAKE_INSTALL_RPATH 用以指定安装后二进制文 件的 RPATH:

1
cmake /path/to/project -DCMAKE_INSTALL_RPATH="'\$ORIGIN/../lib'" ....

为什么说是“安装后”呢?因为 cmake 很贴心地提供了两种 RPATH 设置: 1)为了让编译好的程序能直接在编译目录执行, cmake 会根据编译时的选项 自动设置 RPATH ,使得处于编译目录中的动态库能被找到; 2)在执行 make install 时,如果有指定 CMAKE_INSTALL_RPATH , cmake 会将 RPATH 更改为相应的值,让程序转而使用安装好的动态库。

当然第一种 RPATH 也是可以手动设置的,详见 cmake 文档

Boost.Build

呃, 这个东西 除了 Boost 大概也没有别的项目在用了吧……问题是你总会 遇到那么几个情况是需要自己编译 Boost 库的……

跟 cmake 类似, b2 命令本身提供了指定 RPATH 的选项:

1
b2 dll-path="'\$ORIGIN/../lib'" ....

RPATH 和 RUNPATH 的区别

前面说的都是 RPATH ,那 RUNPATH 呢?呃,它俩的功能基本上是一样的,除 了上面提到的搜索顺序: RUNPATH 指定的路径可以被 LD_LIBRARY_PATH 覆盖, 但是 RPATH 指定的路径是优先级最高的。在 RUNPATH 字段存在的情况下, RPATH 字段会被忽略。

貌似 RPATH 的 历史 比 RUNPATH 要久远,所以支持 RPATH 的工具比较多。 但是 RUNPATH 比 RPATH 灵活,因为可以被环境变量覆盖。

GNU linker 在只指定 --rpath 选项的情况下默认只设置 RPATH 字段,要设置 RUNPATH 字段的话,还需要指定 --enable-new-dtags 选项:

1
gcc -L/foo/lib/location -lfoo -Wl,--rpath='$ORIGIN/../lib',--enable-new-dtags source.c -o binary