《Autotools - GNU Autoconf, Automake与Libtool实践者指南》第二章


  在第一章中,我给出了GNU Autotools和一些资源的概述,可以帮助降低所需要的学习曲线来掌握它们。在这一章节中,我们会退一小步,调查可用于任何工程的项目组织技术,不仅仅使用Autotools。


  当你完成阅读这一章节,你应该会熟悉普通的make目标,知晓它们为何存在。你应该也会对工程组织方式有一个坚实的理解。当你完成这一章节,你会是很好地在通往Automake专家的路上。

  这一章节提供的信息最初来自两个资源:
 > GNU编码标准(GCS),见http://www.gnu.org/prep/standards/
 > 文件系统等级标准(FHS),见http://www.pathname.com/fhs/

  如果你想温习你的make语法,你会发现GNU make手册非常有用。如果你特别喜欢可移植make语法(你应该很可能是的),查看make的POSIX手册页。


创建一个新的项目目录结构



  当你为一个开源软件工程建立编译系统时,有两个问题你需要问自己:
 > 目标平台?
 > 用户期望?

  第二个问题回答比较困难。首先,让我们将问题范围缩窄为可控制的。你真正需要问的是:我的用户期望我的编译系统是怎么样的。有经验的开源软件开发者熟悉这些期望,通过下载、解压、编译和安装成千个软件包。最终,他们开始直观地知道用户期望的编译系统。但是,即使如此,软件包配置,编译和安装的过程变化很广,因此,定义任何固定的常态是非常困难的。

  你可以咨询自由软件基金会(FSF),GNU项目的发起者,已经为你做了很多收集资料的工作,而不是自己开展一个每一种编译系统的调查。FSF是获取关于自由、开源软件方面信息最佳的来源之一,包括GCS,GCS涉及宽范围的主题,关于编写、发布和分发自由、开源软件。当设计一个管理打包、编译和安装软件的系统时,许多问题需要考虑,GCS考虑了其中的绝大多数。


项目结构


  我们将开始一个样例项目并在此基础上构建,作为我们继续探索源码级软件分发的旅途。我将我们的项目称为Jupiter,我会使用下列命令创建一个工程目录结构:
$ cd projects
$ mkdir -p jupiter/src
$ touch jupiter/Makefile
$ touch jupiter/src/Makefile
$ touch jupiter/src/main.c
$ cd jupiter
$
  现在我们有一个源码目录称为src,一个C源码文件称为main.c,和为我们项目中的两个目录各一个Makefile文件。大家都知道一个成功的开源软件项目的关键是演进。开始比较下,根据需要增长---当你有时间和倾向。

  让我们以编译和清理项目作为开始。顶层Makefile仅仅递归地传递请求到src/Makefile。这构成了一个相当常见的编译系统类型,称为递归编译系统,之所以这么命名是因为,make文件递归地调用子目录里的make文件。

all clean jupiter:
  cd src && $(MAKE) $@
.PHONY: all clean
列表2-1 Makefile:顶层make文件初始草稿
all: jupiter
jupiter: main.c
  gcc -g -O0 -o $@ main.c
clean:
  -rm jupiter
.PHONY: all clean
列表2-2 src/Makefile:src目录下make文件的首个草稿
#include 
#include 
int main(int argc, char * argv[])
{
  printf("Hello from %s!\n", argv[0]);
  return 0;
}
列表2-3 src/main.c:项目中一个源码的第一版


创建一个源分布存档


  为了让我们的用户获取到Jupiter的源码,我们将创建和发布一个软件源存档---一个压缩包。我们可以写一个单独的脚本来创建压缩包,但是因为我们可以使用伪目标在make文件中创建任意的功能集,让我们设计设计一个make目标来执行这一任务。为发布版构建一个源码档案通常与dist目标相关。

  当我们设计一个新的make目标是,我们需要考虑它的功能是在工程make文件中被分布还是在一个单一的位置被处理。通常情况下, 经验法则是利用递归编译系统的本性,允许每个目录管理一个过程中它自己的部分。我们已这么做,当我们传递编译jupiter程序的控制到src目录时。然而,从一个目录结构构建一个压缩档案不是一个递归过程。因为这个原因,我们不得不在两个make文件中的一个中执行完整的任务。

  全局操作通常在工程目录结构中的顶层make文件中被处理。我们添加dist目标到顶层make文件,如列表2-12所示。
