天天看点

开源项目管理:使用automake 各组件的关联

    使用一个开源项目,获得源码后一般都是按configure/make/make install的步骤进行部署。网上有不少关于使用automake系列工具来生成/管理项目的,最终都指向ibm-developer的一篇文章,但说实话该文章跟真正的开源项目还有一大截距离,大部分coder情愿自己写makefile代替。本文原意是往开源项目中添加代码,当然不是简单的往现成的文件中加几行了事,而是,往开源项目的目录结构中添加文件,并导出接口供开源项目本身或者其他项目使用。

    如果直接写如何添加文件,那这篇文章又可能只是一个花瓶,解决不了什么问题。因此,文章还是得从分析configure内容开始,展现各文件之间的关系。至于其他枝节,不明自喻。附注,本文使用的开源项目为libvirt1.0.0和sed4.2

    先上一张广为流传的automake的流程图:

开源项目管理:使用automake 各组件的关联

图1

    1.第一个问题,拿到开源项目后,大家都会不约而同的运行configure,以生成makefile文件,那么configure中有什么?

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

列表1,sed项目configure部分内容

#这里只摘取部分
#第一部分
...
NEXT_AS_FIRST_DIRECTIVE_WCTYPE_H
NEXT_WCTYPE_H
HAVE_ISWCNTRL
NEXT_AS_FIRST_DIRECTIVE_WCHAR_H
NEXT_WCHAR_H
HAVE_WCHAR_H
HAVE_WINT_T
HAVE_UNISTD_H
NEXT_AS_FIRST_DIRECTIVE_UNISTD_H

#第二部分
...
if test "$cross_compiling" = yes; then
  { $as_echo "$as_me:$LINENO: result: don't care (cross compiling)" >&5
$as_echo "don't care (cross compiling)" >&6; }; XFAIL_TESTS=
else
  cat >conftest.$ac_ext <<_ACEOF
/* confdefs.h.  */
_ACEOF
cat confdefs.h >>conftest.$ac_ext
cat >>conftest.$ac_ext <<_ACEOF
/* end confdefs.h.  */

#include <locale.h>
#include <string.h>
#include <stdlib.h>
#include <wchar.h>
#ifdef HAVE_WCTYPE_H
#include <wctype.h>
#endif

int test(void)
{
  char in[] = "\xD0\xB4";
  char good[] = "\xD0\x94";
  char out[10];
  wchar_t in_wc, good_wc;
  if (mbtowc (&in_wc, in, 3) == -1)
    return 0;
  if (mbtowc (&good_wc, good, 3) == -1)
    return 0;
  if (towupper (in_wc) != good_wc)
    return 0;
  if (wctomb (out, good_wc) != 2)
    return 0;
  if (memcmp (out, good, 2))
    return 0;
  return 1;
}

int main()
{
  char *old;
  int len;

  /* Try hardcoding a Russian UTF-8 locale.  If the name "ru_RU.UTF-8"
     is invalid, use setlocale again just to get the current locale.  */
  old = setlocale (LC_CTYPE, "ru_RU.UTF-8");
  if (old)
    {
      if (test())
        exit (0);
    }
  else
    old = setlocale (LC_CTYPE, "C");

  /* Maybe cyrillic case folding is implemented for all UTF-8 locales.
     If the current locale is not UTF-8, the test will be skipped.  */
  len = strlen (old);
  if ((len > 6 && !strcmp (old + len - 6, ".UTF-8"))
      || (len > 6 && !strcmp (old + len - 6, ".utf-8"))
      || (len > 5 && !strcmp (old + len - 5, ".UTF8"))
      || (len > 5 && !strcmp (old + len - 5, ".utf8")))

    /* ok */
    ;
  else
    exit (1);

  /* Run the test in the detected UTF-8 locale.  */
  setlocale (LC_CTYPE, old);
  exit (!test ());
}

_ACEOF
rm -f conftest$ac_exeext
if { (ac_try="$ac_link"
case "(($ac_try" in
  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
  *) ac_try_echo=$ac_try;;
esac
eval ac_try_echo="\"\$as_me:$LINENO: $ac_try_echo\""
$as_echo "$ac_try_echo") >&5
  (eval "$ac_link") 2>&5
  ac_status=$?
  $as_echo "$as_me:$LINENO: \$? = $ac_status" >&5
  (exit $ac_status); } && { ac_try='./conftest$ac_exeext'
  { (case "(($ac_try" in
  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
  *) ac_try_echo=$ac_try;;
esac
eval ac_try_echo="\"\$as_me:$LINENO: $ac_try_echo\""
$as_echo "$ac_try_echo") >&5
  (eval "$ac_try") 2>&5
  ac_status=$?
  $as_echo "$as_me:$LINENO: \$? = $ac_status" >&5
  (exit $ac_status); }; }; then
  { $as_echo "$as_me:$LINENO: result: yes" >&5
$as_echo "yes" >&6; }; XFAIL_TESTS=
else
  $as_echo "$as_me: program exited with status $ac_status" >&5
