《Autotools - GNU Autoconf, Automake与Libtool实践者指南》第三章<用Autoconf配置你的项目>

93 篇文章 1 订阅
3 篇文章 0 订阅

因为对于原本的Autoconf框架,Automake和Libtool本质上是追加的组件,花费一些时间使用Autoconf而不使用Automake和Libtool是有用的。通过暴露这个工具的那些经常被Automake隐藏的部分,提供给你关于Autoconf如何运作相当多的见解。

  在Automake出现之前,Autoconf是被单独使用的。实际上,很多遗留的开源软件项目从没有做从Autoconf到全GNU Autotools工具集的转型。结果,在较早的开源软件工程中找到一个称为configure.in(Autoconf最初命名约定)的文件和手写的Makefile.in模板,并不是不寻常的事。

  在这一章中,我会向你展示如何添加一个Autoconf编译系统到一个存在的项目。我会花费这章节的绝大多数来讨论Autoconf的基础特性,在第四章,我会进入更深的细节,关于一些更为复杂的Autoconf宏是如何工作的,和如何恰当地使用它们。在此过程中,我会继续使用Jupiter项目作为我们的例子。


Autoconf配置脚本


  autoconf程序的输入时shell脚本,里面布满宏调用。输入流必须包含所有引用宏的定义---同时包括那些Autoconf提供的和那些你自己编写的。

  在Autoconf宏中使用的宏语言被称为M4。m4工具是一种通用宏语言预处理器,最初是由Brian Kernighan和Dennis Ritchie在1977年编写。

  Autoconf依赖于相对少的工具而存在:Bourne shellM4Perl解析器。它生成的配置脚本和make文件依赖于一个不同的工具集而存在:包括Bourne shellgreplssed或awk

  注意:别让Autotools的需求和它们所生成的脚本与make文件的需求混淆了。Autotools维护者的工具,然而所生成的脚本和make文件最终用户的工具。我们可以在开发系统上适度地希望一个相比最终用户系统更高版本的安装功能。

  配置脚本确保最终用户的编译环境被合适地配置为适合编译你的项目。这一脚本检查安装的工具、程序、库和头文件,同时包括这些资源内的特定功能。Autoconf从其它工程配置框架中区别开来的原因是,Autoconf测试确保这些资源可以被你的项目恰当地使用。重要的不仅仅是你的用户具有库libxyz.so和公共头文件被安装在他们的系统上,他们具有这些文件的正确版本也是很重要的。Autoconf病态地包含这样的测试。它确保最终用户的环境是按照项目需求的,通过为每个功能编译和链接一个小的测试程序---一个典型的例子,做你的代码在一个大尺度上运行的那样的事。

  能不能通过在搜索库函数路径查找文件名的方式来确保特定版本的库是被安装了的呢?这一问题的答案是有争议的。不依赖库版本号的最重要原因是,它们并不能代表一个库的特定发行版本。如我们会在第七章中讨论的,库版本号表示在一个特定平台上的二进制接口符号。这意味着相同功能集的库版本号在不同平台间可以是不同的,你可能并不能说一个特定的库是否具有你项目所需要的功能。

  Autoconf提供了很多宏可以确定Autoconf的功能测试理念。你应该仔细学习,并使用可用的宏列表,而不是自己去写,因为它们被特别的设计以确保需要的功能在广泛的系统和平台上是可用的。


最短的configure.ac文件


  可能最简单的configure.ac文件只有两行,如列表3-1所示。

  1. AC_INIT([Jupiter], [1.0])  
  2. AC_OUTPUT  

列表3-1: 最简单的configure.ac文件

  那些对Autoconf比较默认的人来说,这两行看上去是一对函数调用,可能是有些晦涩的编程语言的语法。实际上它们是M4宏调用。这些宏定义在分布于autoconf软件包的文件中。例如,你可以在Autoconf安装目录的autoconf/general.m4文件中找到AC_INIT的定义(通常是/usr/(local/)share/autoconf)。AC_OUTPUT定义于autoconf/status.m4。


比较M4与C预处理器


  M4宏与定义于C语言代码文件中的C预处理器宏在很多方面非常类似。C预处理器也是个文本替换工具,这并不奇怪:M4和C预处理器都是由Kernighan和Ritchie在相同的时期设计和编写。


  Autoconf在宏参数周围使用方括号作为一种引用机制。在宏调用内容会引起多义性从而使宏处理器错误解析(不会告诉你)的情况下,引用才是必须的。我们将在第十章更加细致讨论M4引用。现在,只需要在每个参数周围用方括号,以确保期望的宏扩展被生成


  像C预处理器宏那样,你可以定义M4宏来接受一个括号中的用逗号分隔的参数。然而,一个重要的不同是,在M4中,参数化宏参数是可选的,调用者可以简单的省略它们。如果没有传递参数,你也可以忽略括号,传递给M4宏的额外参数简单地被忽略。最后,M4不允许在一个宏调用的宏名与开括号之间插入空格。


执行autoconf


  执行autoconf是简单的:只要在与configure.ac相同目录下执行它。虽然我可以为在这章中的每个例子这么做,但是我将会运行与autoconf相同效果的autoreconf,除了autoreconf会在你开始添加Automake和Libtool功能到你的编译系统时会去做正确的事。那就是,它会基于你configure.ac文件的内容,以正确的顺序执行所有的Autotools。


  autoreconf足够聪明来执行你需要的工具,以你需要它们的顺序,用你想要的选项。因此,运行autoreconf执行Autotools工具链的推荐方法


  让我们以添加一个来自列表3-1的简单configure.ac文件到你的工程目录作为开始。一旦你添加configure.ac到顶层目录,运行autoreconf:

  1. $ autoreconf  
  2. $  
  3. $ ls -1p  
  4. autom4te.cache/  
  5. configure  
  6. configure.ac  
  7. Makefile  
  8. src/  
  9. $  
  首先,注意autoreconf默认安静地运行。如果你想要看见发生的事情,使用-v--verbose选项。如果你想要autoreconf也以详细模式执行Autotools,添加-vv到命令行。


  接着,注意autoconf创建了一个称为autom4te.cache的目录。这是autom4te缓存目录。这个缓存在Autotools工具链中工具连续执行期间,加速访问configure.ac。


  通过autoconf的configure.ac的结果本质上是个相同的文件(称为configure),但是所有的宏完全被展开。经过M4宏的扩展,configure.ac已经被转换成一个包含几千行Bourne shell脚本文本文件