package = jupiter
version = 1.0
tarname = $(package)
distdir = $(tarname)-$(version)
all clean jupiter:
	cd src && $(MAKE) $@

dist: $(distdir).tar.gz

$(distdir).tar.gz: $(distdir)
	tar chof - $(distdir) | gzip -9 -c > $@
	rm -rf $(distdir)

$(distdir):
	mkdir -p $(distdir)/src
	cp Makefile $(distdir)
	cp src/Makefile $(distdir)/src
	cp src/main.c $(distdir)/src

.PHONY: all clean dist
列表2-12: Makefile:添加dist目标到顶层make文件

  我已将dist目标的功能分成了三个独立地规则,为的是可阅读性,模块化和可维护性。在任何软件工程处理中,这是一个需要遵循的重要的经验法则:从较小的构建大过程,在有用的地方重用较小的过程。

  我们并不希望对象文件和可执行文件被存放在压缩档案中,因此我们需要构建一个镜像目录,其中确切地包含我们需要附带的,包括在编译和安装过程中需要的任何文件和任何添加的文档或license文件。不幸的是,这大大增加了单独拷贝命令的使用。


强制一个规则运行


  如果镜像目录(jupiter-1.0)已经存在,当你执行make dist时,make不会试图去创建它。dist目标不会拷贝任何文件,jupiter会是个空文件。更糟糕的是,如果来自前一次试图存档的镜像目录任然存在,新的压缩存档会包含来自前一次存档的旧的源代码。

  问题是$(distdir)目标是个真实的目标但是没有依赖,这意味着只要它存在,make就认为它是最新的。我们可能添加$(distdir)目标到.PHONY规则来强制在每次make dist时重编译它,但是它不是个伪目标---它是个真实的文件系统目标。合适的方式是确保$(distdir)目标总是被重编译,确保在make试图构建它时不存在。一种完成这个的方式是创建一个总是会执行的伪目标,添加那个目标到$(distdir)目标的依赖链中。这种类型目标的常用名是FORCE,我已在列表2-13中实现了这一想法。
...
$(distdir).tar.gz: $(distdir)
	tar chof - $(distdir) | gzip -9 -c > $@
	rm -rf $(distdir)

$(distdir): FORCE
	mkdir -p $(distdir)/src
	cp Makefile $(distdir)
	cp src/Makefile $(distdir)/src
	cp src/main.c $(distdir)/src

FORCE:
	-rm $(distdir).tar.gz >/dev/null 2>&1
	-rm -rf $(distdir) >/dev/null 2>&1

.PHONY: FORCE all clean dist
列表2-13 Makfile:使用FORCE目标

  FORCE规则的命令每次都会被执行,因为FORCE是一个伪目标。因为我们使得FORCE是$(distdir)目标的依赖,我们有机会删除任何先前创建的文件和目录,然后开始让make评估是否应该执行$(distdir)的命令。

前导控制字符


  一个命令上的前导破折号(-)告诉make不必关心它所前导命令的执行状态。通常,当make遇到一个命令返回非零状态码到shell,它会停止执行并显示一个错误信息---但是如果你使用了一个前导破则好,它会忽略错误并继续。我在FORCE规则中rm命令前使用破折号,是因为如果我试图删除一个不存在的文件,rm会返回错误。

  注意,我在打包规则的rm命令前,并没有使用前导破折号。因为我想知道rm如果有错误---如果它不成功,应该有非常大的错误,因为前面的命令应该已根据此目录创建了一个打包命令了。



自动测试一个发布版


  构建归档目录的规则可能是make文件中最让人沮丧的规则,因为它包含命令来拷贝独立文件到发布目录。在项目中每次我们修改文件结构,我们不得不在我们的顶层make文件中更新这一规则,否则我们会破坏dist目标。但是我们没有什么跟多可以做---我们已经使得规则尽可能简单。现在我们不得不记住来恰当地管理这一过程。

  尽管不幸,破坏dist目标不是最糟糕的事情。最为糟糕的是,dist目标在工作,但是实际上并没有拷贝所有需要的文件到压缩包中。实际上,远非如此,没有一个错误会产生,因为添加文件到一个工程是一个更为常见的活动,相比移动或删除它们。新文件没有被拷贝,但是dist规则没有注意到差别。

  有一种方式来执行在dist目标上的一种自检。我们可以创建另一个称为distcheck的伪目标,做我们用户确切会做的事:解压压缩包和编译工程。我们可以在一个临时目录里用此规则的名利执行这一任务。如果编译过程失败,distcheck目标会终止,告诉我们在发布版中忘记了一些重要的东西。

  列表2-14显示了在顶层make文件中需要实现distcheck目标的修改。