$as_echo "$as_me: failed program was:" >&5
sed 's/^/| /' conftest.$ac_ext >&5

( exit $ac_status )
{ $as_echo "$as_me:$LINENO: result: no" >&5
$as_echo "no" >&6; }; XFAIL_TESTS='utf8-1 utf8-2 utf8-3 utf8-4'
fi
...
#第三部分
# Check whether --enable-i18n was given.
if test "${enable_i18n+set}" = set; then
  enableval=$enable_i18n;
else
  enable_i18n=yes
fi

if test "x$enable_i18n" = xno; then
  ac_cv_func_wcscoll=false
fi

# Check whether --enable-regex-tests was given.
if test "${enable_regex_tests+set}" = set; then
  enableval=$enable_regex_tests; if test "x$with_included_regex" = xno; then
  enable_regex_tests=no
fi
else
  enable_regex_tests=$with_included_regex
fi      

仔细看是一些变量定义和一些bash语句用于检测编译环境。这configure文件动辄过万行,估计都跟一些小的项目的代码量差不多了,那可能是由某个输入文件输入,然后通过工具生成。结合图1知,输入文件是configure.in/configure.ac生成工具是autoconf。工具肯定是没法修改了,那只能退一步,查看输入文件configure.in

列表2,configure.in部分内容:

#这里只摘取部分
#configure中第一部分在此文件中找不到对应项
#对应configure第三部分
AC_MSG_CHECKING([whether UTF-8 case folding tests should pass])
AC_TRY_RUN([
#include <locale.h>
#include <string.h>
#include <stdlib.h>
#include <wchar.h>
#ifdef HAVE_WCTYPE_H
#include <wctype.h>
#endif

int test(void)
{
  char in[] = "\xD0\xB4";
  char good[] = "\xD0\x94";
  char out[10];
  wchar_t in_wc, good_wc;
  if (mbtowc (&in_wc, in, 3) == -1)
    return 0;
  if (mbtowc (&good_wc, good, 3) == -1)
    return 0;
  if (towupper (in_wc) != good_wc)
    return 0;
  if (wctomb (out, good_wc) != 2)
    return 0;
  if (memcmp (out, good, 2))
    return 0;
  return 1;
}

int main()
{
  char *old;
  int len;

  /* Try hardcoding a Russian UTF-8 locale.  If the name "ru_RU.UTF-8"
     is invalid, use setlocale again just to get the current locale.  */
  old = setlocale (LC_CTYPE, "ru_RU.UTF-8");
  if (old)
    {
      if (test())
        exit (0);
    }
  else
    old = setlocale (LC_CTYPE, "C");

  /* Maybe cyrillic case folding is implemented for all UTF-8 locales.
     If the current locale is not UTF-8, the test will be skipped.  */
  len = strlen (old);
  if ((len > 6 && !strcmp (old + len - 6, ".UTF-8"))
      || (len > 6 && !strcmp (old + len - 6, ".utf-8"))
      || (len > 5 && !strcmp (old + len - 5, ".UTF8"))
      || (len > 5 && !strcmp (old + len - 5, ".utf8")))

    /* ok */
    ;
  else
    exit (1);

  /* Run the test in the detected UTF-8 locale.  */
  setlocale (LC_CTYPE, old);
  exit (!test ());
}
], [AC_MSG_RESULT([yes]); XFAIL_TESTS=],
   [AC_MSG_RESULT([no]); XFAIL_TESTS='utf8-1 utf8-2 utf8-3 utf8-4'],
   [AC_MSG_RESULT([don't care (cross compiling)]); XFAIL_TESTS=])
...

#对应configure第二部分

AC_ARG_ENABLE(i18n,[ --disable-i18n disable internationalization (default=enabled)], ,enable_i18n=yes)
if test "x$enable_i18n" = xno; then 
ac_cv_func_wcscoll=falsefiAC_ARG_ENABLE(regex-tests, 
[ --enable-regex-tests enable regex matcher regression tests (default=yes)],
[if test "x$with_included_regex" = xno; then enable_regex_tests=nofi],
enable_regex_tests=$with_included_regex)      

虽然在configure.ac和configure在内容有极大不同,但细看还是有很多相同点,只是比较隐晦。比如

都有设置变量:

enable_regex_tests=no      

都有函数测试:

int main()
{
  char *old;
  int len;

  /* Try hardcoding a Russian UTF-8 locale.  If the name "ru_RU.UTF-8"
     is invalid, use setlocale again just to get the current locale.  */
  old = setlocale (LC_CTYPE, "ru_RU.UTF-8");
  if (old)
    {
      if (test())
        exit (0);
    }
  else
    old = setlocale (LC_CTYPE, "C");

  /* Maybe cyrillic case folding is implemented for all UTF-8 locales.
     If the current locale is not UTF-8, the test will be skipped.  */
  len = strlen (old);
  if ((len > 6 && !strcmp (old + len - 6, ".UTF-8"))
      || (len > 6 && !strcmp (old + len - 6, ".utf-8"))
      || (len > 5 && !strcmp (old + len - 5, ".UTF8"))
      || (len > 5 && !strcmp (old + len - 5, ".utf8")))

    /* ok */
    ;
  else
    exit (1);

  /* Run the test in the detected UTF-8 locale.  */
  setlocale (LC_CTYPE, old);
  exit (!test ());
}      

这么说,可以猜测configure中的shell脚本内容是通过configure.ac按某种语法规则组合生成的。

按 http://gnu.april.org/software/automake/manual 的说明,configure中的内容是有configure.ac通过一条条宏语句扩展后生成。configure.ac就像.h文件定义了大量的宏操作,每个宏中有一个或多个变量。而在configure就像.cpp,在文件中引用了这些宏。经过编译宏被展开,只不过configure中被展开为shell脚本。

1.1)比如

configure.ac中的宏: AC_TRY_RUN([program, [action-if-true [, action-if-false [, action-if-cross-compiling]]]]); 其中,program是C程序的文本。本宏在编译时使用CFLAGS或者CXXFLAGS以及 CPPFLAGS、LDFLAGS和LIBS。

经过autoconf把宏展开到configure中。当configure运行时,如果被program成功地编译和连接了并且在执行的时候返回的退出状态为0,就运行action-if-true中的shell命令。否则就运行action-if-false中的shell命令;而程序的退出状态可以通过shell变量$ac_status;得到。

放在这个项目中,宏扩展后,configure脚本运行,成功则为XFAIL_TESTS设置xxx,不成功又设置XFAIL_TESTS为xxx

1.2)又如

configure.ac中的宏:AC_ARG_ENABLE (feature, help-string [, action-if-given [, action-if-not-given]])

它进过autoconf,生成shell到configure中,使得如果用户以选项`--enable-feature'或者`--disable-feature'作为附加参数 运行configure,就运行action-if-given中的shell命令。

如果两个选项都没有给出,就运行 action-if-not-given中的shell命令

3)而像宏:AC_DEFINE_UNQUOTED(name, value,[help info])

它更简单了直接在configure中定义了名为name的变量,值为value。

1.3)结论:

总体来说configure.ac作为configure的输入文件,由autoconf将其中定义的宏进行扩展,生成configure中的shell,宏里面定义了一些变量和一套对应不同变量值所应有的动作。configure在运行时要检测系统环境,根据检测结果去设置宏里的变量以及做出相应的动作。

    2.程序的执行总有输入和输出,既然configure.ac是configure的输入文件,那么,configure运行后的输出结果有哪些,即上文提到的相应的动作指啥?

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

首先看configure.ac中这段:

列表3:

# Check whether we are able to follow symlinks
AC_CHECK_FUNC(lstat, have_lstat=yes)
AC_CHECK_FUNC(readlink, have_readlink=yes)
if test "x$have_lstat" = xyes -a "x$have_readlink" = xyes; then
   AC_DEFINE(ENABLE_FOLLOW_SYMLINKS, ,[Follow symlinks when processing in place])
fi      

对应到configure中

if test "x$have_lstat" = xyes -a "x$have_readlink" = xyes; then

cat >>confdefs.h <<\_ACEOF
#define ENABLE_FOLLOW_SYMLINKS /**/      

