在第一章中,我给出了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
- $
让我们以编译和清理项目作为开始。顶层Makefile仅仅递归地传递请求到src/Makefile。这构成了一个相当常见的编译系统类型,称为递归编译系统,之所以这么命名是因为,make文件递归地调用子目录里的make文件。
- all clean jupiter:
- cd src && $(MAKE) $@
- .PHONY: all clean
- all: jupiter
- jupiter: main.c
- gcc -g -O0 -o $@ main.c
- clean:
- -rm jupiter
- .PHONY: all clean
- #include <stdio.h>
- #include <stdlib.h>
- int main(int argc, char * argv[])
- {
- printf("Hello from %s!\n", argv[0]);
- return 0;
- }
创建一个源分布存档
当我们设计一个新的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
我已将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
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
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
- ...
- check: all
- ./jupiter | grep "Hello from .*jupiter!"
- @echo "*** ALL TESTS PASSED ***"
- ...
- .PHONY: all clean 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."
- ...
现在我们可以运行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
- ...
- install:
- cp jupiter /usr/bin
- chown root:root /usr/bin/jupiter
- chmod +x /usr/bin/jupiter
- .PHONY: all clean check install
安装选择
我们目前编译系统的另一个问题是,为了安装文件,我们必须做很多材料。大多数Unix系统提供一个系统级程序---通常是一个shell脚本---称为install,允许用户指定被安装文件的多种属性。这一工具的恰当使用,可以简化一些Jupiter的安装,因此当我们添加位置灵活性时,我们可能可以使用install工具。这些修改显示在列表2-20和2-21中。
- ...
- prefix=/usr/local
- export prefix
- all clean check install jupiter:
- cd src && $(MAKE) $@
- ...
- ...
- install:
- install -d $(prefix)/bin
- install -m 0755 jupiter $(prefix)/bin
- ...
注意的是,我只在顶层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
- ...
有了这一系统,我们的用户可能安装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
- ...
- uninstall:
- -rm $(prefix)/bin/jupiter
- .PHONY: all clean check install uninstall
在我们修改安装过程时,现在有两个位置我们需要更新: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."
- ...
注意,在$$(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."
- ...
我并不想通过打印嵌入的echo声明来提醒人们,除非它应该被执行时,因此我用@前缀整个测试语句,从而使make不会打印代码到stdout。因为make认为这五行代码是一个单一的命令(代码中用\拼接),唯一抑制打印echo申明的方法是抑制打印整个命令。
这里的代码只是检查常规文件。如果你的安装过程创建了任何软链接,如果它们被落下,这个测试程序不会注意到。在安装过程中构建的目录结果被落下在原地,因为检查代码不知道一个子目录是属于系统的还是工程的。uninstall规则命令可以知道哪些目录是工程相关的,并合适地删除它们,但是我不想添加工程相关知识到distcheck测试。
支持标准目标和变量
除了我已提到的,GNU编码标准列出了一些重要的目标和变量,你应该在你的项目中去支持---主要是因为你的用户会期望有它们的支持。
对GCS中的一些章节应该持怀疑态度,除非你在一个GNU发起的项目上工作。例如,你很可能不太关心第五章中关于C源代码格式的建议。你的用户当然也不会在意,因此你可以使用你希望的任何源代码格式风格。
那并不是说对于非GNU开源项目,第五章中的所有部分都是没有价值的。例如,“系统类型之间的可移植性”和“CPU之间的可移植性”子部分,提供了关于C源代码可移植性方面极好的信息。“国际化”子部分给你一些关于使用GNU软件国际化你的项目的有用建议。
第六章讨论了GNU方式的文档,第六章的一些部分描述了项目中经常会有的多种顶层文本文件,例如AUTHORS,NEWS, INSTALL, README和ChangeLog文件。在任何声誉良好的项目中,这些都是受过良好熏陶的开源软件用户期望看到的所有信息点。
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
标准变量
你应该支持的变量如下面表中所示。大多数变量以多个方面被定义,最终只有一个: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
添加位置变量到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
- ...
- ...
- install:
- install -d $(bindir)
- install -m 0755 jupiter $(bindir)
- uninstall:
- -rm $(bindir)/jupiter
- ...
尽管我们在src/Makfile中只是用了bindir,我们必须export prefix,exec_prefix和bindir,因为bindir根据exec_prefix形式定义,后者根据prefix定义。当我们运行install命令时,首先展开bindir到$(exec_prefix)/bin,然后到$(prefix)/bin,最终到/usr/local/bin。因此,src/Makefile在此过程中需要访问所有三个变量。
- $ make prefix=/usr sysconfdir=/etc install
- ...
项目进入Linux发行版
在GCS的第七部分,包含了一小部分来讨论支持分阶段安装。为支持分阶段安装,所有你需要的只是一个称为DESTDIR的变量,作为一类超级前缀到你所有的安装产品。列表2-28列出了需要的修改。
- ...
- install:
- install -d $(DESTDIR)$(bindir)
- install -m 0755 jupiter $(DESTDIR)$(bindir)
- uninstall:
- -rm $(DESTDIR)$(bindir)/jupiter
- ...
你不必定义一个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."
- ...
在install和uninstall命令中修改prefix到DESTDIR允许我们恰当地测试一个完整的安装目录等级,如我们马上会看到的。
在这一点,一个RPM特定文件可以提供下列文本作为Jupiter软件包的安装命令:
- %install
- make prefix=/usr DESTDIR=%BUILDROOT install
你可能会疑惑为何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 在编译时把替代路径放入源码
- #ifndef CFGDIR
- # define CFGDIR "/etc"
- #endif
- const char cfgdir[FILENAME_MAX] = CFGDIR;
在后面的代码中,你可能会使用C全局标量cfgdir来访问应用程序的配置文件。
Linux发布版软件包管理器通常为在RPM特定文件中的编译和安装使用不同的前缀覆盖。在编译阶段,实际运行时目录被人工编码到可执行文件,使用如列表2-32所示的命令行。
- %build
- %setup
- ./configure prefix=/usr sysconfdir=/etc
- make
注意伴随着prefix,我们已明确指定sysconfdir,因为,如前面讲到的,系统配置目录通常是在系统前缀目录结构的外面。软件包管理器安装这些可执行文件到一个阶段性目录,从而使在编译二进制软件包时,可以将它们拷贝出它们的安装路径。相应的安装命令可能像列表2-33中所示。
- %install
- make DESTDIR=%BUILDROOT% install
在安装期间使用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
- . . .
你可以在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
- ...
这时可行的,因为make工具允许这样的变量被命令行的选项覆盖。例如,为了切换编译器和设置一些编译器的命令行选项,用户只需输入下列信息:
- $ make CC=gcc3 CFLAGS='-g -O2' CPPFLAGS=-dtest
- $ CC=gcc3 CFLAGS='-g -O2' CPPFLAGS=-dtest make
- ...
- CFLAGS ?= -g -O0
- ...
?=操作符是一个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行代码。