...
distcheck: $(distdir).tar.gz
	gzip -cd $(distdir).tar.gz | tar xvf -
	cd $(distdir) && $(MAKE) all
	cd $(distdir) && $(MAKE) clean
	rm -rf $(distdir)
	@echo "*** Package $(distdir).tar.gz is ready for distribution."
...
.PHONY: FORCE all clean dist distcheck
列表2-14 Makefile:添加一个distcheck目标到顶层make文件

  distcheck目标依赖于压缩包自己,因此构建压缩包的规则先执行。make然后执行distcheck命令,解压刚构建的压缩包,递归运行递归目录里的make命令。如果那个过程成功,它打印一条表示你的用户不怎么可能在此压缩包使用中遇到问题的信息。

  现在所有你得做是,记住,在向大家发布你的压缩包之前,执行make distcheck。



单元测试


  合适的单元测试是份艰苦的工作,但是最后是有回报的。那些做这件事的人,已经学了一课关于延迟享乐的价值。

  一个良好的编译系统应该包含合适的单元测试。为测试一个构建最为常用的目标是check目标,因此我们会继续,以通常的方式添加它。实际的单元测试应该可能会放在src/Makefile中,因为那是被构建的jupiter可执行文件所在处,因此我们会从顶层make文件向下传递check目标

  但是我们在check规则中放什么命令呢?jupiter是个相当简单的程序,它打印一条信息。我们使用grep工具来测试jupiter实际上是输出了这样一条字符串。

  列表2-15和列表2-16分别阐述了顶层和src目录下的make文件的修改。
...
all clean check jupiter:
  cd src && $(MAKE) $@
...
.PHONY: FORCE all clean check dist distcheck
列表2-15 Makefile:传递check目标到src/Makefile
...
check: all
  ./jupiter | grep "Hello from .*jupiter!"
  @echo "*** ALL TESTS PASSED ***"
...
.PHONY: all clean check
列表2-16 src/Makefile:在check目标中实现单元测试

  注意check目标依赖于all。我们不能真正测试我们的产品除非他们最新的,反映了已近做的任何源码或构建系统的修改。如果用户需要测试产品,他想要产品存在,并且是最新的。我们能够确保它们存在并且是最新的,通过添加all到check的依赖列表中。

  对于我们的编译系统,我们可以做一个更多的提升:我们可以在distcheck规则中添加check到由make执行的目标列表,在make all和make clean命令之间,如列表2-17所示。

...
distcheck: $(distdir).tar.gz
  gzip -cd $(distdir).tar.gz | tar xvf -
  cd $(distdir) && $(MAKE) all
  cd $(distdir) && $(MAKE) check
  cd $(distdir) && $(MAKE) clean
  rm -rf $(distdir)
@echo "*** Package $(distdir).tar.gz is ready for distribution."
...
列表2-17 Makefile: 添加check目标到$(MAKE)命令

  现在我们可以运行make distcheck,它会测试软件包附带的整个编译系统。



安装产品


  安装在Jupiter项目中显得那么不重要,因为只有一个程序,大多数用户会猜到并安装它。然而,复杂的项目就会引起用户的恐慌,当涉及把用户和系统二进制文件、库、头文件,和文档包括手册、PDF文件,和或多或少的README,AUTHORS,NEWS,INSTALL,COPYING等。

  当创建一个发布版软件包时,可能不是一个内在的递归过程,安装确实是,因此我们允许工程中每个子目录管理它自己组件的安装。为了这么做,我们需要同时修改顶层和src层make文件。修改顶层make文件是简单地:因为没有产品被安装在顶层目录,我们将会以通常的方式传递责任到src/Makefile。

  添加install目标的修改显示在列表2-18和2-19中。
...
all clean check install jupiter:
  cd src && $(MAKE) $@
...
.PHONY: FORCE all clean check dist distcheck install
列表2-18 Makefile:传递install目标到src/Makefile
...
install:
  cp jupiter /usr/bin
  chown root:root /usr/bin/jupiter
  chmod +x /usr/bin/jupiter