执行configure


  GNU编码标准指出,一个手写的configure脚本应该生成另一个称为config.status的脚本,它的工作是从模板生成文件。不奇怪,这正是那种你会在Autoconf生成的配置脚本里会发现的功能。这个脚本有两个主要的任务
 > 执行请求的检查;
 > 生成和然后调用config.status;

  configure执行检查的结果写入config.status,以一种允许被用作替换文本的方式,为模板文件(Makefile.in,config.h.in等等)中的Autoconf替换变量所替换。当你执行configure,它告诉你它正创建config.status。它也创建一个称为config.log具有重要属性的日志文件。让我们运行configure,然后看看我们项目目录中有什么新的。

  1. $ ./configure  
  2. configure: creating ./config.status  
  3. $  
  4. $ ls -1p  
  5. autom4te.cache/  
  6. config.log  
  7. config.status  
  8. configure  
  9. configure.ac  
  10. Makefile  
  11. src/  
  12. $  
  我们看到configure确实同时生成了config.status和config.log。config.log文件包含下列信息:
 > 用于调用configure的命令行(非常方便);
 > configure执行的平台信息
 > configure执行的核心测试信息
 > configure中生成和调用config.status的行号

  日志文件中到这点,config.status接过来生成日志信息,添加了下列信息:
 > 用于调用config.status的命令行

  在config.status从它们的模板生成所有的文件之后,它退出,返回控制到configure,后者附加下列信息到日志:
 > config.status执行任务所用的缓存变量
 > 可能会在模板中被替换的输出变量列表
 > configure返回到shell的退出码

  当调试一个configure脚本和它相关联的configure.ac文件时,这个信息是非常宝贵的。

  为何configure不直接执行写到config.status中的代码,而是生成第二个脚本,只是立即去调用它?有一些好的理由。首先,执行检查和生成文件的操作在概念上是不同的,make在概念不同的操作用分开的make目标相联系时,工作最佳。第二个理由是,你可以独立执行config.status来从它们相应的模板文件生成输出文件,节约了需要执行这些冗长检查的时间。最后,config.status记住了最初用于执行configure的命令行参数。因此,当make检测到它需要更新编译系统,它会调用config.status来重新执行configure,使用我们最初指定的命令行选项。


执行config.status


  现在你知道configure如何工作,你可能会忍不住去执行config.status。这确实是Autoconf设计者和GCS作者们的意图,他们最初构思了这些设计目标。然而,从模板处理分开检查的一个重要原因是,make规则可以使用config.status从它们的模板生成make文件,当make确定一个模板比它相应的make文件新的时候。

  make文件规则应该被写为表明输出文件是独立于它们的模板的,不是调用configure来执行不必要的检查。为这些规则运行config.status的命令,将规则的目标作为一个参数来传递。例如,如果你修改了其中一个Makefile.in模板,make调用config.status来重新生成相应的Makefile,在此之后,make重新执行他原来的命令行---基本上是重启本身。

  列表3-2显示了这样一个Makefile.in模板的相关部分,包含重新生成相应Makefile所需要的规则。

  1. ...  
  2. Makefile: Makefile.in config.status  
  3.   ./config.status $@  
  4. ...  

列表3-2 一个如果模板变化会重新生成Makefile的规则


  如果没有给出特定目标,它会在用户特定目标或默认目标之前执行。


   既然config.status本生是一个生成的文件,按理说你可以写这样一个规则在需要时重新生成这个文件。扩展前面的例子,列表3-3添加了在configure变化时需要重建config.status的代码。

  1. ...  
  2. Makefile: Makefile.in config.status  
  3.   ./config.status $@  
  4. config.status: configure  
  5.   ./config.status --recheck  
  6. ...  
列表3-3 当configure变化时重建config.status的规则


添加一些真实的功能


  我已建议你应该在你的make文件中调用config.status来从模板生成那些make文件。列表3-4显示了configure.ac中使得它发生的代码。

  1. AC_INIT([Jupiter],[1.0])  
  2. AC_CONFIG_FILES([Makefile src/Makefile])  
  3. AC_OUTPUT  

列表3-4 configure.ac: 使用AC_CONFIG_FILES宏


  这个代码假设存在为Makefile和src/Makefile的模板,分别称为Makefile.in和src/Makefile.in。这些模板与它们对应的Makefile非常像,有一个例外:任何我想要Autoconf替换的文本,被标记为Autoconf替代变量,使用@VARIABLE@语法。

  为了创建这些文件,简单的重命名存在Makefile文件到Makfile.in,同时在顶层和src目录里。这是一个普通的惯例。

  1. $ mv Makefile Makefile.in  
  2. $ mv src/Makefile src/Makefile.in  
  3. $  
  接下来,让我们添加一些Autoconf替换变量来替换原来的默认值。在这些文件的顶部,我也添加了Autoconf替换变量,@configure_input@,在评论标志之后。列表3-5显示了在Makefile中生成的评论文本。
  1. # Makefile. Generated from Makefile.in by configure.  
  2. ...  

列表3-5 Makefile: 从Autoconf @configure_input@变量生成的文本

  1. # @configure_input@  
  2. # Package-specific substitution variables  
  3. package = @PACKAGE_NAME@  
  4. version = @PACKAGE_VERSION@  
  5. tarname = @PACKAGE_TARNAME@  
  6. distdir = $(tarname)-$(version)  
  7. # Prefix-specific substitution variables  
  8. prefix = @prefix@  
  9. exec_prefix = @exec_prefix@  
  10. bindir = @bindir@  
  11. ...  
  12. $(distdir): FORCE  
  13.   mkdir -p $(distdir)/src  
  14.   cp configure.ac $(distdir)  
  15.   cp configure $(distdir)  
  16.   cp Makefile.in $(distdir)  
  17.   cp src/Makefile.in $(distdir)/src  
  18.   cp src/main.c $(distdir)/src  
  19. distcheck: $(distdir).tar.gz  
  20.   gzip -cd $(distdir).tar.gz | tar xvf -  
  21.   cd $(distdir) && ./configure  
  22.   cd $(distdir) && $(MAKE) all  
  23.   cd $(distdir) && $(MAKE) check  
  24.   cd $(distdir) && $(MAKE) DESTDIR=$${PWD}/_inst install  
  25.   cd $(distdir) && $(MAKE) DESTDIR=$${PWD}/_inst uninstall  
  26.   @remaining="`find $${PWD}/$(distdir)/_inst -type f | wc -l`"; \  
  27.   if test "$${remaining}" -ne 0; then \  
  28.     echo "*** $${remaining} file(s) remaining in stage directory!"; \  
  29.     exit 1; \  
  30.   fi  
  31.   cd $(distdir) && $(MAKE) clean  
  32.   rm -rf $(distdir)  
  33.   @echo "*** Package $(distdir).tar.gz is ready for distribution."  
  34. Makefile: Makefile.in config.status  
  35.   ./config.status $@  
  36. config.status: configure  
  37.   ./config.status --recheck  
  38. ...  

