《Autotools - GNU Autoconf, Automake与Libtool实践者指南》第二章<了解GNU编码标准>
《Autotools - GNU Autoconf, Automake与Libtool实践者指南》第二章<理解GNU编码标准>
当你完成阅读这一章节,你应该会熟悉普通的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,我会使用下列命令创建一个工程目录结构:
让我们以编译和清理项目作为开始。顶层Makefile仅仅递归地传递请求到src/Makefile。这构成了一个相当常见的编译系统类型,称为递归编译系统,之所以这么命名是因为,make文件递归地调用子目录里的make文件。
当我们设计一个新的make目标是,我们需要考虑它的功能是在工程make文件中被分布还是在一个单一的位置被处理。通常情况下, 经验法则是利用递归编译系统的本性,允许每个目录管理一个过程中它自己的部分。我们已这么做,当我们传递编译jupiter程序的控制到src目录时。然而,从一个目录结构构建一个压缩档案不是一个递归过程。因为这个原因,我们不得不在两个make文件中的一个中执行完整的任务。
全局操作通常在工程目录结构中的顶层make文件中被处理。我们添加dist目标到顶层make文件,如列表2-12所示。
我已将dist目标的功能分成了三个独立地规则,为的是可阅读性,模块化和可维护性。在任何软件工程处理中,这是一个需要遵循的重要的经验法则:从较小的构建大过程,在有用的地方重用较小的过程。
我们并不希望对象文件和可执行文件被存放在压缩档案中,因此我们需要构建一个镜像目录,其中确切地包含我们需要附带的,包括在编译和安装过程中需要的任何文件和任何添加的文档或license文件。不幸的是,这大大增加了单独拷贝命令的使用。
问题是$(distdir)目标是个真实的目标但是没有依赖,这意味着只要它存在,make就认为它是最新的。我们可能添加$(distdir)目标到.PHONY规则来强制在每次make dist时重编译它,但是它不是个伪目标---它是个真实的文件系统目标。合适的方式是确保$(distdir)目标总是被重编译,确保在make试图构建它时不存在。一种完成这个的方式是创建一个总是会执行的伪目标,添加那个目标到$(distdir)目标的依赖链中。这种类型目标的常用名是FORCE,我已在列表2-13中实现了这一想法。
FORCE规则的命令每次都会被执行,因为FORCE是一个伪目标。因为我们使得FORCE是$(distdir)目标的依赖,我们有机会删除任何先前创建的文件和目录,然后开始让make评估是否应该执行$(distdir)的命令。
注意,我在打包规则的rm命令前,并没有使用前导破折号。因为我想知道rm如果有错误---如果它不成功,应该有非常大的错误,因为前面的命令应该已根据此目录创建了一个打包命令了。
尽管不幸,破坏dist目标不是最糟糕的事情。最为糟糕的是,dist目标在工作,但是实际上并没有拷贝所有需要的文件到压缩包中。实际上,远非如此,没有一个错误会产生,因为添加文件到一个工程是一个更为常见的活动,相比移动或删除它们。新文件没有被拷贝,但是dist规则没有注意到差别。
有一种方式来执行在dist目标上的一种自检。我们可以创建另一个称为distcheck的伪目标,做我们用户确切会做的事:解压压缩包和编译工程。我们可以在一个临时目录里用此规则的名利执行这一任务。如果编译过程失败,distcheck目标会终止,告诉我们在发布版中忘记了一些重要的东西。
列表2-14显示了在顶层make文件中需要实现distcheck目标的修改。
distcheck目标依赖于压缩包自己,因此构建压缩包的规则先执行。make然后执行distcheck命令,解压刚构建的压缩包,递归运行递归目录里的make命令。如果那个过程成功,它打印一条表示你的用户不怎么可能在此压缩包使用中遇到问题的信息。
现在所有你得做是,记住,在向大家发布你的压缩包之前,执行make distcheck。
一个良好的编译系统应该包含合适的单元测试。为测试一个构建最为常用的目标是check目标,因此我们会继续,以通常的方式添加它。实际的单元测试应该可能会放在src/Makefile中,因为那是被构建的jupiter可执行文件所在处,因此我们会从顶层make文件向下传递check目标
但是我们在check规则中放什么命令呢?jupiter是个相当简单的程序,它打印一条信息。我们使用grep工具来测试jupiter实际上是输出了这样一条字符串。
列表2-15和列表2-16分别阐述了顶层和src目录下的make文件的修改。
注意check目标依赖于all。我们不能真正测试我们的产品除非他们最新的,反映了已近做的任何源码或构建系统的修改。如果用户需要测试产品,他想要产品存在,并且是最新的。我们能够确保它们存在并且是最新的,通过添加all到check的依赖列表中。
对于我们的编译系统,我们可以做一个更多的提升:我们可以在distcheck规则中添加check到由make执行的目标列表,在make all和make clean命令之间,如列表2-17所示。
现在我们可以运行make distcheck,它会测试软件包附带的整个编译系统。
当创建一个发布版软件包时,可能不是一个内在的递归过程,安装确实是,因此我们允许工程中每个子目录管理它自己组件的安装。为了这么做,我们需要同时修改顶层和src层make文件。修改顶层make文件是简单地:因为没有产品被安装在顶层目录,我们将会以通常的方式传递责任到src/Makefile。
添加install目标的修改显示在列表2-18和2-19中。
我们目前编译系统的另一个问题是,为了安装文件,我们必须做很多材料。大多数Unix系统提供一个系统级程序---通常是一个shell脚本---称为install,允许用户指定被安装文件的多种属性。这一工具的恰当使用,可以简化一些Jupiter的安装,因此当我们添加位置灵活性时,我们可能可以使用install工具。这些修改显示在列表2-20和2-21中。
注意的是,我只在顶层make文件中声明和赋值prefix变量,但是在src/Makefile中引用。我可以这么做是因为我在顶层Makefile中使用修饰语export,这一修饰语输出make变量到shell。这一make的特性允许我们定义我们所有的用户变量到一个明显的位置---顶层Makefile的开始。
注意:GNU make允许你在赋值行使用export关键词,但是这一语法在其它版本的make中不可移植。
我已在makefile中定义prefix变量为/usr/local,make允许你在命令行定义make变量,以这种方式:
有了这一系统,我们的用户可能安装jupiter到任意所选择目录下的bin目录中。实际上,这是我们在列表2-21中添加install -d $(prefix)/bin的理由---如果不存在安装目录bin,这一命令创建它。既然我们允许用户在make命令行定义prefix,我们实际上无法知道用户会把jupiter安装在哪个目录;因此,我们必须为位置不存在的可能性做好准备。
在我们修改安装过程时,现在有两个位置我们需要更新:install和uninstall目标。在第五章,我会向你展示使用GNU Automake如何以一种更为简单的方式重写这个makefile。
在第一章中,我给出了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:项目中一个源码的第一版
创建一个源分布存档
当我们设计一个新的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文件。不幸的是,这大大增加了单独拷贝命令的使用。
强制一个规则运行
问题是$(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)的命令。
前导控制字符
注意,我在打包规则的rm命令前,并没有使用前导破折号。因为我想知道rm如果有错误---如果它不成功,应该有非常大的错误,因为前面的命令应该已根据此目录创建了一个打包命令了。
自动测试一个发布版
尽管不幸,破坏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,它会测试软件包附带的整个编译系统。
安装产品
当创建一个发布版软件包时,可能不是一个内在的递归过程,安装确实是,因此我们允许工程中每个子目录管理它自己组件的安装。为了这么做,我们需要同时修改顶层和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目标
安装选择
我们目前编译系统的另一个问题是,为了安装文件,我们必须做很多材料。大多数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安装在哪个目录;因此,我们必须为位置不存在的可能性做好准备。
卸载一个软件包
... 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工具。