.PHONY: all clean check install
列表2-19 src/Makefile:实现install目标

安装选择


  上面的install目标是好的,但是当考虑安装的位置时,可以有更加灵活的方式---指定安装路径。
  我们目前编译系统的另一个问题是,为了安装文件,我们必须做很多材料。大多数Unix系统提供一个系统级程序---通常是一个shell脚本---称为install允许用户指定被安装文件的多种属性。这一工具的恰当使用,可以简化一些Jupiter的安装,因此当我们添加位置灵活性时,我们可能可以使用install工具。这些修改显示在列表2-20和2-21中。
...
prefix=/usr/local
export prefix

all clean check install jupiter:
  cd src && $(MAKE) $@
...
列表2-20 Makefile:添加一个prefix变量
...
install:
	install -d $(prefix)/bin
	install -m 0755 jupiter $(prefix)/bin
...
列表2-21: src/Makefile:在install目标中使用prefix变量

  注意的是,我只在顶层make文件中声明和赋值prefix变量,但是在src/Makefile中引用。我可以这么做是因为我在顶层Makefile中使用修饰语export,这一修饰语输出make变量到shell。这一make的特性允许我们定义我们所有的用户变量到一个明显的位置---顶层Makefile的开始。

  注意:GNU make允许你在赋值行使用export关键词,但是这一语法在其它版本的make中不可移植。

  我已在makefile中定义prefix变量为/usr/local,make允许你在命令行定义make变量,以这种方式:

$ sudo make prefix=/usr install
...
  记住,在命令行定义的变量覆盖定义在makefile中定义的。因此,用户需要安装jupiter到/usr/bin目录的,现在就可以有在make命令行指定这一方面的选项。

  有了这一系统,我们的用户可能安装jupiter到任意所选择目录下的bin目录中。实际上,这是我们在列表2-21中添加install -d $(prefix)/bin的理由---如果不存在安装目录bin,这一命令创建它。既然我们允许用户在make命令行定义prefix,我们实际上无法知道用户会把jupiter安装在哪个目录;因此,我们必须为位置不存在的可能性做好准备。


卸载一个软件包


  列表2-22和列表2-23显示了添加一个uninstall目标到两个make文件中的情况。
...
all clean install uninstall jupiter:
	cd src && $(MAKE) $@
...
.PHONY: FORCE all clean dist distcheck install uninstall
列表2-22 Makefile:添加uninstall目标到顶层makefile
...
uninstall:
	-rm $(prefix)/bin/jupiter
.PHONY: all clean check install uninstall
列表2-23 src/Makefile:添加uninstall目标到src级makefile

  在我们修改安装过程时,现在有两个位置我们需要更新:install和uninstall目标。在第五章,我会向你展示使用GNU Automake如何以一种更为简单的方式重写这个makefile。

测试安装和卸载

  现在让我们添加一些代码到我们的distcheck目标,来测试install和uninstall目标的功能。列表2-24显示了在顶层Makefile中的必要修改。

...
distcheck: $(distdir).tar.gz
    gzip -cd $(distdir).tar.gz | tar xvf -
    cd $(distdir) && $(MAKE) all
    cd $(distdir) && $(MAKE) check
    cd $(distdir) && $(MAKE) prefix=$${PWD}/_inst install
    cd $(distdir) && $(MAKE) prefix=$${PWD}/_inst uninstall
    cd $(distdir) && $(MAKE) clean
    rm -rf $(distdir)
@echo "*** Package $(distdir).tar.gz is ready for distribution."
...
列表2-24 Makefile: 为install和uninstall目标添加distcheck测试

  注意,在$$(PWD)变量应用中,我使用了两个美元符号,确保make使用命令行的其余部分传递变量引用到shell,而不是在执行命令前扩展。我希望这个变量被shell解引用,而不是make工具。

  

  我们可以或多或少地写一个通用测试,检查我们已安装的是否已被合适地移除了。列表2-25显示了为这一测试所增加的。