AC_DEFINE宏用来生成configure中名为ENABLE_FOLLOW_SYMLINKS的宏,在这段shell中,如果检测到have_lstat=yes同时have_readlink=yes,则定义宏ENABLE_FOLLOW_SYMLINKS。可是configure要ENABLE_FOLLOW_SYMLINKS这个宏干啥?继续往后看。

2.1)对于configure运行的结果,会保存到config.status文件中,来看下config.status文件

BEGIN {
...
D["HAVE_GETTEXT"]=" 1"
D["HAVE_DCGETTEXT"]=" 1"
D["ENABLE_FOLLOW_SYMLINKS"]=" /**/"
  for (key in D) D_is_set[key] = 1
  FS = ""
}
...
if (D_is_set[macro]) {
    # Preserve the white space surrounding the "#".
    print prefix "define", macro P[macro] D[macro]
    next
  } else {
    # Replace #undef with comments.  This is necessary, for example,
    # in the case of _POSIX_SOURCE, which is predefined and required
    # on some systems where configure will not decide to define it.
    if (defundef == "undef") {
      print "/*", prefix defundef, macro, "*/"
      next
    }
  }      

对configure中定义的宏全部添加到config.status中,并通过循环,测试这些宏的值。如果configure检测到某一项,则config.h中生成#define 宏名;如果configure没有检测到某一项,则在config.h中生成#undef 宏名,如下:

列表4:config.h部分内容

/* Define to the copyright year printed by --version. */
#define COPYRIGHT_YEAR 2009

/* Define to one of `_getb67', `GETB67', `getb67' for Cray-2 and Cray-YMP
   systems. This function is required for `alloca.c' support on those systems.
   */
/* #undef CRAY_STACKSEG_END */

/* Define to 1 if using `alloca.c'. */
/* #undef C_ALLOCA */ #configure过程中检测失败的用#undef定义

/* Define to 1 if // is a file system root distinct from /. */
/* #undef DOUBLE_SLASH_IS_DISTINCT_ROOT */

/* Follow symlinks when processing in place */
#define ENABLE_FOLLOW_SYMLINKS /**/

/* Define to 1 if translation of program messages to the user's native
   language is requested. */
#define ENABLE_NLS 1      

2.2)configure文件头部还会定义一堆变量,这堆变量在configure.in中找不到归宿,正如列表2中提到。通过grep -rn 变量名 搜索,会在makefile.in/makefile中找到这些变量名,如

列表5:makefile.in部分内容

#这部分内容正好弥补了configure.in中没有找到的变量
NEXT_AS_FIRST_DIRECTIVE_SYS_TIME_H = @NEXT_AS_FIRST_DIRECTIVE_SYS_TIME_H@
NEXT_AS_FIRST_DIRECTIVE_UNISTD_H = @NEXT_AS_FIRST_DIRECTIVE_UNISTD_H@
NEXT_AS_FIRST_DIRECTIVE_WCHAR_H = @NEXT_AS_FIRST_DIRECTIVE_WCHAR_H@
NEXT_AS_FIRST_DIRECTIVE_WCTYPE_H = @NEXT_AS_FIRST_DIRECTIVE_WCTYPE_H@
NEXT_ERRNO_H = @NEXT_ERRNO_H@
NEXT_STDINT_H = @NEXT_STDINT_H@
NEXT_STDIO_H = @NEXT_STDIO_H@      

看makefile.in中的格式,有点像变量赋值的形式,具体赋值在哪?都说了,这些变量定义在configure中,configure运行后的结果保存在config.status中,因此,要查看makefine.in中某个变量的值,可以去config.status中查找,比如

NEXT_AS_FIRST_DIRECTIVE_SYS_TIME_H = @NEXT_AS_FIRST_DIRECTIVE_SYS_TIME_H@      

在config.status中保存的值为:

列表6:config.status部分内容:

S["REPLACE_GETTIMEOFDAY"]="0"
S["HAVE_STRUCT_TIMEVAL"]="1"
S["HAVE_SYS_TIME_H"]="1"
S["NEXT_AS_FIRST_DIRECTIVE_SYS_TIME_H"]="<sys/time.h>"
S["NEXT_SYS_TIME_H"]="<sys/time.h>"
S["LTLIBINTL"]=""      

由此可知NEXT_AS_FIRST_DIRECTIVE_SYS_TIME_H值为<sys/time.h>

2.3)结论:

configure的运行结果保存到config.status中,其值用于生成config.h中的宏以及为makefile.in中的变量提供值。