列表3-6 Makefile.in: 来自第二章最后的Makefile所需修改

  1. # @configure_input@  
  2. # Package-specific substitution variables  
  3. package = @PACKAGE_NAME@  
  4. version = @PACKAGE_VERSION@  
  5. tarname = @PACKAGE_TARNAME@  
  6. distdir = $(tarname)-$(version)  
  7. # Prefix-specific substitution variables  
  8. prefix = @prefix@  
  9. exec_prefix = @exec_prefix@  
  10. bindir = @bindir@  
  11. ...  
  12. Makefile: Makefile.in ../config.status  
  13.   cd .. && ./config.status src/$@  
  14. ../config.status: ../configure  
  15.   cd .. && ./config.status --recheck  
  16. ...  

列表3-7 src/Makefile.in: 来自第二章最后的src/Makefile所需修改

  我已在顶层Makefile.in中移除输出声明,添加了一份所有make变量的拷贝到src/Makefile.in。这么做的主要优势是我可以在任何子目录运行make,不用担心未初始化的变量,这些变量最初是由较高层make文件传递下来的。


从模板生成文件


  注意,你可以使用AC_CONFIG_FILES来生成任何文本文件,从一个相同目录下的带.in扩展的相同名字的文件。

  Autoconf生成sed或awk表达式到结果configure脚本,然后将它们考本到config.status。config.status脚本使用这些表达在输入模板文件中执行字符替换。

  sed和awk都是操作在文件流上的文本处理工具。一个流编辑器的优势是它以字节流的形式替换文本。因此,sed和awk都可以在大文件上进行操作,因为它们为了处理不必加载整个输入文件到内存中去。Autoconf构建了表达列表,config.status从一个有多个宏定义的变量列表传递给sed或awk,其中很多我将会在本章接下来的部分详细涉及。需要明白的很重要一点是,Autoconf替换变量是唯一在生成输出文件时在模板文件中要替换的项目。

  到此,没花费多少努力,我已创建了一个基本的configure.ac文件。现在我可以执行autoreconf,接着是configure和make,为的是编译Jupiter项目。这是简单的,三行的configure.ac文件生成了一个全功能的configure脚本,根据GCS定义的恰当配置脚本的定义。

  由此产生的配置脚本运行多个系统检查,生成一个config.status脚本。config.status脚本可以在这个编译系统中的一个特定模板文件集中替换相当数量的替换变量。以三行的代码来说,那是很多的功能了。


添加VPATH构建功能


  在第二章的最后,我提到我尚未涉及一个重要的概念---那就是VPATH构建。一个VPATH构建是一种使用一个makefile结构体在一个目录(不是源码目录)中配置和构建一个项目的方式。如果你需要执行下列任何任务,这是很重要的:

 > 维护一个独立地调试配置;
 > 一起测试不同的配置;
 > 在本地修改后,为patch diff保持一个赶紧的源码目录;
 > 从一个只读源码目录构建。


  VPATH是虚拟搜索路径(virtual search path)的缩写。VPATH声明包含一个用冒号分割的位置列表,用于在相对当前路径找不到时寻找依赖的相对路径。换句话说,当make在当前路径找不到一个文件时,它会在VPATH中声明的每个路径中顺序寻找那个文件。

  使用VPATH来添加一个远程编译功能到一个现存make文件是非常简单的。李表3-8显示了一个在Makefile中使用VPATH声明的例子。

  1. VPATH = some/path:some/other/path:yet/another/path  
  2. program: src/main.c  
  3. $(CC) ...  

列表3-8 在Makefile中使用VPATH的一个例子

  列表3-9和列表3-10显示了对工程中两个make文件的必要修改。

  1. ...  
  2. # VPATH-specific substitution variables  
  3. srcdir = @srcdir@  
  4. VPATH = @srcdir@  
  5. ...  
  6. $(distdir): FORCE  
  7.   mkdir -p $(distdir)/src  
  8.   cp $(srcdir)/configure.ac $(distdir)  
  9.   cp $(srcdir)/configure $(distdir)  
  10.   cp $(srcdir)/Makefile.in $(distdir)  
  11.   cp $(srcdir)/src/Makefile.in $(distdir)/src  
  12.   cp $(srcdir)/src/main.c $(distdir)/src  
  13. ...  

列表3-9 Makefile.in: 添加VPATH构建能力到顶层make文件
  1. ...  
  2. # VPATH-related substitution variables  
  3. srcdir = @srcdir@  
  4. VPATH = @srcdir@  
  5. ...  
