一、概述
现在,免费的操作系统Linux的使用者越来越多。Linux的爱好者们也许要在Linux软件开发上一展身手,不过很遗憾,Linux平台上还没有一个像Visual C++或Borland Delphi这样功能强大的开发工具,在Linux平台上编写和调试程序还基本上使用的是命令行方式。
在命令行方式下,如果源程序只有一两个文件,那么还可以使用特定的语言编译器进行编译和链接,但是,一旦源程序文件多达数十个,甚至上百,再使用语言编译器进行编译链接则是一件很繁琐的事。在这种情况下,就需要使用make工具。而使用make工具时则需要编写Makefile(或makefile)的文件,以指示编译器的工作。
二、Makefile文件的基本格式
Makefile文件中最核心的部分是规则部分(rules),规则规定了文件之间的依赖关系,即指示编译器在产生一个目标文件时需要编译那几个源文件。
规则(rules)的基本格式如下:
TARGET:DEPENDENCIES
COMMAND
注意:COMMAND前面必须是一个tab键,而不是空格!
下面分别介绍一下TARGET,DEPENDENCIES,COMMAND的意义:
TARGET:目标。经常是一个文件的名字或者一个动作。常见的是一个可执行文件或者obj文件,而常见的动作则是"clean"或"clear"等。
DEPENDENCIES:依赖关系。经常是一些文件名,作为TARGET的输入,即产生TARGET所以来的文件。
COMMAND:命令。它就是make要做的动作。经常是编译或者链接命令,有时可能是一些shell命令,完成一些特殊的操作,例如ar,rm等。
下面举一个简单的例子:
用户编写了以下源文件:printline、printline.c、main.h、main.c。文件内容如下:
//printline.h
#if !defined(PRINTLINE_H)
#define PRINTLINE_H
void printline();
#endif
//printline.c
#include <stdio.h>
#include "printline.h"
void printline()
{
int i;
for(i=0;i<15;i++)
printf("-");
printf("\n");
}
//main.h
#if !defined(STRING)
#define STRING "Test Makefile\n"
#endif
//main.c
#include <stdio.h>
#include "printline.h"
#include "main.h"
int main()
{
printline();
printf(STRING);
printline();
return 0;
}
按照上述所讲的规则,Makefile文件的编写如下:
test:printline.o main.o
gcc -o test printline.o main.o
printline.o:printline.h printline.c
gcc -c printline.c
main.o:main.h main.c
gcc -c main.c
clean:
rm -f printline.o main.o
键入make命令后,屏幕显示:
gcc -c printline.c
gcc -c main.c
gcc -o test printline.o main.o
然后在当前目录下就有一个可执行文件test。
键入make clean后,屏幕显示:
rm -f printline.o main.o
在察看当前目录,果然上述文件以被删除。
由这个例子,读者可看出make的执行过程。当键入make命令时,它会在当前目录中搜索makefile或Makefile文件。如果make没有参数,它会搜索makefile中的第一个rule。比如上例中,它搜索到test这条rule,而test依赖于main.o和printline.o,于是它接着搜索,关于main.o和util.o的rule。果然,它先找到了以printline.o为target的rule,而这个rule中printline.o依赖于printline.h和printline.c。因为printline.h和printline.c不是任何rule的TARGET,这时候make就可以执行一个COMMAND了:gcc -c printline.c。同样道理,make接着执行gcc -c main.c。最后,make执行gcc -o test printline.o main.o,生成了test文件
如果你敲一个make clean,它会在makefile中搜索TARGE为clean的rule,然后执行这个rule中的COMMAND,即
rm -f printline.o main.o。
总而言之,一个程序的完成包含了顺序正确的链结(chain)。一般而言,你只要要求make去建造链结中最后的那个规则即可。make会透过依赖关系链结(在依赖关系中所指定的那些必备规则所构成的树状结构构成附属规则链结),自己回朔追踪(traces back,也就是往树状结构的叶部方向)这个链结,然后找出哪些命令必须被执行。最后,make会慢慢在链结中前进(moves forward,就是往树状结构的根部移动),执行每个建造目标所必须要有的命令直到目标建立完成(或被更新)。因为具有这种特性,make是一个使用反项链结法(backward-chaining:在人工智能领域中,一种搜索方法,它的搜索方向是由目标状态开始,然后向初始状态前进,最后再慢慢回来)的工具。
再说一下make的更新功能。如果在你用make编译后,未修改任何源代码,再键入make命令。屏幕显示:
make:test is up to date.
说明用户没有对test作任何修改。
如果仅仅是修改了main.c的源代码,键入make后,屏幕显示:
gcc -c make.c
gcc -o test printline.o main.o
可以看出,系统并没有对printline.c进行编译,只是对修改过的文件进行了编译。
接着,通过例子说一下固有规则(implicit rule)的概念。上例中的Makefile文件可以利用固有规则简写如下:
test:printline.o main.o
gcc -o test printline.o main.o
printline.o:printline.h
main.o:main.h
clean:
rm -f printline.o main.o
这个Makefile的功效和上面的第一中写法的功能完全一样。但是显然它要简单许多,因为它利用了固有规则。
初学者一个常常犯的错误就是在每个命令列的开头省略了tab字符(在每个命令列中按空格键充当tab字符也不行,因为make无法辨别出这就是tab字符),在这种情况下,就算出了错误,make也无法提供有用的讯息。因为make是靠开头的那个tab字来辨识命令列,所以一定要注意不要在其他不是命令列的列之前加上tab字符,如果你把tab当作第一个字符加在目标列,注解列,或者是一个空白列之前,你都会得到出错信息。tab字符可以用在每一列的任何地方,只有在每一列的第一个字符才有上述的限制。
如果用户想要检查Makefile文件中的tab字符,你可以下指令
cat -v -t -e Makefile
在这里-v与-t会使得文件中的每一个tab字符以^I的方式显示出来,而-e会使得每一列的最后以$的样子印出来,所以你可以看出在每一列的结束之前有几个空格。
在Makefile文件中,用户可以打很长一串指令,如果已经到了文字编辑器的右边界,你可以在到达右边界之前放入一个斜线(\)符号,在第二行中再敲入一个斜线符号(\),然后可以接着输入未输入完的命令了,但是必须保证斜线符号和新的一行之间不要有空格。由斜线符号所连续的每一行都会被当作单独一行来分析(parsing)。
在Makefile文件中同样可以加入注释,注释行是以#符号开头。在make的执行过程中,会忽略Makefile文件中的以#符号开头,到这一行结尾之间的字符。当然,也会忽略空白行。
三、使用变量和通配符
Makefile文件中的变量与shell脚本中的变量的作用和用法类似,下面举例说明。
上例中的Makefile文件可修改如下:
COMPILER=gcc
objects=printline.o main.o
test:$(objects)
$(COMPILER) -o test $(objects)
printline.o:printline.h
main.o:main.h
clean:
rm -f $(objects)
再上例中,使用了两个变量--COMPILER和objects。他们的值gcc和printline.o main.o。如果用变量的值替换掉变量,就和起初的Makefile文件一样。
使用变量的目的就是方便移植和简化管理。例如,将上面的小程序移到另一台机器上,那台机器没有gcc编译器,只有cc这个编译器,如果使用初始的Makefile文件,则需要将所有的gcc均改为cc。而使用变量的话,则只需将COMPILER=gcc改为COMPILER=cc即可。使用object的目的在于简化管理,如果有如下的规则:
target1.o:a.h a.c
target2.o:a.h a.c b.h b.c
如果使用变量的话,则可写成:
sources=a.h a.c
target1.o:$(sources)
sources+=b.h b.c
#这时sources=a.h a.c b.h b.c
target2:$(sources)
从上例中,可能看不出使用变量的优越性,但是当源文件的数量非常多的时候,就可以看出使用变量的优越性,免去重复输入很多的字符。
Makefile中的变量分为两种,分别叫递归扩展变量(recursively expanded variable)和简单扩展变量(simply expanded variable)。前者用“=”赋值,后者用“:=”赋值。从字面上,大家可以看出两者的区别:前者是递归扩展,后者是简单扩展。因此要避免出现以下的错误:
FOO=$(FOO) -O
这会导致一个无限循环,make会认为这是一个错误并报告。
再介绍一下自动变量(automatic variables)。自动变量是系统预先定义的,代表一些特殊的含义。下面介绍一些常见的:
“$@”:rule中的target的文件名。
“$<”:rule中的dependencies中的第一个文件名。
“$^”:rule中的所有的dependencies文件名。
据一个简单的例子:有这样一条规则
foo:foo.c
gcc $< -o $@
当make foo时将执行:
gcc foo.c -o foo
最后简单介绍一下通配符在Makefile中的应用。在Makefile中的通配符通常有'*','?','[...]','~'。这些通配符和bash中的意义类似。“*.c”就表示目录中所有以“.c”结尾的文件;“~/bin”则表示(假设用户是foo)“/home/foo/bin”。
使用通配符时应当深思熟虑,看下面这条规则:
objects=*.o
foo:$(objects)
gcc -o foo $(objects)
这条规则初看起来没有什么毛病,其实不然。变量objects的值是“*.o”,因此foo依赖的文件是该目录下所有的以“.o”结尾的文件。但是,当你使用make clean删除掉目录下所有的.o文件后,你再次make foo,会出现什么情况呢?由于该目录下没有以'.o'结尾的文件,make将认为foo依赖的文件是一个叫做“.o”的文件,然后,make会给出一个错误信息。
四、Makefile文件的自动生成
上述介绍的仅仅是入门级的知识,详细的介绍请见GNU make的文档,不过,这份文档一共有几百页。在这份文档中,规定了书写Makefile文件的一些标准和规范,不过,这些标准和规范不但复杂而且经常变更,给程序员增添了不少麻烦。
下面介绍使用自动工具autoscan、autoconf和automake来自动生成Makefile文件。其实这三个自动工具不但可以生成Makefile文件,还可用来发布软件。熟悉Linux的用户可能知道,在Linux下安装一个应用程序时,一般先运行脚本configure,然后用make来编译源程序,在运行make install,最后运行make clean删除一些临时文件。使用上述三个自动工具,就可以生成configure脚本。运行configure脚本,就可以生成Makefile文件,然后就可以运行make、make install和make clean。
使用上述自动工具,用户只需定义一些宏(macro),然后交给自动工具处理就行了。
在介绍自动工具之前请先检查一下系统中指否安装了以下工具:
1. GNU Automake
2. GNU Autoconf
3. GNU m4
4. perl
以上文中所举的例子为例来介绍自动工具的使用方法:
1. 使用autoscan产生一个configure.scan的文件,此文件用来产生configure.in的蓝本。
$ autoscan
运行autoscan之后,会产生一个configure.scan的文件,编辑这个文件,并更名为configure.in。编辑后的内容如下:
dnl Process this file with autoconf to produce a configure script.
AC_INIT(printline.h)
AM_INIT_AUTOMAKE(test,1.0)
dnl Checks for programs.
AC_PROG_CC
dnl Checks for libraries.
dnl Checks for header files.
dnl Checks for typedefs, structures, and compiler characteristics.
dnl Checks for library functions.
AC_OUTPUT(Makefile)
2. 再运行aclocal和autoconf,产生aclocal.m4和configure文件。
$ aclocal
$ autoconf
运行aclocal后会产生aclocal.m4文件,然后autoconf会根据configure.in和aclocal.m4产生configure脚本。
Autoconf是用来产生configure脚本的工具。configure是一个shell脚本,它可以自动设定源程序以符合各种不同平台上Unix系统的特性,并且根据系统叁数及环境产生合适的Makefile文件或是C的头文件(header file),让源程序可以很方便地在这些不同的平台上被编译连接。Autoconf会读取configure.in文件并产生configure这个shell 脚本。configure.in文件的内容是一连串GNU m4宏,这些宏经过autoconf处理后会变成检查系统特征的shell脚本。
configure.in内宏的顺序并没有特别的规定,但是每一个configure.in文件必须在所有宏前加入AC_INIT宏,然后在所有宏的最后面加上AC_OUTPUT宏。我们可先用autoscan扫描源程序并产生一个configure.scan文件,再对configure.scan做些修改成configure.in文件。在范例中所用到的宏如下:
dnl:这个宏后面的字不会被处理,可视为注解。
AC_INIT(unique-file-in-source-dir):处理所有命令行参数并且寻找源代码目录。unique-file-in-source-dir是一些在包的源代码目录中文件; configure在目录中检查这些文件是否存在以确定该目录是否包含源代码。
AM_INIT_AUTOMAKE(PACKAGE,VERSION):这是使用Automake所必备的宏,PACKAGE是我们所要产生软件的名称,VERSION是版本编号。
AC_PROG_CC:检查系统可用的C编译器,如果原始程序是用C写的就需要这个宏。对于其他语言写成程序则需要其他的宏,例如,C++语言需要:AC_PROG_CXX,FORTRAN语言需要:AC_PROG_F77等。
AC_OUTPUT(FILE):设定configure所要产生的文件,如果是Makefile的话,configure便会把它检查出来的结果带入Makefile.in文件然后产生合适的Makefile。
实际上,使用Automake时,还须要一些其它的宏,这些额外的宏用aclocal来帮我们产生。执行aclocal会产生aclocal.m4文件,如果没有特别的用途,我们可以不必修改它。有了configure.in及aclocal.m4两个文件后,便可以执行autoconf来产生configure脚本了。
3. 编辑Makefile.am文件,用来产生Makefile.in文件。Makefile.am内容如下:
AUTOMAKE_OPTIONS=foreign
bin_PROGRAMS=test
test_SOURCES=printline.h printline.c main.h main.c
4. 运行automake -add-missing,运行后会产生Makefile.in文件。
$ automake -add-missing
Automake会根据configure.in中的宏把Makefile.am转成Makefile.in文件。Makefile.am文件中定义了所要产的目标:
AUTOMAKE_OPTIONS:设定automake的选项。Automake主要是帮助开发GNU软件的人员维护软件,所以在执行automake时,会检查目录下是否存在标准GNU软件包中应具备的文件,例如NEWS、AUTHOR、ChangeLog等文件。设成foreign时,automake会改用一般软件包的标准来检查。
bin_PROGRAMS:定义我们所要产生的可执行文件文件名。如果要产生多个可执行文件,每个可执行文件名用空格分开。
test_SOURCES:定义test这个可执行文件所需要的源文件。多个源文件之间用空格分开。如果我们定义多个可执行文件,则对每个可执行文件都要定义自己的filename_SOURCES。
编辑好Makefile.am文件,就可以用automake --add-missing来产生Makefile.in。加上--add-missing选项是告诉automake顺便帮我们加入包装一个软件包所必备的档案。Automake产生出来的Makefile.in文件是完全符合GNU Makefile的标准和规范,我们只要执行configure脚本就可以产生合适的Makefile文件了。
5. 这时,就可运行configure脚本了,运行configure脚本,就可产生出符合GNU规范的Makefile文件了:
$ ./configure
6. 到此时,就可以运行make进行编译,在运行make install进行安装了,最后运行make clean删除临时文件。
$ make
$ make install (注:运行这个要有足够的权限)
$ make clean
利用configure所产生的Makefile文件有几个预设的目标可供使用,其中几个重要的简述如下:
make all:产生我们设定的目标,即此范例中的可执行文件。只打make也可以,此时会开始编译原始码,然后连结,并且产生可执行文件。
make clean:清除编译产生的可执行文件及目标文件(object file,*.o)。
make distclean:除了清除可执行文件和目标文件外,把configure所产生的Makefile也清除掉。
make install:将程序安装至系统中。如果原始码编译无误,且执行结果正确,便可以把程序安装至系统预设的可执行文件存放路径。如果用bin_PROGRAMS宏的话,程序会被安装至/usr/local/bin这个目录。
make dist:将程序和相关的档案包装成一个压缩文件以供发布。执行完在目录下会产生一个以PACKAGE-VERSION.tar.gz为名称的文件。PACKAGE和VERSION这两个变数是根据configure.in文件中AM_INIT_AUTOMAKE(PACKAGE,VERSION)的定义。在此范例中会产生test-1.0.tar.gz的档案。
make distcheck:和make dist类似,但是加入检查包装后的压缩文件是否正常。这个目标除了把程序和相关文件包装成tar.gz文件外,还会自动把这个压缩文件解开,执行configure,并且进行make all 的动作,确认编译无误后,会显示这个tar.gz文件可供发布了。这个检查非常有用,检查过关的包,基本上可以给任何一个具备GNU开发环境-的人去重新编译。
五、小结
本文介绍了Makefile文件的基本编写方法,和Makefile自动生成工具autoscan、autoconf和automake的基本的使用方法。当然这些还远远不够,用户如想详细了解Makefile文件的编写方法,请参见GNU make的文档,想详细了解自动工具的使用方法,请参阅autoconf和automake的info。
(本文的例子在RedHat Linux 6.1下调试通过,)
|