3.既然知道config.h中保存了一堆宏定义,那么除了configure.ac还有谁同时规定了要检测这些宏?

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

configure不过是个执行者,肯定不会规划要生成哪些宏,而configure.in也作为configure的规划者,并没有把要生成宏全部写进自己的规划书中,比如:

列表7:

/* Follow symlinks when processing in place */
#define ENABLE_FOLLOW_SYMLINKS /**/

/* Define to 1 if translation of program messages to the user's native
   language is requested. */
#define ENABLE_NLS 1

/* Define on systems for which file names may have a so-called `drive letter'
   prefix, define this to compute the length of that prefix, including the
   colon. */      

宏定义:#define ENABLE_NLS 1在configure.in中就找不到,上图:

开源项目管理:使用automake 各组件的关联

由此可知,除了configure.in作为configure的规划者,一定有另一个规划者。还是通过grep -rn  " ENABLE_NLS"查找 ENABLE_NLS的出处。

很快在config_h.in中找到了英雄的出处:

列表8:config_h.in部分输出

/* Follow symlinks when processing in place */
#undef ENABLE_FOLLOW_SYMLINKS

/* Define to 1 if translation of program messages to the user's native
   language is requested. */
#undef ENABLE_NLS

/* Define on systems for which file names may have a so-called `drive letter'
   prefix, define this to compute the length of that prefix, including the
   colon. */
#undef FILE_SYSTEM_ACCEPTS_DRIVE_LETTER_PREFIX      

类似的,FILE_SYSTEM_ACCEPTS_DRIVE_LETTER_PREFIX也在configure.in中找不到出处,但是可以在config_h.in中找到出处

3.1)结论,config_h.in文件初始化一部分需要生成的宏,同过configure检测,则在config.h中输出#define 宏名,如果检测失败,输出结果为#undef 宏名

4.makefile的生成

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

全篇的目的是生成makefile,前面这么多也是为他铺垫。

makefile指定了编译的文件,以及编译的规则。但是倒推回去,谁指定了这些规则,configure里看不到一行编译相关的内容(编译选项除外),剩下的只有makefile.am和makefile.in了。

开源项目的顶层目录中的makefile.am只指定了需要编译递归那些子目录,而子目录下的makefile.am则指定了需要编译的文件,以及依赖关系,编译/连接选项相关的变量,如图

开源项目管理:使用automake 各组件的关联

这是顶层目录的makefile.in

下面是子目录的:

开源项目管理:使用automake 各组件的关联

其中编译相关的变量可以在makefile.am和config.status中找到原型:

开源项目管理:使用automake 各组件的关联
开源项目管理:使用automake 各组件的关联

最后,经过automake的加工,这些输入模板文件中的内容全部添加到输出文件--makefile中:

图示:makefile中的变量

开源项目管理:使用automake 各组件的关联

图示makefile与makefile.in编译方式的异同

开源项目管理:使用automake 各组件的关联

因此,如果需要往开源项目中添加文件,最好在makefile.am中添加。但是有时会发生修改makefile.am后automake失败的情况,无奈只能到makefile.in中修改。毕竟按文档,makefile.in是用来生成makefile的模版文件。

makefile.in中有啥?

列表9:部分makefile.in输出:

CC = @CC@
CCDEPMODE = @CCDEPMODE@
CFLAGS = @CFLAGS@
COPYRIGHT_YEAR = @COPYRIGHT_YEAR@
CPP = @CPP@
CPPFLAGS = @CPPFLAGS@      

都说了,makefile.in中指定的变量值来自config.status的生成值,那么回到config.status中查看这部分变量的值:

列表10:还是部分config.status的输出:

S["DEPDIR"]=".deps"
S["OBJEXT"]="o"
S["EXEEXT"]=""
S["ac_ct_CC"]="gcc"
S["CPPFLAGS"]=""
S["LDFLAGS"]=""
S["CFLAGS"]="-g -O2"
S["CC"]="gcc"
S["COPYRIGHT_YEAR"]="2009"      

看到没,编译连接相关的变量值在此!经过automake,把config.status中的值装入到模版文件makefile.in,生成makefile,如下:

BITSIZEOF_WCHAR_T = 
BITSIZEOF_WINT_T = 
CC = gcc
CCDEPMODE = depmode=gcc3
CFLAGS = -g -O2
COPYRIGHT_YEAR = 2009
CPP = gcc -E
CPPFLAGS = 
CYGPATH_W = echo      

此外,各个子目录下的makefile.in还指定了生成makefile源码的方式。