列表3-10 src/Makefile.in: 添加VPATH构建能力到较低层make文件

  在你的构建系统中支持远程构建所需要的修改,总结如下:
 > 设置一个make变量,srcdir,到@srcdir@替换变量;
 > 设置VPATH变量到@srcdir@;
 > 在命令行用$(srcdir)/指定所有文件使用的依赖;


  注意:别在你的VPATH声明本身中使用$(srcdir),因为较早版本的make不会替换VPATH中声明的变量引用。


  如果源码目录与构建目录相同,@srcdir@替换变量退化为一个点(.)。意思是所有的$(srcdir)/指定简单地退化为./,这并没有坏处。


  一个快速的例子是最容易向你展示这是如何工作的。现在Jupiter全功能支持远程构建,让我梦给它试试。开始于Jupiter工程目录,创建一个称为build的子目录,然后进入那个目录。使用一个相对路径执行configure脚本,然后列出当前目录内容:

  1. $ mkdir build  
  2. $ cd build  
  3. $ ../configure  
  4. configure: creating ./config.status  
  5. config.status: creating Makefile  
  6. config.status: creating src/Makefile  
  7. $  
  8. $ ls -1p  
  9. config.log  
  10. config.status  
  11. Makefile  
  12. src/  
  13. $  
  14. $ ls -1p src  
  15. Makefile  
  16. $  
  整个构建系统已经被build子目录中的configure和config.status构建。在build目录中输入make来构建工程。
  1. $ make  
  2. cd src && make all  
  3. make[1]: Entering directory '../prj/jupiter/build'  
  4. gcc -g -O2 -o jupiter ../../src/main.c  
  5. make[1]: Leaving directory '../prj/jupiter/build'  
  6. $  
  7. $ ls -1p src  
  8. jupiter  
  9. Makefile  
  10. $  
  无论你在哪,如果你使用一个相对或绝对路径访问工程目录,你可以在那个位置做一个远程构建。那只是Autoconf生成的配置脚本为你做的更多的一件事情。

  创建一个几乎完整的configure.ac文件最简单的方法是运行autoscan工具,它是autoconf软件包的一部分。这一工具检查一个工程目录的内容,使用存在的make文件和源码文件生成configure.ac文件的基础(autoscanf将其命名为configure.scan)。

  让我们看看autoscanf在Jupiter项目上会做什么。首先,我会清理来自前期实验的遗留物,然后在jupiter目录运行autoscan。注意,我不会删除我原本的configure.ac文件---我只是让autoscanf告诉我如何提高它。

  1. $ rm -rf autom4te.cache build  
  2. $ rm configure config.* Makefile src/Makefile src/jupiter  
  3. $ ls -1p  
  4. configure.ac  
  5. Makefile.in  
  6. src/  
  7. $  
  8. $ autoscan  
  9. configure.ac: warning: missing AC_CHECK_HEADERS([stdlib.h]) wanted by: src/main.c:2  
  10. configure.ac: warning: missing AC_PREREQ wanted by: autoscan  
  11. configure.ac: warning: missing AC_PROG_CC wanted by: src/main.c  
  12. configure.ac: warning: missing AC_PROG_INSTALL wanted by: Makefile.in:18  
  13. $  
  14. $ ls -1p  
  15. autom4te.cache/  
  16. autoscan.log  
  17. configure.ac  
  18. configure.scan  
  19. Makefile.in  
  20. src/  
  21. $  
   autoscan工具检查项目目录等级,创建了两个称为configure.scan和autoscan.log的文件。项目可能还没有安装Autotools---这没有关系,因为autoscan绝对无损。它绝不会修改工程中任何存在的文件。

  autoscan工具为在现存configure.ac中的每个问题生成一个警告。在此例子中,autoscan注意到configure.ac应该使用Autoconf提供的AC_CHECK_HEADERS,AC_PREREQ,AC_PROG_CC和AC_PROG_INSTALL宏。它做的假设是基于现存Makefile.in模板和C语言源码文件中的信息。你可以通过查看autoscan.log看到这些信息(更为详细)。


  查看生成的configure.scan文件,我注意到相比我原来的configure.ac,autoscan已近添加更多文本到这个文件。在我查看完之后,确保我理解了所有,我看到用configre.scan重写configure.ac可能是最简单的,然后修改少许专用于Jupiter的信息。

  1. $ mv configure.scan configure.ac  
  2. $ cat configure.ac  
  3. # -*- Autoconf -*-  
  4. # Process this file with autoconf to produce a configure script.  
  5.   
  6. AC_PREREQ([2.64])  
  7. AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])  
  8. AC_CONFIG_SRCDIR([src/main.c])  
  9. AC_CONFIG_HEADERS([config.h])  
  10.   
  11. # Checks for programs.  
  12. AC_PROG_CC  
  13. AC_PROG_INSTALL  
  14.   
  15. # Checks for libraries.  
  16.   
  17. # Checks for header files.  
  18. AC_CHECK_HEADERS([stdlib.h])  
  19.   
  20. # Checks for typedefs, structures, and compiler characteristics.  
  21.   
  22. # Checks for library functions.  
  23. AC_CONFIG_FILES([Makefile  
  24. src/Makefile])  
  25. AC_OUTPUT  
  26. $  
  我第一个修改是Jupiter的AC_INIT宏参数的修改,如列表3-11中所示。

  1. # -*- Autoconf -*-  
  2. # Process this file with autoconf to produce a configure script.  
  3.   
  4. AC_PREREQ([2.64])  
  5. AC_INIT([Jupiter], [1.0], [jupiter-bugs@example.org])  
  6. AC_CONFIG_SRCDIR([src/main.c])  
  7. AC_CONFIG_HEADERS([config.h])  
  8. ...  

列表3-22 configure.ac: 调整autoscan生成的AC_INIT宏


  autoscan工具为你做了大量工作。GNU Autoconf手册声明在你使用它之前,你应该修改这个文件来符合你项目的需求,但是只有一些关键的问题需要考虑(除了那些AC_INIT相关的)。我将返回来介绍每一个问题,但首先,让我们看一些管理细节。


众所周知的autogen.sh脚本


  在autoscan出现之前,维护者传递一份简短的shell脚本,常称为autogen.sh或bootstrap.sh,它会为它们的工程以恰当地顺序运行所有需要的Autotools。autogen.sh脚本可以是非常古怪的,但是为了解决install-sh脚本确实的问题,我会添加一个简单的临时autogen.sh脚本到工程根目录,如列表3-12中所示。

  1. #!/bin/sh  
  2. autoreconf --install  
  3. automake --add-missing --copy >/dev/null 2>&1  

列表3-12 autogen.sh: 执行所需Autotools的临时引导脚本


  automake的--add-missing选项拷贝需要的丢失工具脚本到项目,--copy选项表明真实的拷贝应该做(否则,指向安装位置文件的符号链接被创建)。


  我们不需要看到来自automake的警告,因此我已在这个脚本中的automake命令行上,重定向stderr和stdout到/dev/null。在第五张,我们会移除autogen.sh,简单地运行autoreconf --install,但是对于现在,这会解决我们丢失文件的问题。


