Why Y

使用libreadline6.3编译Ruby

Debian unstable版的软件包一般都很新,但是想尝试最新的ruby,还是不能依赖操作系统的升级。原来一直不太想用RVM、rbenv等工具,觉得系统自带的ruby足够了。现在想要尝试新版本了,就只能尝试一下了,顺便学习一下这些工具的使用。

原来曾经尝试过rbenv,感觉还不错,又听到过一些RVM负面的信息,所以决定还是从rbenv入手。rbenv的安装很简单,按照官网的README来就好。所有的文件都安装在 ~/.rbenv/目录中,还是比较好管理的。当时做得比较急,安装完成就准备安装ruby,但是tab了半天也没出install这个子命令。咋回事儿?继续看README吧,原来把编译安装ruby的功能做成了插件,需要单独安装。安装好后,终于有install子命令了,挑个比较新的版本,走起。

1
$ rbenv install 2.1.1

当时的网速不太好,历尽千辛万苦,终于下载完了。解压、编译,然后等来的是一个编译错误。rbenv把所有的临时文件全部删除了,只留下一个日志文件。初看这个日志文件,貌似与OpenSSL相关,因为日志的最后几行确实有OpenSSL的痕迹。但仔细查看日志,发现上面几行才是出错的真正原因,在日志中有类似下面的两行:

1
2
make[1]: *** [ext/readline/all] 错误 2
make[1]: *** 正在等待未完成的任务....

也就是说下面的应该是正常输出,是因为上面出错了才停止的。造成编译错误的罪魁祸首是这两行上面的日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
make[2]: Entering directory `/tmp/ruby-build.20140519215343.7209/ruby-2.0.0-p451/ext/readline'
compiling readline.c
readline.c: In function ‘Init_readline’:
readline.c:1886:26: error: ‘Function’ undeclared (first use in this function)
     rl_pre_input_hook = (Function *)readline_pre_input_hook;
                          ^
readline.c:1886:26: note: each undeclared identifier is reported only once for each function it appears in
readline.c:1886:36: error: expected expression before ‘)’ token
     rl_pre_input_hook = (Function *)readline_pre_input_hook;
                                    ^
readline.c: At top level:
readline.c:530:1: warning: ‘readline_pre_input_hook’ defined but not used [-Wunused-function]
 readline_pre_input_hook(void)
 ^
make[2]: *** [readline.o] 错误 1
make[2]: Leaving directory `/tmp/ruby-build.20140519215343.7209/ruby-2.0.0-p451/ext/readline'
make[1]: *** [ext/readline/all] 错误 2
make[1]: *** 正在等待未完成的任务....
...

从上面的错误日志中可以发现,编译错误是在编译ext/readline/readline.c时,1886行的Function未声明造成的。上网搜索关键字rbenv readline Function,发现这还不是个别现象,应该算是ruby的一个BUG。问题的根本原因是ruby依赖的libreadline库从6.2升级到6.3时,将Function的typedef去掉了,但ruby使用libreadline的代码中还存在对Function的使用,因此就出现了符号未声明的编译错误。找到了问题的根源,修复的措施就简单了,使用libreadline新风格的typedef,将Function替换成rl_hook_func_t就行了。

rbenv除了可以用rbenv install来自动编译安装ruby外,也可以手动编译,最后安装到~/.rbenv/versions/对应版本的目录中。比如2.1.1版本的ruby,在configure时添加--prefix=~/.rbenv/versions/2.1.1即可。

1
2
3
4
$ ./configure --prefix=~/.rbenv/versions/2.1.1
$ make
$ make check
$ make install

在查找这一问题时,通过ruby的版本控制系统,还发现了一些比较有趣的东西。这才是本文的重点。:) Ruby有自己的独立版本库,使用的应该是svn,不过在GitHub上有镜像版本库,提交日志是基本相同的。因为比较熟悉GitHub,下面描述的提交日志都来自GitHub。

关于该问题的代码修改都在ext/readline目录中,看一下与该目录有关的提交日志,哇,从2014年3月1日到2014年4月4日,总共用了5次提交才比较好的修复了本问题。依次浏览这五次提交,看看到底是怎么会事儿。

2014年3月1日,提交ed6a2d3bf6,修改了ext/readline/readline.c。修正方法与我上面描述的相同,就是把Function换成了rl_hook_func_t,貌似问题解决。

2014年3月2日,前一天刚修复了BUG,怎么又有针对同一问题的提交,而且一天就提交了两次?第一次提交2bb8811484rl_hook_func_t for old readline。好吧,前一天的修改简单的改了类型名,没有考虑向前兼容老版本的readline,从提交信息看是这样的。修正方法也比较简单,就是判断一下有没有rl_hook_func_t这个类型,没有的话就通过宏定义将rl_hook_func_t替换成Function。第二次提交083bf23759,添加了针对上一次提交的注释。难道是修改得太急,改了问题,忘了用注释解释一下?

1
2
3
+  # rl_hook_func_t is available since readline-4.2 (2001).
+  # Function is removed at readline-6.3 (2014).
+  # However, editline (NetBSD 6.1.3, 2014) doesn't have rl_hook_func_t.

貌似是为了兼容NetBSD的editline库,那第一次提交里提到的for old readline就不太准确了。也就是说第二次提交可能是为了补救上一次提交出现的歧义。

但仍然没有结束。2014年3月31日,提交6648136779fix typo,哦,是$defs而不是$DEFS,但为什么过了将近一个月才发现?

2014年4月4日,提交d2a8e28597,不是检查rl_hook_func_t而应该检查rl_hook_func_t*。我的疑问与上一次提交一样,为什么过了一个月才发现?

通过对这一系列提交的追溯,我们发现是不是有些似曾相识?在修正BUG的时候不小心又引入了新的BUG,然后再修正新BUG,然后因为匆忙修正的不彻底。类似这样的问题有很多,如何才能在开发过程中尽量避免?Ruby的代码是有单元测试的,但是在不同库版本、不同操作系统的环境中单元测试的结果可能是不同的。我猜测单元测试可能只在比较主流的环境中运行,相当于跳过了这些兼容的代码,因此也就没有及时发现问题,导致一个BUG花了一个月才彻底解决。

PS:Ruby 2.1.2已经合并了这些修改,编译2.1.2不需要再手动修改代码。