opensource 的循环依赖

这篇文章主要记录下在openssl和一些依赖ssl的应用程序的编译过程中,遇到的一些问题和和思考。

一个原则

通常源代码编译应该是只包含3个步骤

  1. 配置
  2. 编译
  3. 部署

配置多种多样,但编译理应与配置区配开来,不需要再进行相似的配置,理想情况只需一个 make 即可

循环依赖

最近升级opensource模块,遇到一个很有趣的情况,需要编译的模块如下:

  • openssl
  • libssh2
  • curl

正常情况的依赖关系是这样的:

1
2
curl +-> libssh2 +-> openssl
+--------------------^

然而引用的 openssl-1.0.1h-cmp 开源模块在默认配置下造成了如下的依赖关系:

1
2
curl +-> libssh2 +-> openssl
^--------------------^

这样在编译 curl 时,会产生一个很微妙的问题,先编译的 openssl 由于在一开始只是编译库而没有链接可执行文件,所以没有问题,但是到了链接 curl 时,会报出在libssl里找不到 libcurl 的接口为定义的问题,也就是说 curl 自己来依赖一个自己还没有编译出来的自己的接口的循环依赖问题!

所以依赖管理对于多模块的项目是是很重要的,对于 openssl-1.0.1h-cmp 模块依赖 curl 的选项本来就不应该提供,所以默认不要去打开就可以了。

源代码获取

openssl

https://www.openssl.org/source/

下载 openssl-1.1.0c.tar.gz

另外,有依赖问题的cmp版本openssl源自 cmpforopenssl 项目

因为是svn项目,用svn检出

1
2
3
4
5
$ svn checkout http://svn.code.sf.net/p/cmpforopenssl/code/src/openssl-1.0.1h-cmp

$ svn info
Last Changed Rev: 782
Last Changed Date: 2016-10-28 17:04:07 +0800 (Fri, 28 Oct 2016)

编译前先保证svn仓库的清爽

1
2
$ svn revert -R .
$ svn status | grep "^? " | awk '{print $2}' | xargs rm -rf

libssh2

https://www.libssh2.org/download/

下载 libssh2-1.7.0.tar.gz

curl

https://curl.haxx.se/download/

下载 curl-7.50.3.tar.gz

源代码编译

toolchain

交叉编译工具版本

1
2
$ arm-none-linux-gnueabi-gcc -v
gcc version 4.8.1 (Sourcery CodeBench Lite 2013.11-33)

编译openssl

openssl 采用了自有的编译脚本,而非GNU标准的 configure 配置脚本
另外,这里指定了一下 linux-generic32 的目标,为了在64位系统上编译32位的可执行程序

1
2
3
4
5
$ export CROSS_COMPILE=arm-none-linux-gnueabi-
$ ./Configure linux-generic32 no-asm shared --prefix=$HOME/opt/openssl
$ make update
$ make
$ make install

如果没有一开始export,那么就要这样编译

1
2
$ make MAKEDEPPROG=arm-none-linux-gnueabi-gcc update
$ make CROSS_COMPILE=arm-none-linux-gnueabi-

这里其实有个问题,执行完 config 可以看到 CC=gcc,在 make 时通过 CROSS_COMPILE=arm-none-linux-gnueabi- 这个“常用”变量来添加交叉编译前缀,这样多少导致“配置”的内容让“编译”步骤又配置了一次
其实,在官方的 openssl-1.1.x 版本开始已经支持 –cross-compile-prefix 参数来指定交叉编译器版本

1
2
$ ./Configure linux-generic32 no-asm shared --prefix=$HOME/opt/openssl --cross-compile-prefix=arm-none-linux-gnueabi-
$ make

libssh

来到libssh,总算回到了标准GNU工具automake自动生成的 configure 脚本,交叉编译都是通过经典的 –host 等参数指定

这里本想使libssh安装到和openssl不同的路径进行配置

1
$ ./configure --prefix=$HOME/opt/libssh --host=arm-none-linux-gnueabi --with-openssl --with-libssl-prefix=$HOME/opt/openssl --disable-examples-build