更行Makefile.in


  让我们执行autoge.sh,看看:

  1. $ sh autogen.sh  
  2. $ ls -1p  
  3. autogen.sh  
  4. autom4te.cache/  
  5. config.h.in  
  6. configure  
  7. configure.ac  
  8. install-sh  
  9. Makefile.in  
  10. src/  
  11. $  
  从列表中可以看到,config.h.in已经被创建,因此我们知道autoreconf已经执行了autoheader。我们也看到当我们执行autogen.sh中的automake时,新的install-sh脚本被创建。任何由Autotools提供或生成的东西应该被拷贝进档案目录,从而使它被发布的压缩包附带。因此,我们会为这两个文件添加cp命令到顶层Makefile.in模板中的$(distdir)目标。注意,我们不需要拷贝autogen.sh脚本,因为它纯粹是维护者的工具---用户绝不需要在一个发布版里执行它。


  列表3-13显示了顶层Makefile.in模板中的修改。

  1. ...  
  2. $(distdir): FORCE  
  3.   mkdir -p $(distdir)/src  
  4.   cp $(srcdir)/configure.ac $(distdir)  
  5.   cp $(srcdir)/configure $(distdir)  
  6.   cp $(srcdir)/config.h.in $(distdir)  
  7.   cp $(srcdir)/install-sh $(distdir)  
  8.   cp $(srcdir)/Makefile.in $(distdir)  
  9.   cp $(srcdir)/src/Makefile.in $(distdir)/src  
  10.   cp $(srcdir)/src/main.c $(distdir)/src  
  11. ...  

列表 3-13 Makefile.in: 在发布版档案镜像目录中需要添加的文件

初始化和软件包信息


  现在把我们的注意回到列表3-11中configure.ac中的内容。第一部分包含Autoconf初始化宏。这些对于所有的项目是必须的。让我们独立地考虑每一个宏,因为它们都很重要。


AC_PREREQ


  AC_PREREQ宏简单地定义了可能成功被用于处理这个configure.ac文件的Autoconf的最早版本。

  1. AC_PREREQ(version)  
  GNU Autoconf手册表明AC_PREREQ是唯一一个可以在AC_INIT之前使用的宏。这是因为它好确保你正使用一个足够新的Autoconf版本,在你开始处理任何其它宏之前,它们可能是版本依赖的。

AC_INIT


  AC_INIT宏如它名字所暗示,初始化Autoconf系统。这是它的原型,同在GNU Autoconf手册中所定义的。

  1. AC_INIT(package, version, [bug-report], [tarname], [url])  
  它最多接受五个参数(autoscan只会生成带前三个的调用):package,version,和可选的,bug-report,tarname和url。package参数被规定为软件包的名称。当你执行make dist时,它会作为一个Automake生成的发布版名称的第一部分。
  

  注意:Autoconf在压缩包命名中的软件包名使用一种标准化的形式,因此如果你需要,你可以在软件包名上使用大写字母。Automake生成的压缩包被默认命名为tarname-version.tar.gz,但是tarname被设置为一种标准化的软件包名(小写,所有标点被转化为下划线)。 记住这一点,当你选择你的软件包名和版本字符时。


  可选的bug-report参数通常设置为一个E-Mail地址,但是任何文本字符串都是有效的。一个称为@PACKAGE_BUGREPORT@的Autoconf替换变量为它创建,那个变量也被添加进config.h.in模板作为C预处理定义。这里的目的是在你的代码中使用变量,在合适的位置呈现一个电邮地址用于报告BUG,通常是用户在你的应用中请求帮助或版本信息。

  虽然版本参数可以是任何你虚幻的,有一些通用OSS(Open Source Software)惯例会让事情变得一些简单。最为广泛使用的惯例是传递marjor.minor(例如,1.2)。然而,没有什么能说你不能使用marjor.minor.reversion,使用这种方法也没有错误。没有一个最终的VERSION变量会在任何地方被解析或分析---它们只是在多种位置中被用作替换文本的占位符。因此,如果你喜欢,你甚至可以添加非数字文本到这个宏,例如0.15.alphal,有时这是有用的。

  注意:另一方面,RPM软件包管理器关心你放在版本字符串中的内容。为了RPM,你可能会希望限制版本字符串文本到只有字母数字字符和时间---没有破折号或下划线。

  可选的url参数应该是你的项目网站的URL。它通过configure --help在帮组文本中被显示。

  Autoconf从AC_INIT的参数生成替换变量@PACKAGE_NAME@,@PACKAGE_VERSION@,@PACKAGE_TARNAME@,@PACKAGE_STRING@ (软件包名和版本信息的一个程式化串联),@PACKAGE_BUGREPORT@ 和 @PACKAGE_URL@。


AC_CONFIG_SRCDIR


  AC_CONFIG_SRCDIR宏是一个明智的检查。它的目的是确保生成的configure脚本知道它所执行的目录是否实际上是项目的目录。

  更为详细的说,configure需要能够定位自己,因为它生成自我执行的代码,可能是来自一个远程目录。有无数的方式非故意地欺骗configure寻找一些其它的configure脚本。例如,用户可能偶然地提供一个不正确的--srcdir参数到configure。$0 shell脚本参数是不可靠的,最好的情况下---它可能包含shell的名称,而不是脚本,或者它是在系统搜寻目录中找到的configure,因此没有路径信息是在命令行中所指定的。


  configure脚本可以尝试在当前或父目录中查找,但是任然需要一种确认configure脚本自身位置的方式。因此,AC_CONFIG_SRCDIR给予configure一个在正确位置查找的重要提示。这里是AC_CONFIG_SRCDIR的原型:

  1. AC_CONFIG_SRCDIR(unique-file-in-source-dir)  
  这个参数可以是一个到你喜欢的任何源码的路径(相对项目的configure脚本)。你应该选择一个对于你的项目来说是独特的,从而使configure错误地认为其它项目的配置脚本是它本生的可能性最小化。我会尝试选着一个那种代表项目的文件,例如一个以定义项目特性命名的源文件。那样的话,在我决定整理源码的时候,我不太可能迷失在一个文件名的重命名。但这不要紧,因为autoconf和configure都会告诉你和你的用户,如果它找不到这个文件的话。

