编写一个可移植的 ‘configure.in’ 文件是一个需要技巧的工作。你可以把任意的 shell code 写到 ‘configure.in’ 文件里,选择是非常多的。第一次使用 Autoconf 时可以会遇到以下疑问: 什么是可移植的,什么不是可移植的?应该检测(测试系统是否有某个功能或是满足什么条件)什么? 不应该检测什么? 怎样更好地使用 Autoconf 的功能? ‘configure.in’ 里不应该写什么? 检测的顺序是什么? 什么时个只检测系统的名字就可以而不必检测它有什么功能?
在讨论检测什么和怎样检测的技巧之前,我们先问自己一个简单的问题:什么是可移植性? 可移植性是具有可以在不同平台上构建和运行的品质的代码。在 Autoconf 环境下,可移植性通常是指可以运行在 Unix-like 系统的能力,有时包括 Windows。
我第一次使用 Autoconf 时,我费了很大力气决定在 ‘configure.in’ 文件里要检测什么。当时,我维护一个只能运行在 SunOS 4 下的程序。然而,我想把它移植到 Solaris,OSF/1,也许包括 Irix。
我花了很多时间做到这一点:我写了一个很小的 ‘configure.in’,试着让我的程序在 Solaris 下运行。每当我遇到问题,就改进一下 ‘configure.in’ 和程序代码。当它可以构建时,我开始测试它移植后能否正确运行。
因为我没有开始就考虑到可移植问题,也没有意识到应该把 Autoconf 放到软件包里(see section 24),我遇到了比想像中更多的困难。如果开始就考虑到移植问题会更好。
有许多的 Unix-like 系统,且有许多还在使用的系统是过时的。把应用程序移植到所到像这样的系统里的尝试往往无效。移植所有的程序是一个困难的过程,尤其是通常无法检测所有的系统,因为有很多新的系统产生,这些系统往往存在问题,还有一些怪异的行为。
我们提供一个可用的可移植方法:我们写程序时考虑为数众多的以相当先进的 Unix-like 的系统。能过改进 ‘configure.in’ 和源代码到解决移植时遇到的问题。在实用中,这是一个有效的方法。
如果读过一些 ‘configure.in’,会注意到他们往往用不平常的风格。例如,很难看到 ‘[' 的使用,取而代之的是调用 'test'。在这里我们有会深入的讲解编写可移植脚本的细节,这在第22章讲解。
像其它可移植性问题一样,要有目的地写 'configure.in' 和 'Makefile.am'。有些系统并不遵从标准的 sh 规则。例如 Ultrix 系统不支持 unset 命令。GNU Autotool 可以编写在大多数系统上可移植的代码,它不会制约可移植性能力。
可移植的 sh 代码并不完全可行。sh 能做的很少,大多数工作是靠单独的有潜在移植性问题的程序完成的。例如,一些选项不是在所有系统都支持的,一些显然应该有的程序在有些系统上没有。所以你 不但要知道什么 sh 命令不可以移植,还要知道什么你能(和不能)使用,什么是可移植的。
真令人灰心,但实际上编写可移植的 shell 脚本并不像相像中困难。你使用一次后,就会掌握它的规则。不幸的是,这要花很多时间。想想其它的可移植代码,你会发现“实验 - 验证”的方法很有效。再一次,你会发现你想要的风格是什么 -- 编写只在主流 linux 运行的程序和像 emacs、gcc 一样的可移植性非常好的程序是非常不同的。编写不可移植的 'configure.in' 非常简单。通常改写不可移植的程序段也很简单。
编写可移植 sh 代码的另一个问题是决定各种检测的顺序。,这里将给出说明 Autoconf 间接(通过 autoscan 程序, see 24.)建议的标准顺序。
标准顺序是:
1.模板。应该包含标准模板代码,如调用 AC_INIT (必须在前面), AM_INIT_AUTOMAKE, AC_CONFIG_HEADER,或者 AC_REVISION.
2.选项。应该包括 configure 命令行选项和宏,如AC_ARG_ENABLE。 一般也包括选项代码,如下面的 libgcj 例子:
AC_ARG_ENABLE(getenv-properties, [ --disable-getenv-properties don't set system properties from GCJ_PROPERTIES]) dnl Whether GCJ_PROPERTIES is used depends on the target. if test -n "$enable_getenv_properties"; then enable_getenv_properties=${enable_getenv_properties_default-yes} fi if test "$enable_getenv_properties" = no; then AC_DEFINE(DISABLE_GETENV_PROPERTIES) fi
3. 程序文件。在执行 configure 时,构建时,或者程序文件构建时要检测程序文件。一般是执行 AC_CHECK_PROG 和 AC_PATH_TOOL 这样的宏。
4. 库文件。 在 C (或 C++,或其它) 库文件要比其它的先检测。这是必要的,因为在进行其它检测时可能会尝试连接程序,检测库文件保证正确连接。
5. 头文件。 检测头文件是否存在。
6. 类型和结构定义。在检测头文件后检测类型和结构是因为它们在头文件中出现。在使用之前要确定它是否可用。
7. 函数。最后检测是因为函数依赖于前面的文件。
8. 输出文件。通过 AC_OUTPUT 完成。
这个顺序可以作为简单准则,它并不严格。有时为了使地 ‘configure.in’ 更容易维护,必须要交错测试,或者检测本身必须用不同顺序。例如,如果项目同时使用 C 和 C++,为了使 ‘configure.in’ 更可读可以先检测全部的 C 再检测 C++。
决定要检测什么是 ‘configure.in’ 的主要问题。读过 Autoconf 参考手册后,就会清楚怎样编写单独的检测代码。“何时”仍然很神秘 — 检测很多东西和检测很少的东西一样简单。
在 Unix-like 系统中需要注意的是并不是所有的系统都拥有相同的程序。 即使有,也不一定行为都一样。对于这些问题,我们建议,尽量遵守 GNU 代码标准:对相对有限的程序使用最通用的选项。如果这样还不行,试着使用 POSIX 选项,或者检测你关心的平台的问题。
检测工具和他们的差异往往是 ‘configure’ 脚本的一小部分,一般还会检测函数、库文件等等。
除了一些如 ‘libc’ 的核心库文件,一些不被认为是核心库的库文件如 ‘libm’ 和 ‘libX11′,在不同的 Unix 系统中名字和内容可能不同。即使这样,库文件依然很好处理,因为库文件只会影响 ‘Makefile’。这意味着检测另个一个库文件并不需要对源代码进行很大(或者有时会,几乎不会)改变。增加一个库文件的检测对开发周期的影响很小, 只需重新运行 ‘configure’ 重新连接,对库文件的检测可以使用很松散的方法。例如,你可以先使库文件只在很少的你关心的系统上有效,再视需要改进它。
遇到连接问题该如何处理?要做的第一件事是用 nm 检测库文件中是否存在要找的函数。如果存在并且可以使用,问题就变得简单 — 只要加入一个 AC_CHECK_LIB 宏。注意只在库文件有找到这个函数是不够的,因为在一些系统中一些 “标准” 库并不好用,’libucb’ 就是一个应当避免使用的库的例子。
如果在库文件中找不到函数,意味着它是一个不可移植的函数。找不到函数的原因基本上有三种。下面讨论函数,这些方法多少也可以用在 typedef,结构体和全局变量上。
第一种方法是写一个替代函数,使用条件编译,或者把它放到一个适当命名的文件里,用 AC_REPLACE_FUNCS 宏使用它。例如,Tcl 用 AC_REPLACE_FUNCS(strstr) 处理没有 strstr 函数的系统。
第二种方法是使用一个功能相似的不同函数。根据系统选择使用不同的函数。 习惯作法是把 break 作为 AC_CHECK_FUNCS 的第二个参数;这种作法可以避免不必要的检测和读者相关的检测.例如,下面的例子是 libgcj 检测 inet_aton 或 inet_addr 函数,它只使用它找到的第一个:
AC_CHECK_FUNCS(inet_aton inet_addr, break)
使用这些检测结果的代码如下:
#if HAVE_INET_ATON ... use inet_aton here #else #if HAVE_INET_ADDR ... use inet_addr here #else #error Function missing! #endif #endif
注意当函数不存在时是如何产生一个编译时错误。通常在构建过程中越早发现错误越好。
第三种方法是写一个可选的函数。例如,写一个编辑器时可以用 mmap 函数把文件映射到内存中。但是 mmap 不可移植,还要再写一个可移植的函数。
处理不可移植的函数只是检测过程的一部分。实用的方法很有效,但它在如 GNU/Linux 这样函数齐全的现代的系统中效率很低。所以在项目快完成之前可能不会关心不可移植的结构。
不幸的是,没有更好的方法解决这个问题。你需要对 Unix 有实践经验。POSIX 和 XPG 标准的知识很有帮助。但标准不是万灵药,不是所有系统都遵从标准。而且有些系统的函数存在 bug。
可能遇到的最后一类问题是,检测过多。它会增加你的维护负担。例如,有时会看到检测 的代码,这是没有必要的,因为它是可移植的。同样,这只能通过你对目标系统的实践知识的了解解决。
当采用的最好的方法检测功能时,’configure’ 偶尔会任配置名做决定。当标准的 Autoconf 功能检测不能检测一些东西时,这种做法可能是必要的。例如,需要关于系统的 ‘tty’ 实现的信息,不检测配置名是无法了解到的。
通常检测特定的功能要比检测特定系统类型更好,因为 Unix 和其它系统的演变,不同的系统从另一个系统复制了某些功能。
在 ‘configure’ 脚本中不能检测配置名时,定义一个描述这个功能的宏要比描述系统类型的宏要好。这样可以把这个宏用于其它检测这个功能的系统(see section 23)。
在 autoconf ‘configure.in’ 文件里一般用一个 case 语句检测检测系统。case 语句可能像下面一样寻找某个东西,来确定 ‘host’ 是不是一个规定的系统 shell 的变量。这里 case 语句使用 ‘AC_CANONICAL_HOST’ 或 ‘AC_CANONICAL_SYSTEM’ 宏。
case "${host}" in i[[3456]]86-*-linux-gnu*) do something ;; sparc*-sun-solaris2.[[56789]]*) do something ;; sparc*-sun-solaris*) do something ;; mips*-*-elf*) do something ;; esac
注意这段代码里的两组方括号。它在 autoconf 实现里随处可见。它使用 M4 引擎。如果不是两组括号,M4 不会解析它,也不会出现在 ‘configure’ 里。详见24章。
在系统字段后面使用 ‘*’ 非常重要,它可以匹配 ‘config.guess’ 产生的系统版本。大多数的 case 要小心地匹配处理器类型。大多数的处理器家族,用一个 ‘*’ 就可以,像上面的 ‘mips*’。对于 i386 家族,使用 ‘i[34567]86′ 就可以。对于 m68k 处理器家族,可能会用到 ‘m68*’ 这样的字段。当然,如果不强调处理器,可以用 ‘*’ 替换整个字段,如 ‘*-*-irix*’。