Linux应用程序设计
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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目标文件。