实例化宏


  在我们投入到AC_CONFIG_HEADERS的细节之前,我想花些时间在Autoconf提供的文件生成框架上。从一个高层视角,在configure.ac中有四件主要的事会发生。
 > 初始化;
 > 检查请求处理;
 > 文件实例化请求处理;
 > configure脚本的生成;


  我们已涉及过初始化---没有多的差别,尽管你应该知道一些更多的宏。查阅GNU Autoconf手册获取更多信息---例如,查找AC_COPYRIGHT。现在让我们继续看文件的实例化。


  实际上,有四个所谓的实例化宏:AC_CONFIG_FILES,AC_CONFIG_HEADERS,AC_CONFIG_COMMANDS和AC_CONFIG_LINKS。一个实例化的宏接受一个关键词或文件的列表;configure会根据包含Autoconf替换变量的模板生成这些文件。 

  注意:你可能在你的configure.scan版本中需要修改AC_CONFIG_HEADER(单数)的名字为AC_CONFIG_HEADERS(复数)。单数的版本是这个宏的较早名字,比新版本的功能要少。

  这四个实例化宏具有一个有趣的公共签名。下列原型可被用于代表它们中的每一个,用合适的文本替换这个宏名的XXX部分:

  1. AC_CONFIG_XXXS(tag..., [commands], [init-cmds])  
  对于这四个宏的每一个,参数具有OUT[:INLIST]的形式,INLIST具有IN0[:IN1:...:INn]的形式。经常地,你会看见这些宏其中之一的一个调用,只有一个参数,如下面三个例子(注意,这些例子代表了宏调用,而不是原型,因此,方括号实际上是Autoconf引用,而不是表示可选参数):
  1. AC_CONFIG_HEADERS([config.h])  
  在这个例子中,config.h是上述规则的OUT部分。INLIST的默认值是带.in的OUT部分。因此,换句话说,前面的调用实际上等价于:
  1. AC_CONFIG_HEADERS([config.h:config.h.in])  
  这意味着config.status包含的shell代码会从config.h.in生成config.h,在过程中替换所有的Autoconf变量。你也可能会在INLIST部分提供一个输入文件的列表。在这种情况下,INLIST中的文件会被串联,形成结果OUT文件:
  1. AC_CONFIG_HEADERS([config.h:cfg0:cfg1:cfg2])  
  这里,config.status在替换所有的Autoconf变量之后,通过串联cfg0、cfg1和cfg2(以那种顺序)生成config.h。GNU Autoconf引用这个完整的OUT[:INLIST]结构体作为标记。这个参数的主要目的是提供一种命令行目标名---非常像makefile目标。它也可以被用作一个文件系统名,如果相关的宏生成文件,AC_CONFIG_HEADERS,AC_CONFIG_FILES和AC_CONFIG_LINKS同样的情况。


  但是AC_CONFIG_COMMANDS是独特的,因为它不生成任何文件。反而,它运行任意的shell代码,由用户在宏参数中指定。GNU Autoconf手册以下述形式引用它:

  1. $ ./config.status config.h  
  config.status命令行基于configure.ac中的AC_CONFIG_HEADERS宏调用会重新生成config.h文件。它只会重新生成config.h。


  输入./config.status --help来查看在你执行config.status时可以使用的其它命令行选项:

  1. $ ./config.status --help  
  2. 'config.status' instantiates files from templates according to the current configuration.  
  3.   
  4. Usage: ./config.status [OPTION]... [TAG]...  
  5. -h, --help           print this help, then exit  
  6. -V, --version    print version number and configuration settings, then exit  
  7. -q, --quiet, --silent  
  8.                      do not print progress messages  
  9. -d, --debug          don't remove temporary files  
  10.     --recheck        update config.status by reconfiguring in the same conditions  
  11.     --file=FILE[:TEMPLATE]  
  12.                      instantiate the configuration file FILE  
  13.     --header=FILE[:TEMPLATE]  
  14.                      instantiate the configuration header FILE  
  15.   
  16. Configuration files:  
  17. Makefile src/Makefile  
  18.   
  19. Configuration headers:  
  20. config.h  
  21. Report bugs to <bug-autoconf@gnu.org>.  
  22. $  
  注意,config.status提供关于一个项目的config.status的定制的帮助。它列出可用作命令行上标志的配置文件和配置头文件,在usage中指定为[TAG]...。在这种情况下,config.status只会实例化指定目标。

  这些宏中的每一个都可能会在configure.ac中被使用多次。结果是累积的,我们可以在configure.ac中使用AC_CONFIG_FILES我们所需要的次数。同样重要的是要注意config.status支持--file=选项。当你在命令行中使用标记来调用config.status,你唯一可以使用的标记是那些帮助文本所列出的可用的配置文件,头文件,链接和命令。当你使用--file=选项执行config.status时,你可以告诉config.status来生成一个新的文件,它没有与任何configure.ac中可以找到的实例化宏的调用相关联。这个新的文件根据一个使用配置选项的相关模板和由最后一次configure执行决定的检查结果生成。例如,我可以以这种方式执行config.status:
  1. $ ./config.status --file=extra:extra.in  
  注意:默认模板名是文件名带a.in后缀,因此这个调用可以不使用extra.in。我在这里添加是为了清除。


  让我们回到实例化宏签名。我一想你展示tag...参数具有一个复杂的格式,但是省略号表明它代表多个标签,由空格分隔。你在几乎所有的configure.ac文件中可以看到的格式如列表3-14所示。

  1. ...  
  2. AC_CONFIG_FILES([Makefile  
  3.          src/Makefile  
  4.          lib/Makefile  
  5.          etc/proj.cfg])  
  6. ...  
列表3-14 在AC_CONFIG_FILES中指定多个标签(文件)

  这里每个入口是一个标签指定,如果完全指定,会像列表3-15中的调用那样。

  1. ...  
  2. AC_CONFIG_FILES([Makefile:Makefile.in  
  3.         src/Makefile:src/Makefile.in  
  4.         lib/Makefile:lib/Makefile.in  
  5.         etc/proj.cfg:etc/proj.cfg.in])  
  6. ...  
列表3-15 在AC_CONFIG_FILES中完全指定多个标签

  回到实例化宏原型,有两个可选参数你很少会在这些宏中看到:commands和init-cmds。commands参数可能会被用于指定一些任意的shell代码,这些代码在与标签相关的文件生辰之前由config.status执行。这个功能不太在文件生成实例化宏中被使用。你总会在默认不生成文件的AC_CONFIG_COMMANDS中看到commands参数被使用,因为对于这个宏调用,没有命令执行的话是毫无用处的。在这种情况下,tag参数成为一种告诉config.status执行特定shell命令集的方式。


  init-cmd参数使用configure.ac和configure中的变量,初始化在config.status顶部的shell变量。重要的是记住所有实例化宏调用与config.status共享一个共同的命名空间。因此,你应该小心地选择你的shell变量,从而使它们不会相互冲突,与Autoconf生成的变量之间也不会有冲突。


  创建一个configure.ac的测试版本,只是包含列表3-16中的内容。

  1. AC_INIT([test], [1.0])  
  2. AC_CONFIG_COMMANDS([abc],  
  3.          [echo "Testing $mypkgname"],  
  4.          [mypkgname=$PACKAGE_NAME])  
  5. AC_OUTPUT  