...
distcheck: $(distdir).tar.gz
  gzip -cd $(distdir).tar.gz | tar xvf -
  cd $(distdir) && $(MAKE) all
  cd $(distdir) && $(MAKE) check
  cd $(distdir) && $(MAKE) prefix=$${PWD}/_inst install
  cd $(distdir) && $(MAKE) prefix=$${PWD}/_inst uninstall
  @remaining="`find $${PWD}/$(distdir)/_inst -type f | wc -l`"; \
  if test "$${remaining}" -ne 0; then \
    echo "*** $${remaining} file(s) remaining in stage directory!"; \
    exit 1; \
  fi
  cd $(distdir) && $(MAKE) clean
  rm -rf $(distdir)
  @echo "*** Package $(distdir).tar.gz is ready for distribution."
...
  测试首先生成了一个称为remaining的数值,代表了安装目录中常规文件的数目。如果这个值不是零,它会打印一条信息到控制台,显示有多少个文件被uninstall命令落下,然后它以错误退出。

  我并不想通过打印嵌入的echo声明来提醒人们,除非它应该被执行时,因此我用@前缀整个测试语句,从而使make不会打印代码到stdout。因为make认为这五行代码是一个单一的命令(代码中用\拼接),唯一抑制打印echo申明的方法是抑制打印整个命令。

  这里的代码只是检查常规文件。如果你的安装过程创建了任何软链接,如果它们被落下,这个测试程序不会注意到。在安装过程中构建的目录结果被落下在原地,因为检查代码不知道一个子目录是属于系统的还是工程的。uninstall规则命令可以知道哪些目录是工程相关的,并合适地删除它们,但是我不想添加工程相关知识到distcheck测试。



支持标准目标和变量



  除了我已提到的,GNU编码标准列出了一些重要的目标和变量,你应该在你的项目中去支持---主要是因为你的用户会期望有它们的支持。

  对GCS中的一些章节应该持怀疑态度,除非你在一个GNU发起的项目上工作。例如,你很可能不太关心第五章中关于C源代码格式的建议。你的用户当然也不会在意,因此你可以使用你希望的任何源代码格式风格。

  那并不是说对于非GNU开源项目,第五章中的所有部分都是没有价值的。例如,“系统类型之间的可移植性”和“CPU之间的可移植性”子部分,提供了关于C源代码可移植性方面极好的信息。“国际化”子部分给你一些关于使用GNU软件国际化你的项目的有用建议。

  第六章讨论了GNU方式的文档,第六章的一些部分描述了项目中经常会有的多种顶层文本文件,例如AUTHORS,NEWS, INSTALL, README和ChangeLog文件。在任何声誉良好的项目中,这些都是受过良好熏陶的开源软件用户期望看到的所有信息点。

  GCS文档中,真正有用的信息开始于第七章:“发布流程”。作为一个维护者,这一章对你来说是关键,因为它定义了你的用户会期望的项目编译系统。第七章包含了软件包在源码级发布版中提供的用户选项的事实标准。

标准目标


  GCS中第七章的子部分“配置应该如何工作”定义了配置过程。“Makfile惯例”子部分涉及了用户在开源软件包中所期望的所有标准目标和许多标准变量。GCS定义的标准目标如下:
all        install              install-html
install-dvi      install-pdf          install-ps
install-strip    uninstall            clean
distclean        mostlyclean          maintainer-clean
TAGS             info                 dvi
html             pdf                  ps
dist             check                installcheck
installdirs
   你不需要支持所有的目标,但是你应该考虑支持对你项目有意义的目标。例如,如果你编译和安装HTML页面,你应该考虑支持html和install-html目标。Autotools项目支持这些,并且更多。一些目标对最终用户有用,然而另一些只是对项目维护者有用。

标准变量


  你应该支持的变量如下面表中所示。大多数变量以多个方面被定义,最终只有一个:prefix。因为一个标准名称的缺少,我称这些prefix变量。大多数可以被归类为参考标准位置的安装目录变量,但也有一些例外,例如srcdir。表2-1列出了这些prefix变量和它们的默认值。

Variable         Default Value

