在第一章中,我给出了GNU Autotools和一些资源的概述,可以帮助降低所需要的学习曲线来掌握它们。在这一章节中,我们会退一小步,调查可用于任何工程的项目组织技术,不仅仅使用Autotools。
$ 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文件。大家都知道一个成功的开源软件项目的关键是演进。开始比较下,根据需要增长---当你有时间和倾向。
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
#include
int main(int argc, char * argv[])
{
printf("Hello from %s!\n", argv[0]);
return 0;
}
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
...
$(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
...
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
...
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
...
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."
...
...
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
...
prefix=/usr/local
export prefix
all clean check install jupiter:
cd src && $(MAKE) $@
...
...
install:
install -d $(prefix)/bin
install -m 0755 jupiter $(prefix)/bin
...
$ sudo make prefix=/usr install
...
记住,在命令行定义的变量覆盖定义在makefile中定义的。因此,用户需要安装jupiter到/usr/bin目录的,现在就可以有在make命令行指定这一方面的选项。
...
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
现在让我们添加一些代码到我们的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."
...
测试首先生成了一个称为remaining的数值,代表了安装目录中常规文件的数目。如果这个值不是零,它会打印一条信息到控制台,显示有多少个文件被uninstall命令落下,然后它以错误退出。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文件和编译系统,你应该在编译和安装过程中尽可能多地支持它们。列表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
...
没有多层次修改前缀变量的能力,配置文件最终会在/usr/etc,因为$(sysconfdir)的默认值是$(prefix)/etc。
...
install:
install -d $(DESTDIR)$(bindir)
install -m 0755 jupiter $(DESTDIR)$(bindir)
uninstall:
-rm $(DESTDIR)$(bindir)/jupiter
...
...
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
不要关心软件包管理器的文件格式。只要注意通过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 在编译时把替代路径放入源码
#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
在这个例子中,我们的用户已经决定使用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
...
?=操作符是一个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行代码。