列表3-16 实验1: 一个简单的configure.ac

  现在让我们执行autoreconf,configure,和config.status的多种方式,来看看发生了什么:

  1. $ autoreconf  
  2. $ ./configure  
  3. configure: creating ./config.status  
  4. config.status: executing abc commands  
  5. Testing test  
  6. $  
  7. $ ./config.status  
  8. config.status: executing abc commands  
  9. Testing test  
  10. $  
  11. $ ./config.status --help  
  12. 'config.status' instantiates files from templates according to the current  
  13. configuration.  
  14. Usage: ./config.status [OPTIONS]... [FILE]...  
  15. ...  
  16. Configuration commands:  
  17. abc  
  18. Report bugs to <bug-autoconf@gnu.org>.  
  19. $  
  20. $ ./config.status abc  
  21. config.status: executing abc commands  
  22. Testing test  
  23. $  
  如你所见,执行configure引起不带命令行选项的config.status被执行。在configure.ac中没有指定检查,因此手动执行config.status有相同的效果。在命令行执行config.status并带abc标签,运行相关的命令。

  总结下,关于实例化宏的重点如下:
 > config.status脚本更具模板生成所有的文件
 > configure脚本执行所有的检查,然后执行config.status
 > 当你不带命令行选项执行config.status,它会根据最后一次检查结果的设置生成文件
 > 你可以调用config.status来执行文件生成,或者是任何实例化宏调用中给定的标签指定的命令集
 > config.status可能会生成与configures.ac中指定的任何标签无关的文件,它会根据最后一次检查设置执行的结果来替换变量


AC_CONFIG_HEADERS


  如你现在毫无疑问得出的结论,AC_CONFIG_HEADERS宏允许你指定一个或多个config.status会从模板文件生成的头文件。一个配置头文件模板的格式是非常特殊的。列表3-17中给出了一个简短的例子。

  1. /* Define as 1 if you have unistd.h. */  
  2. #undef HAVE_UNISTD_H  

列表3-17 一个头文件模板的简短样例

  你可以在你的头文件模板里放置多个像这样的声明,每行一个。让我试尝试另一个实验。列表3-18中显示了一个新创建的configure.ac文件。

  1. AC_INIT([test], [1.0])  
  2. AC_CONFIG_HEADERS([config.h])  
  3. AC_CHECK_HEADERS([unistd.h foobar.h])  
  4. AC_OUTPUT  

列表3-18 实验2: 一个简单的configure.ac文件

  创建一个称为config.h.in的模板头文件,里面包含两行,如列表3-19。

  1. #undef HAVE_UNISTD_H  
  2. #undef HAVE_FOOBAR_H  

列表3-19 实验2: 继续---一个简单的config.h.in文件

  现在执行下列命令:

  1. $ autoconf  
  2. $ ./configure  
  3. checking for gcc... gcc  
  4. ...  
  5. checking for unistd.h... yes  
  6. checking for unistd.h... (cached) yes  
  7. checking foobar.h usability... no  
  8. checking foobar.h presence... no  
  9. checking for foobar.h... no  
  10. configure: creating ./config.status  
  11. config.status: creating config.h  
  12. $  
  13. $ cat config.h  
  14. /* config.h. Generated from ... */  
  15. #define HAVE_UNISTD_H 1  
  16. /* #undef HAVE_FOOBAR_H */  
  17. $  
  你可以看到config.status从我们写的简单的config.h.in模板生成了一个config.h。这个头文件的内容基于configure执行的检查。因为AC_CHECK_HEADERS([unistd.h foobar.h])生成的shell代码能够在系统include目录定位unistd.h头文件,相应的#undef声明被转换成#define声明。当然,在系统include目录中没有找到foobar.h头文件,因此,它的定义被注释掉了。

  因此,你可以适当地添加这种代码到你项目中的C语言源代码文件中,如列表3-20所示。

  1. #if HAVE_CONFIG_H  
  2. # include <config.h>  
  3. #endif  
  4.   
  5. #if HAVE_UNISTD_H  
  6. # include <unistd.h>  
  7. #endif  
  8.   
  9. #if HAVE_FOOBAR_H  
  10. # include <foobar.h>  
  11. #endif  

列表 3-20 在C语言源文件中使用生成的C++定义

使用autoheader生成include文件模板


  手动维护一个config.h.in模板,麻烦多于必要性。config.h.in的格式非常严格---例如,你不能有任何前导或后续空格在#undef行。除此之外,你需要的大多数config.h.in信息,在configure.ac文件中同样可得到。

  幸运的是,autoheader工具会生成一个合适格式的头文件模板,基于configure.ac的内容,因此,你不怎么必要编写config.h模板。让我们回到命令行提示符做最后一个实验。这个是简单地---只是删除config.h.in模板,然后运行autoheader和autoconf:

  1. $ rm config.h.in  
  2. $ autoheader  
  3. $ autoconf  
  4. $ ./configure  
  5. checking for gcc... gcc  
  6. ...  
  7. checking for unistd.h... yes  
  8. checking for unistd.h... (cached) yes  
  9. checking foobar.h usability... no  
  10. checking foobar.h presence... no  
  11. checking for foobar.h... no  
  12. configure: creating ./config.status  
  13. config.status: creating config.h  
  14. $  
  15. $ cat config.h  
  16. /* config.h. Generated from config.h.in... */  
  17. /* config.h.in. Generated from configure.ac... */  
  18. ...  
  19. /* Define to 1 if you have... */  
  20. /* #undef HAVE_FOOBAR_H */  
  21. /* Define to 1 if you have... */  
  22. #define HAVE_UNISTD_H 1  
  23. /* Define to the address where bug... */  
  24. #define PACKAGE_BUGREPORT ""  
  25. /* Define to the full name of this package. */  
  26. #define PACKAGE_NAME "test"  
  27. /* Define to the full name and version... */  
  28. #define PACKAGE_STRING "test 1.0"  
  29. /* Define to the one symbol short name... */  
  30. #define PACKAGE_TARNAME "test"  
  31. /* Define to the version... */  
  32. #define PACKAGE_VERSION "1.0"  
  33. /* Define to 1 if you have the ANSI C... */  
  34. #define STDC_HEADERS 1  
  35. $  
  注意:再一次,我鼓励你使用autoreconf,在configure.ac中注意到一个AC_CONFIG_HEADERS的表达后,它会自动运行autoheader。

  如你在cat命令的输出所见,一个完整的预处理定义集由autoheader从configure.ac被派生出来。

  列表3-21显示了一个更加真实的例子,使用一个生成的config.h文件来提高你项目源代码的可移植性。在此例子中,AC_CONFIG_HEADERS宏调用表明config.h应该被生成,AC_CHECK_HEADERS宏会引起autoheader插入一个定义到config.h。

  1. AC_INIT([test], [1.0])  
  2. AC_CONFIG_HEADERS([config.h])  
  3. AC_CHECK_HEADERS([dlfcn.h])  
  4. AC_OUTPUT  

