《Autotools - GNU Autoconf, Automake与Libtool实践者指南》第二章<了解GNU编码标准>

《Autotools - GNU Autoconf, Automake与Libtool实践者指南》第二章<理解GNU编码标准>


  在第一章中,我给出了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 <stdio.h>
#include <stdlib.h>
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工具。