但在编译时出现了问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ make
CCLD libssh2.la
make[2]: Leaving directory `/home/ubuntu/git/libssh2-1.7.0/src'
make[1]: Leaving directory `/home/ubuntu/git/libssh2-1.7.0/src'
Making all in tests
make[1]: Entering directory `/home/ubuntu/git/libssh2-1.7.0/tests'
CC ssh2.o
CCLD ssh2
/home/ubuntu/opt/arm-2013.11/bin/../lib/gcc/arm-none-linux-gnueabi/4.8.1/../../../../arm-none-linux-gnueabi/bin/ld: warning: libssl.so.1.0.0, needed by ../src/.libs/libssh2.so, not found (try using -rpath or -rpath-link)
/home/ubuntu/opt/arm-2013.11/bin/../lib/gcc/arm-none-linux-gnueabi/4.8.1/../../../../arm-none-linux-gnueabi/bin/ld: warning: libcrypto.so.1.0.0, needed by ../src/.libs/libssh2.so, not found (try using -rpath or -rpath-link)
../src/.libs/libssh2.so: undefined reference to `BN_rand'
../src/.libs/libssh2.so: undefined reference to `EVP_CipherInit'
../src/.libs/libssh2.so: undefined reference to `BN_set_word'
../src/.libs/libssh2.so: undefined reference to `EVP_PKEY_get1_DSA'

这里根据提示 try using -rpath or -rpath-link 来补充些 LDFLAGS 变量

1
$ make LDFLAGS=-R$HOME/opt/openssl/lib

注意:虽然这样可以编译通过,但是会带来另外的问题(下面再展开讨论)

再次为了避免 make 使用额外的参数,调整一下 –prefix 参数使用和 openssl 一样的路径配置

1
2
3
$ ./configure --prefix=$HOME/opt/openssl --host=arm-none-linux-gnueabi --with-openssl --with-libssl-prefix=$HOME/opt/openssl --disable-examples-build
$ make
$ make install

curl

这里继续配置相同的安装路径,避免最后链接 curl 时找不到库路径的问题
编译部署完成后,在 $HOME/opt/openssl 的bin目录下终于能看了一个可执行的 curl 程序 :)

1
2
3
$ ./configure --prefix=$HOME/opt/openssl --host=arm-none-linux-gnueabi --with-ssl=$HOME/opt/openssl --with-libssh2=$HOME/opt/openssl
$ make
$ make install

最后,通过 readelf 来观察一下 curl 的库依赖情况

1
2
3
4
5
6
7
8
9
$ arm-none-linux-gnueabi-readelf -d bin/curl
Dynamic section at offset 0x44698 contains 29 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libcurl.so.4]
0x00000001 (NEEDED) Shared library: [libssl.so.1.0.0]
0x00000001 (NEEDED) Shared library: [libcrypto.so.1.0.0]
0x00000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000f (RPATH) Library rpath: [/home/ubuntu/opt/openssl/lib]

重点注意 RPATH Library rpath: [/home/ubuntu/opt/openssl/lib] 的内容,这里如果是编译本机运行的程序是没有问题的,但是在交叉编译的情境下多多少少就会有不合理的地方
因为 /home/ubuntu/opt/openssl/lib 的路径是编译机的路径,而部署到目标机上可能根本就不存在这样的目录
所以,回过来看 make install 其实还不是真正的“部署”,configure 脚本还差一个指定目标机运行时库路径的参数,automake工具(更准确说是gcc工具链)直接的把编译时指定的路径用作运行时路径是不够的。

后记

其实,编译 opensource 模块本身并不困难,真正的挑战在于将这些模块整合进自己的项目中,并且要一起构建整个项目群时,才会遇到超出 automake 工具能解决的问题。由于 configure 一般要求依赖模块已经存在,这就导致需要在编译一个模块前先 make 依赖的模块,最后总的 makefile 很可能就变成了一个 makefile.sh 的形式。这时考验这个 makescript 的一个关键就是是否会在每次 make 时都需要重新 configure 了。
当然,将模块在外部 configure 后再加入项目版本控制肯定是不推荐的,比较好的方法是采用 prebuilt 预编译管理开源模块。
另外,关于 make install 又有个问题就是 make 一个模块 install 一个,还是最后统一遍历 install 呢?对此就保留下建议了。
总之,没有最好的方法,切合自身项目的构建需要才是最重要的,而且如果编译能做到尽可能优化最终节省的还是整个 team 的时间。
这次先总结到这里,欢迎留言讨论指正。


版权声明

作者: Marcus Tang
许可证: 创作共用保留署名-非商业-禁止演绎4.0国际许可证
License: Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
本文永久链接: https://blog.tmc1900.com/open-build/