prefix          /usr/local
exec_prefix             $(prefix)
bindir                  $(exec_prefix)/bin
sbindir                 $(exec_prefix)/sbin
libexecdir              $(exec_prefix)/libexec
datarootdir             $(prefix)/share
datadir                 $(datarootdir)
sysconfdir              $(prefix)/etc
sharedstatedir          $(prefix)/com
localstatedir           $(prefix)/var
includedir              $(prefix)/include
oldincludedir           /usr/include
docdir                  $(datarootdir)/doc/$(package)
infodir                 $(datarootdir)/info
htmldir                 $(docdir)
dvidir                  $(docdir)
pdfdir                  $(docdir)
psdir                   $(docdir)
libdir                  $(exec_prefix)/lib
lispdir                 $(datarootdir)/emacs/site-lisp
localedir               $(datarootdir)/locale
mandir                  $(datarootdir)/man
manNdir                 $(mandir)/manN (N = 1..9)
manext                  .1
manNext                 .N (N = 1..9)
srcdir                  The source-tree directory corresponding to the
                        current directory in the build tree
  基于Autotools的项目自动地支持这些和其它有用变量;Automake提供了地它们的全面支持,而Autoconf的支持更为有限。如果你编写你自己的make文件和编译系统,你应该在编译和安装过程中尽可能多地支持它们。

添加位置变量到Jupiter


  列表2-26和列表2-27显示了顶层Makefile和src下Makefile中的修改。

...
prefix = /usr/local
exec_prefix = $(prefix)
bindir = $(exec_prefix)/bin
export prefix
export exec_prefix
export bindir
...

列表2-26 Makefile: 添加bindir

...
install:
install -d $(bindir)
install -m 0755 jupiter $(bindir)
uninstall:
-rm $(bindir)/jupiter
...

列表2-27 src/Makefile: 添加bindir

  尽管我们在src/Makfile中只是用了bindir,我们必须export prefix,exec_prefix和bindir,因为bindir根据exec_prefix形式定义,后者根据prefix定义。当我们运行install命令时,首先展开bindir到$(exec_prefix)/bin,然后到$(prefix)/bin,最终到/usr/local/bin。因此,src/Makefile在此过程中需要访问所有三个变量。

  在多个层次修改前缀变量的能力对于一个Linux发行版软件包打包者来说特别有用,他需要安装软件包到非常特定的系统位置。例如,一个发行版打包者可以使用下面的命令来修改安装前缀到/usr,和系统配置目录到/etc。
$ make prefix=/usr sysconfdir=/etc install
...
   没有多层次修改前缀变量的能力,配置文件最终会在/usr/etc,因为$(sysconfdir)的默认值是$(prefix)/etc。


项目进入Linux发行版


  在GCS的第七部分,包含了一小部分来讨论支持分阶段安装。为支持分阶段安装,所有你需要的只是一个称为DESTDIR的变量,作为一类超级前缀到你所有的安装产品。列表2-28列出了需要的修改。
...
install:
  install -d $(DESTDIR)$(bindir)
  install -m 0755 jupiter $(DESTDIR)$(bindir)
uninstall:
  -rm $(DESTDIR)$(bindir)/jupiter
...
列表2-28 src/Makefile: 添加分阶段编译功能

  你不必定义一个DESTDIR的默认值,因为如果它未定义,它会被扩展为一个空字符串,这对它所考虑的目录没有影响。

  我没有必要添加$(DESTDIR)到uninstall规则的rm命令,因为对于软件包管理器,它们并不关心你的软件包如何卸载。软件包管理器例如RPM,使用它们自己的规则从一个系统中移除产品,这些规则基于一个软件包管理数据库,而不是你的uninstall目标。

  然而,处于对称性和完整性,添加$(DESTDIR)到uninstall并没有坏处。另外,为了distcheck目标的完整性,我们需要它。修改如下:

...
distcheck: $(distdir).tar.gz
  gzip -cd $(distdir).tar.gz | tar xvf -
  cd $(distdir) && $(MAKE) all
  cd $(distdir) && $(MAKE) check
  cd $(distdir) && $(MAKE) DESTDIR=$${PWD}/_inst install
  cd $(distdir) && $(MAKE) DESTDIR=$${PWD}/_inst uninstall
  @remaining="`find $${PWD}/$(distdir)/_inst -type f | wc -l`"; \
  if test "$${remaining}" -ne 0; then \
    echo "*** $${remaining} file(s) remaining in stage directory!"; \
    exit 1; \
  fi
  cd $(distdir) && $(MAKE) clean
  rm -rf $(distdir)
  @echo "*** Package $(distdir).tar.gz is ready for distribution."
...
列表2-29 Makefile: 在distcheck目标中使用DESTDIR


  在install和uninstall命令中修改prefix到DESTDIR允许我们恰当地测试一个完整的安装目录等级,如我们马上会看到的。

  在这一点,一个RPM特定文件可以提供下列文本作为Jupiter软件包的安装命令:

%install
make prefix=/usr DESTDIR=%BUILDROOT install
   不要关心软件包管理器的文件格式。只要注意通过DESTDIR变量所提供的阶段性安装功能。

  你可能会疑惑为何prefix变量不能提供这一功能。一方面,在系统级安装中,不是每一个路径是相对prefix定义的。系统配置目录(sysconfdir),例如,通常被软件包管理器定义为/etc。你可以在表2-1中看到,sysconfdir的默认定义是$(prefix)/etc,因此唯一的方式会解析到/etc的是如果你明确的在configure或make命令行中设置它。如果你用那种方式配置它,DESTDIR变量会在分阶段安装过程中影响sysconfdir的基础位置。在本章后续部分和接下来的两章中,这么做的理由会变得更加清晰。



编译与安装的前缀覆盖


  在此,我想稍稍离题解释一个难懂(或者说至少是不明显的)的概念,关于定义在GCS中的prefix和其它路径变量。在前面的例子中,我在make install命令行使用prefix覆盖,像这样:

$ make prefix=/usr install
...

  我想强调的问题是:make all和make install使用一个prefix覆盖之间的差别。在我们样例的make文件中,我们已试图避免在任何与安装无关的目标中使用前缀,因此,对于你来说,可能不会那么清除知道一个前缀在编译阶段中会是那么有用。然而,前缀变量在编译阶段可以是非常有用的,在编译时替换源码中的路径,如列表2-30所示。
program: main.c
  gcc -DCFGDIR="\"$(sysconfdir)\"" -o $@ main.c

列表2-30  在编译时把替代路径放入源码


  在这个例子中,我在用于main.c的编译器命令行中定义了一个称为CFGDIR的C预处理器变量。在main.c中,大概有些代码如列表2-31中所示。
#ifndef CFGDIR
# define CFGDIR "/etc"
#endif
const char cfgdir[FILENAME_MAX] = CFGDIR;
列表2-31 在编译时替代CFGDIR

  在后面的代码中,你可能会使用C全局标量cfgdir来访问应用程序的配置文件。
  Linux发布版软件包管理器通常为在RPM特定文件中的编译和安装使用不同的前缀覆盖。在编译阶段,实际运行时目录被人工编码到可执行文件,使用如列表2-32所示的命令行。

%build
%setup
./configure prefix=/usr sysconfdir=/etc
make

列表2-32 一个编译源码树的RPM特定文件的一部分

  注意伴随着prefix,我们已明确指定sysconfdir,因为,如前面讲到的,系统配置目录通常是在系统前缀目录结构的外面。软件包管理器安装这些可执行文件到一个阶段性目录,从而使在编译二进制软件包时,可以将它们拷贝出它们的安装路径。相应的安装命令可能像列表2-33中所示。

%install
make DESTDIR=%BUILDROOT% install

列表2-33 一个RPM特定文件的安装部分

  在安装期间使用DESTDIR会暂时覆盖所有安装前缀变量,因此,你不必记住在你配置期间你已覆盖了哪些变量。给定如列表2-32中所示配置命令,如列表2-33中所示方式使用DESTDIR,具有与列表2-34所示代码相同的效果。

%install
make prefix=%BUILDROOT%/usr sysconfdir=%BUILDROOT%/etc install

列表2-34 在安装期间覆盖默认sysconfdir


  这里的关键点是我之前谈及的。绝不要把你的install目标写到Makfile中编译所有或部分产品的目标上去。安装功能应该被限制到拷贝文件,如果可能的话。另外,如果你的用户使用前缀覆盖,他们不能访问你的阶段安装特性。


  另一个这种限制安装功能方式的理由是,它允许用户作为一个组安装软件包集到一个独立的位置,然后在合适的位置创建链接到实际的文件。一些人喜欢这么做,当测试一个软件包时,并希望跟踪所有它的组件。



用户变量


  GCS定义了一个变量集,对于用户来说是神圣的。这些变量应该会被GNU编译系统引用,但是决不能被GNU编译系统修改。这些所谓的用户变量包括在那些在表2-2中所列用于C和C++程序的变量。

Variable             Purpose
CC                   A reference to the system C compiler
CFLAGS               Desired C compiler flags
CXX                  A reference to the system C++ compiler
CXXFLAGS             Desired C++ compiler flags
LDFLAGS              Desired linker flags
CPPFLAGS             Desired C/C++ preprocessor flags
. . .