列表3-21 使用AC_CHECK_HEADERS的一个更加真实的例子

  config.h文件被规定为包含在代码自身使用C预处理器测试一个配置选项的位置。这个文件应该被包含在源代码的第一个位置,从而使它能够影响后续所包含的系统头文件。

  注意:autoheader所生成的config.h.in模板不包含#ifndef之类保护结构,因此你必须小心的是,在一个源文件中,不要包含超过一次。


  经常地情况是,工程中每个.c文件需要包含config.h。在此情况下,你理应在一个内部项目头文件的顶部包含config.h,内部项目头文件被你项目中所有源码文件包含。你可以在此内部头文件中添加一个#ifndef保护结构以防止它被多次包含。
使用列表中的configure.ac,生成的configure脚本会创建一个恰当定义了的config.h头文件,用于在编译时判断当前系统是否提供了dlfcn接口。为完成可移植检查,你可以添加列表3-22中的代码到你使用动态加载功能的项目源码文件中。

  1. #if HAVE_CONFIG_H  
  2. # include <config.h>  
  3. #endif  
  4.   
  5. #if HAVE_DLFCN_H  
  6. # include <dlfcn.h>  
  7. #else  
  8. # error Sorry, this code requires dlfcn.h.  
  9. #endif  
  10.   
  11. ...  
  12.   
  13. #if HAVE_DLFCN_H  
  14.   handle = dlopen("/usr/lib/libwhatever.so", RTLD_NOW);  
  15. #endif  
  16. ...  

列表3-22 检查动态加载功能的源文件样例

  如果你已有包含dlfcn.h的代码,autoscan会在configure.ac中生成一行来调用AC_CHECK_HEADERS,使用一个包含dlfcn.h作为其中一个被检查头文件的参数列表。你作为维护者的工作是在你代码中存在的dlfcn.h头文件包含和dlfcn接口功能调用周围,添加条件声明。这是Autoconf提供的可移植性的关键。

  注意:如果你要摆脱一个错误,最好是在配置时而不是编译时。总的原则是尽早走出困境。


  在此源代码中一个明显的缺陷是,config.h只在HAVE_CONFIG_H在你编译环境中定义时,才会被包含。如果你正编写你自己的make文件,你必须在你的编译命令行手动地定义HAVE_CONFIG_H。Automake在生成的Makefile.in模板中为你做了这部分工作。


  HAVE_CONFIG_H是一个定义字符串的一部分,在Autoconf替换变量@DEFS@中在编译器命令行上被传递。在autoheader和AC_CONFIG_HEADERS功能退出之前,Automake会添加所有的编译器配置宏到@DEFS@变量。如果你在configure.ac中不使用AC_CONFIG_HEADERS,你任然可以使用这个方法---主要是因为大量的定义会有很长的编译器命令行。


回到远程构建一会儿


  当我们结束这一章,我们会注意到我们回到了原点。在我们讨论如何添加远程构建到Jupiter之前,我们开始涉及一些事前资料。现在我们会回到这一主题一会儿,因为我尚未涉及如何获取C预处理器,来合适地定位一个生成的config.h文件。
因为这个文件是从一个模板生成,它在构建目录结构中会与它匹配的模板文件相同的相对路径。模板位于顶层source目录(除非你选择放在其它地方),因此,生成的文件会在顶层build目录。


  让我们考虑下头文件在一个工程中的位置。我们可能在当前构建目录生成它们,作为构建过程的一部分。我们也可能添加内部头文件到当前源码目录。我们知道我们有一个config.h文件在顶层构建目录。最后,我们也可能创建一个顶层include目录,一个我们软件包提供的用于库函数接口头文件的目录。这些include目录的优先级顺序是怎么样的呢?


  我们放在编译器命令行上的include指示符(-Ipath选项)的顺序,就是它们会被搜寻的顺序,因此这个顺序应该基于哪些文件会被当前所编译源码源码最相关的。因此,编译器命令行应该包括-Ipath指示符,首先是当前构建目录(.),紧接着是源码目录[$(srcdir)],然后是顶层构建目录(..),最后是我们的项目的include目录,如果有的话。我们通过添加-Ipath选项到编译器命令行来强加这一顺序,如列表3-23所示。

  1. ...  
  2. jupiter: main.c  
  3.   $(CC) -I. -I$(srcdir) -I.. $(CPPFLAGS) $(CFLAGS) -o $@ main.c  
  4. ...  

列表3-23 src/Makefile.in: 添加合适的编译器包含指示符

  现在让我们知道这个,我们需要添加另一个经验法则用于远程构建:
 > 为当前构建目录添加预处理命令,以相关的源码目录,顶层构建目录的顺序;


总结


  在这章节中,我们涉及了关于一个全功能GNU项目编译系统的所有主要特性,包括写一个configure.ac文件,Autoconf从它生成一个全功能configure脚本。我们也涉及了用VPATH声明添加远程构建功能到make文件。

  因此,另外还有什么?很多!在下一章,我会继续向你展示如何使用Autoconf来测试系统特性和功能,在你的用户运行make之前。我们也会继续提升配置脚本,从而当我们完成,用户会有更多选择,并且确切明白我们的软件包如何在它们的系统上被构建。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值