2.11 实训任务6 使用Make管理项目工程
2.11.1 简单Make程序创建
使用VI编辑器,将以下代码输入到文件中。
程序由5个文件组成,源代码如下:
/*main.c*/ #include "mytool1.h" #include "mytool2.h" int main() { mytool1_print("hello mytool1!"); mytool2_print("hello mytool2!"); return 0; } /*mytool1.c*/ #include "mytool1.h" #include <stdio.h> void mytool1_print(char *print_str) { printf("This is mytool1 print : %s ",print_str); } /*mytool1.h*/ void mytool1_print(char *print_str); /*mytool2.c*/ #include "mytool2.h" #include <stdio.h> void mytool2_print(char *print_str) { printf("This is mytool2 print : %s ",print_str); } /*mytool2.h*/ void mytool2_print(char *print_str);
常规法写第一个Makefile:
main:main.o mytool1.o mytool2.o gcc -o main main.o mytool1.o mytool2.o main.o:main.c mytool1.h mytool2.h gcc -c main.c mytool1.o:mytool1.c mytool1.h gcc -c mytool1.c mytool2.o:mytool2.c mytool2.h gcc -c mytool2.c clean: rm -f *.o main
注意上述Makefile文件中gcc和rm之前是一个Tab键,不能使用多个空格代替,在Shell提示符下输入make,执行显示:
gcc -c main.c gcc -c mytool1.c gcc -c mytool2.c gcc -o main main.o mytool1.o mytool2.o
执行结果如下:
[armlinux@lqm makefile-easy]$ ./main This is mytool1 print : hello mytool1! This is mytool2 print : hello mytool2!
这只是最为初级的Makefile,main是最终目标,main.o、mytool1.o和mytool2.o是目标所依赖的源文件。下面是一条命令“gcc -o main main.o mytool1.o mytool2.o”(以Tab键开头)。这个规则表明:
(1)文件的依赖关系,main依赖于main.o、mytool1.o和mytool2.o文件,如果main.o、mytool1.o和mytool2.o的文件日期要比main文件日期要新,或是main不存在,那么依赖关系发生。
(2)如何生成(或更新)main文件。也就是那个GCC命令,其说明了如何生成main这个文件。
目标、依赖、命令的书写格式为:
targets : prerequisites command
或是:
targets : prerequisites ; command command
小知识:command是命令行,如果其不与“target:prerequisites”在一行,那么,必须以Tab键开头,如果和prerequisites在一行,那么可以用分号作为分隔。
clean是一个伪目标,既然生成了许多编译文件,也应该提供一个清除它们的“目标”以备完整地重编译而用(以“make clean”来使用该目标)。
因为并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。只有通过显式地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然就失去了其“伪目标”的意义了。
当然,为了避免和文件重名的这种情况,可以使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。
.PHONY : clean
只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make clean”这样。于是整个过程可以这样写:
.PHONY: clean clean: rm *.o temp
在这个Makefile中有4 个目标体(target),分别为main、main.o、mytool1.o和mytool2.o,其中第一个目标体的依赖文件就是后三个目标体。如果用户使用命令“make main”,则Make管理器就是找到main目标体开始执行。这时,Make会自动检查相关文件的时间戳。首先,在检查“main.o”、“mytool1.o”、“mytool2.o”和“main”4 个文件的时间戳之前,它会向下查找那些把“main.o”或“mytool1.o”或“mytool2.o”作为目标文件的时间戳。比如,“mytool1.o”的依赖文件为“mytool1.c”、“mytool1.h”。如果这些文件中任何一个的时间戳比“mytool1.o”新,则命令“gcc–c mytool1.c”将会执行,从而更新文件“mytool1.o”。在更新完“mytool1.o”或“mytool2.o”或“main.o”之后,Make会检查最初的“main.o”、“mytool1.o”、“mytool2.o”和“main”文件,只要文件“main.o”或“mytool1.o”或“mytool2.o”中的任何一个文件时间戳比“main”新,则第2行命令就会被执行。这样,Make就完成了自动检查时间戳的工作,开始执行编译工作。这也就是Make工作的基本流程。
2.11.2 Makefile改进
现在,对上面的Makefile进行逐步改进,改进过程如下:
1.改进一:使用变量
OBJ=main.o mytool1.o mytool2.o make:$(OBJ) gcc -o main $(OBJ) main.o:main.c mytool1.h mytool2.h gcc -c main.c mytool1.o:mytool1.c mytool1.h gcc -c mytool1.c mytool2.o:mytool2.c mytool2.h gcc -c mytool2.c clean: rm -f main $(OBJ)
Makefile中的变量分为用户自定义变量、预定义变量、自动变量及环境变量。如上例中的OBJ就是用户自定义变量,自定义变量的值由用户自行设定,而预定义变量和自动变量在Makefile中都是经常会出现的变量,其中部分有默认值,也就是常见的设定值,当然用户可以对其进行修改。
在Makefile中的定义的变量,就像是C/C++语言中的宏一样,它代表了一个文本字串,在Makefile执行的时候会自动原模原样地展开在所使用的地方。其与C/C++所不同的是,用户可以在Makefile中改变其值。在Makefile中,变量可以使用在“目标”、“依赖目标”、“命令”或是Makefile的其他部分中。
预定义变量包含了常见编译器、汇编器的名称及其编译选项。Makefile中常见预定义变量及其部分默认值如表2-24所示。
表2-24 Makefile中常见预定义变量
变量在声明时需要给予初值,而在使用时,需要在变量名前加上“$”符号,但最好用小括号“()”或是大括号“{}”把变量给包括起来。如果要使用真实的“$”字符,那么需要用“$$”来表示。
在定义变量的值时,在Makefile中有两种方式来定义变量的值。
先看第一种方式,也就是简单地使用“=”号,在“=”左侧是变量,右侧是变量的值,右侧变量的值可以定义在文件的任何一处,也就是说,右侧中的变量不一定非要是已定义好的值,也可以使用后面定义的值。或者使用Makefile中的另一种用变量来定义变量的方法。这种方法使用的是“:=”操作符。
一般在书写Makefile时,各部分变量引用的格式如下:
(1)make变量(Makefile中定义的或者是make的环境变量)的引用使用“$(VAR)”格式,无论“VAR”是单字符变量名还是多字符变量名。
(2)出现在规则命令行中的Shell变量(一般为执行命令过程中的临时变量,它不属于Makefile变量,而是一个Shell变量),引用使用Shell的“$tmp”格式。
(3)对出现在命令行中的make变量同样使用“$(CMDVAR)”格式来引用。
2.改进二:使用自动推导
CC = gcc OBJ = main.o mytool1.o mytool2.o make: $(OBJ) $(CC) -o main $(OBJ) main.o: mytool1.h mytool2.h mytool1.o: mytool1.h mytool2.o: mytool2.h .PHONY: clean clean: rm -f main $(OBJ)
让Make自动推导,只要看到一个.o文件,它就会自动地把对应的.c文件加到依赖文件中,并且gcc -c *.c也会被推导出来,所以Makefile就简化了。
3.改进三:自动变量($^、$<、$@)的应用
CC = gcc OBJ = main.o mytool1.o mytool2.o main: $(OBJ) $(CC) -o $@ $^ main.o: main.c mytool1.h mytool2.h $(CC) -c $< mytool1.o: mytool1.c mytool1.h $(CC) -c $< mytool2.o: mytool2.c mytool2.h $(CC) -c $< .PHONY: clean clean: rm -f main $(OBJ)
命令中的“$<”和“$@”是自动化变量,“$<”表示所有的依赖目标集,“$@”表示目标集。
4.改进四:使用函数
CC = gcc CFLAGS = -Wall -c LDFLAGS = -lpthread SRCS = $(wildcard *.c) OBJS = $(patsubst %c,%o,$(SRCS)) TARGET = main .PHONY: all clean //“.PHONY”表示,all和clean是个伪目标文件。 all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(LDFLAGS) -o $@ $^ %.o: %.c $(CC) $(CFLAGS) -o $@ $< clean: @rm -f *.o $(TARGET)
在这个Makefile中使用wildcard和patsubst函数来处理变量,从而使命令或是规则更为灵活和具有智能。Makefile所支持的函数也不算很多,不过已经足够操作了。函数调用后,函数的返回值可以当做变量来使用。wildcard和patsubst函数的作用分别是扩展通配符和替换通配符。SRCS = $(wildcard *.c)等于指定编译当前目录下所有.c文件。在$(patsubst %.c,%.o,$(SRCS) )中,patsubst把$(SRCS)中的变量符合后缀是.c的全部替换成.o。
通过上面的例子可以得到如下总结。
Makefile文件保存了编译器和链接器的参数选项,还表述了所有源文件之间的关系(源代码文件需要的特定的包含文件,可执行文件要求包含的目标文件模块及库等)。创建程序(Make程序)首先读取Makefile文件,然后再激活编译器、汇编器、资源编译器和链接器以便产生最后的输出,最后输出并生成的通常是可执行文件。创建程序利用内置的推理规则来激活编译器,以便通过对指定源文件的编译来产生指定的obj目标文件。