表2-2 一些用户变量和他们的意图

  你可以在GNU Make手册中的“被隐含规则使用的变量”部分找到一个关于程序名和标志变量的相对完整的列表。对我我们的意图,表2-2所示标量足够了,但是对于一个更为复杂的make文件,你应该熟悉GNU Make手册中所列的更为复杂的列表。


  为了在我们的make文件中使用这些变量,我们只需要用$(CC)替换gcc。我们会对CFLAGS和CPPFLAGS做相同的事,尽管CPPFLAGS默认值是空。CFLAGS变量也没有默认值,但是这是个好的时机来添加一个。我喜欢使用-g选项来编译对象,-O0来禁止对调试期间的编译进行优化。对src/Makefile的更新如列表2-52中所示。

...
CFLAGS = -g -O0
...
jupiter: main.c
  $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ main.c
...

列表2-35 src/Makefile: 添加合适的用户变量

  这时可行的,因为make工具允许这样的变量被命令行的选项覆盖。例如,为了切换编译器和设置一些编译器的命令行选项,用户只需输入下列信息:

$ make CC=gcc3 CFLAGS='-g -O2' CPPFLAGS=-dtest
  在这个例子中,我们的用户已经决定使用GCC的版本3替代版本4,生成调试符号,使用第二级优化来优化他的代码。他也决定使能test选项,通过一个C预处理器定义的使用。注意,如果这些变量是在make命令行设置的,这很明显等价于Bourne-shell语法,不会如期望的那样工作:
$ CC=gcc3 CFLAGS='-g -O2' CPPFLAGS=-dtest make
  理由是,我们仅仅在本地环境中设置环境变量,通过shell传递给make工具。记住,环境变量不会自动覆盖这些在make文件中的设置。为了我们需要的功能,我们可以在我们的make文件中使用一些GNU特定make语法,如列表2-36所示。
...
CFLAGS ?= -g -O0
...
列表2-36 在一个make文件中使用GNU make特定的查询赋值操作符(?=)

  ?=操作符是一个GNU make特定的运算符,在make文件中只会设置哪些尚未在其它地方设置过的变量。这意味着,我们现在可以覆盖这些特殊的变量设置,通过在环境中设置。但别忘了这仅仅在GNU make中工作。总的来说,最好在make命令行中设置make变量。



配置你的软件包


  GCS在第七部分的“配置应该如何工作”子部分中讲述了配置过程。到此为止,我们在仅使用make文件的情况下,对Jupiter做任何我们想要的,因此,你可能会疑惑配置到底是为了什么。在GCS这一部分的开始段落,回答了我们的问题:

  每一个GNU发布版应该附带一个称为configure的shell脚本。这一脚本给出了你想为程序编译的机器和系统的类型。configure脚本必须记录配置选项,从而影响编译。


  一个典型的配置脚本的主要任务如下:
 > 从包含替代变量的模板生成文件;
 > 根据项目源码生成一个C语言头文件(config.h);
 > 为一个特定的make环境设置用户选项(调试标志等);
 > 设置多个软件包选项作为环境变量;
 > 测试工具、库和头文件的存在性.


  对于复杂的项目,配置脚本经常从一个或多个由项目开发者维护的模板生成项目make文件。这些模板包含配置变量,以一种容易识别和替换的格式。配置脚本用配置过程中决定的变量值替换这些变量---要么从用户指定的命令行选项,或者是从一个平台环境的完整分析。这个分析细化到检查某种系统或软件包头文件和库的存在性,为需要的程序和工具搜索系统路径,甚至运行设计的小程序来掌握shell、C编译器或需要的库的特性集。

  在过去,变量替换的工具选择是sed流编辑器。一个简单的sed命令通过单次扫过文件可以替换在make文件模板中所有的配置变量。然而,Autoconf 2.62或更新的版本选择awk代替sed用于这一过程。awk程序提供跟多的功能允许很多变量的高效替换。



总结


  我们现在通过手写的方式已创建了一个完整的工程编译系统,有一个重要的例外:我们没有根据GNU编码标准中指定的设计标准,设计一个configure脚本。我们可以做,但是这会占据很多文本页面。我还是简单的继续一个Autoconf的讨论,而不是花费时间和精力去做这事,Autoconf允许我们构建其中一个脚本少到2到3行代码。





你可能感兴趣的:(Autotools)