MAKEFILE教程
之后要尝试做一些项目,在加上前一段时间接触PA,发现自己对自动构建项目一类的操作实在是一知半解,而且之后也需要要尝试自动化编译一些大型的源码,所以学习一下makefile的基本使用,完善一下自己对工具链的认知。这里我看的教程是廖雪峰的makefile入门
Makefile基础
在Linux中我们使用make命令时,他就会在当前目录下找一个名为Makefile的文件,并根据里面的内容进行自动化的执行。我们以下面这个需求为例子:
1 2 a.txt + b.txt -> m.txt m.txt + c.txt -> x.txt
上述逻辑我们编写makefile
规则
Makefile由各种规则构成,每一条规则需要指出一个目标文件和若干个依赖文件,以及用于生成目标文件的命令,例如我们想要生成m.txt则规则如下:
1 2 3 m.txt: a.txt b.txt cat a.txt b.txt > m.txt
其中#用来注释,一条规则的格式如上,Tab后使用命令来实现目标文件
现在我们就可以完整的实现上述的规则:
1 2 3 4 5 x.txt: m.txt c.txt cat m.txt c.txt > x.txt m.txt: a.txt b.txt cat a.txt b.txt > m.txt
我们可以尝试执行一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 ylin@Ylin:~/Program/test$ make cat a.txt b.txt > m.txt cat m.txt c.txt > x.txt ylin@Ylin:~/Program/test$ ls a.txt b.txt c.txt Makefile m.txt x.txt ylin@Ylin:~/Program/test$ cat x.txt I am a.txt I am b.txt I am c.txt ylin@Ylin:~/Program/test$ cat m.txt I am a.txt I am b.txt ylin@Ylin:~/Program/test$
make默认执行第一条规则,也就是创建x.txt,但是由于x.txt依赖的文件m.txt不存在(另一个依赖c.txt已存在),故需要先执行规则m.txt创建出m.txt文件,再执行规则x.txt。执行完成后,当前目录下生成了两个文件m.txt和x.txt。
所以我们可以知道,实际上Makefile就是一堆规则(即你些的目标文件和依赖),当满足规则时,就调用规则后的命令,创建出一个新的目标文件
把默认的规则放在第一条,其他规则的顺序makefile会自动判断依赖。make会把每次执行的命令输出出来,便于我们观察调试。
如果我们在不对任何目标文件进行修改的情况下,我们在此使用make就会得到:
1 2 ylin@Ylin:~/Program/test$ make make: 'x.txt' is up to date.
make会根据文件的创建和修改时间来判断是否应该更新一个目标文件,例如我们这里只修改c.txt,并不会触发对m.txt的更新,因为他的依赖文件没有发生改变:
1 2 3 4 5 6 7 ylin@Ylin:~/Program/test$ make cat m.txt c.txt > x.txt ylin@Ylin:~/Program/test$ cat x.txt I am a.txt I am b.txt I am c.txt (modified) ylin@Ylin:~/Program/test$
make只会重新编译那些依赖被修改,或者是尚未完成的部分,重新编译的过程并不是每一条命令都会执行,make只会选择必要的部分执行,我们称这种编译为增量编译。能否正确的实现增量编译,取决于我们编写的规则。
伪目标
为了进一步的便于自动化的构建,有时候我们会需要定义一些常用的规则。例如在我们使用make之后,我们自动生成了m.txt和x.txt,现在我们可以定义一个规则用于清理这些生成的文件:
1 2 3 clean: rm -f m.txt rm -f x.txt
然后我们可以通过make clean来调用这个规则:
1 2 3 4 5 6 7 ylin@Ylin:~/Program/test$ ls a.txt b.txt c.txt Makefile m.txt x.txt ylin@Ylin:~/Program/test$ make clean rm -f m.txt rm -f x.txt ylin@Ylin:~/Program/test$ ls a.txt b.txt c.txt Makefile
但是make这里实际上是把clean当作一个目标文件,我们使用make clean规则时,make检查到没有目标文件clean,于是调用命令尝试构建目标文件,但是clean文件不会被生成,所以我们总可以使用它。可是如果目录中有一个clean文件怎么办呢?make认为clean已经被构建了,就不会再使用命令。为了解决这个问题,我们希望make不要将clean视作文件,我们可以添加一个标识:
1 2 3 4 .PHONY : cleanclean: rm -f m.txt rm -f x.txt
此时,clean就不再被视作一个文件,而是伪目标。一般大型项目会有clean,install一类的常用的伪目标规则,方便用户快速的构建一些任务
执行多条命令
一个规则可以有多条命令:
运行结果如下: 1 2 3 4 5 6 ylin@Ylin:~/Program/test$ make cd pwd /home/ylin/Program/test cd .. pwd /home/ylin/Program/test
我们发现命令cd ..并没有修改当前目录,导致每次输出的pwd都是一样的,这是因为make针对每条指令都会创建一个独立的shell环境,所以命令之间无法互相影响。但是我们可以用以下方法实现
1 2 cd_ok: pwd; cd ..; pwd;
我们查看新的执行结果:
1 2 3 4 ylin@Ylin:~/Program/test$ make cd_ok pwd; cd ..; pwd; /home/ylin/Program/test /home/ylin/Program
当然也可以再;后加\便于分行阅读:
1 2 3 4 cd_ok: pwd; \ cd ..; \ pwd
我们需要注意,在shell中;代表无论当前命令是否生效,都会执行下一个命令。与其相反的一个执行多条命令的语法是&&,当前面的命令执行失败时,后续的命令就不会再继续执行了
控制打印
默认情况下,make会打印出执行的每一条命令,如果我们不想打印某一条命令,我们只需要在命令前面加上@,告诉make不打印该命令:
1 2 3 no_output: @echo 'no display' echo 'will display'
执行结果如下:
1 2 3 4 ylin@Ylin:~/Program/test$ make no_output not display echo 'will display' will display
控制错误
make在执行命令时,会检查每一条命令的返回值,如果返回值错误,就会中断执行。
例如我们手动构建一个错误(用rm删除一个不存在的文件):
1 2 3 has_error: rm zzz.txt echo 'OK'
会发生:
1 2 3 4 ylin@Ylin:~/Program/test$ make has_error rm zzz.txt rm: cannot remove 'zzz.txt': No such file or directory make: *** [Makefile:25: has_error] Error 1
但是有时候我们希望忽略错误,我们可以在特定的指令前面加上-用来忽略错误的命令:
1 2 3 ignore_error: -rm zzz.txt echo 'ok'
输出结果如下:
1 2 3 4 5 6 ylin@Ylin:~/Program/test$ make ignore_error rm zzz.txt rm: cannot remove 'zzz.txt': No such file or directory make: [Makefile:29: ignore_error] Error 1 (ignored) echo 'ok' ok
对于执行可能出错,但是不影响逻辑的命令,可以使用-忽略
编译C程序
现在我们尝试一下编译一个简单的C语言程序,其依赖文件如下:
1 2 3 main.c + hello.h -> main.o hello.c -> hello.o hello.o + main.o -> a.out
文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include "hello.h" #include <stdio.h> int main () { printf ("starting main\n" ); hello(); printf ("ending main\n" ); return 0 ; } int hello () ;#include <stdio.h> int hello () { printf ("Hello, World!\n" ); return 0 ; }
我们可以编写以下规则,用于自动构建可执行程序:
1 2 3 4 5 6 7 8 a.out: main.o hello.o cc -o a.out main.o hello.o main.o: main.c hello.h cc -c main.c hello.o: hello.c hello.h cc -c hello.c clean: rm -f a.out main.o hello.o
同时也可以通过make clean来删除中间文件:
1 2 3 4 5 6 ylin@Ylin:~/Program/test$ ls a.out hello.c hello.h hello.o main.c main.o Makefile ylin@Ylin:~/Program/test$ make clean rm -f a.out main.o hello.o ylin@Ylin:~/Program/test$ ls hello.c hello.h main.c Makefile
我们可以看到完整的命令流程如下:
1 2 3 4 5 ylin@Ylin:~/Program/test$ make clean && make rm -f a.out main.o hello.occ -c main.c cc -c hello.c cc -o a.out main.o hello.o
隐式规则
为了编译这个项目,我们一共编写了三条规则,现在我们尝试删除两个.o文件的规则,然后再编译试试:
1 2 3 4 5 a.out: main.o hello.o cc -o a.out main.o hello.o clean: rm -f a.out main.o hello.o
然后我们执行make,输出如下:
1 2 3 4 ylin@Ylin:~/Program/test$ make cc -c -o main.o main.c cc -c -o hello.o hello.c cc -o a.out main.o hello.o
然后可以发现
我们并没有制定相关的规则,可是程序还是正常的进行了编译,这是make中的隐式规则,因为make本来就是为了编译C程序设计的,所以为了避免重复的编译.o文件,在一开始没有找到对应的规则时,会自动的调用隐式规则。对于C C++ ASM ...等程序,都有内置的隐式规则,这里不展开叙述。
使用变量
我们在编译时难免会遇到许多重复的文件名,为了方便使用,我们引入变量用来解决重复的问题。我们以上一节的Makefile为例:
1 2 3 4 5 a.out: main.o hello.o cc -o a.out main.o hello.o clean: rm -f a.out main.o hello.o
我们可以定义一个变量来替换它:
1 2 3 4 5 6 7 TARGET = a.out $(TARGET) : main.o hello.o cc -o $(TARGET) main.o hello.o clean: rm -f $(TARGET) main.o hello.o
对于变量定义,我们使用变量名 = 值,变量名通常使用大写。在引用变量时通常使用$(变量名)
当然,对于我们的依赖文件列表,也可以使用变量进行替换:
1 2 3 4 5 6 7 8 TARGET := a.out OBJS := main.o hello.o $(TARGET) : $(OBJS) cc -o $(TARGET) $(OBJS) clean: rm -f $(TARGET) $(OBJS)
但是对于依赖文件很多的情况下,我们可能需要一个自动化的方式,来将我们的源文件批量编译成目标文件。我们注意到每个.o文件都是由对应的.c文件编译产生的,我们可以让make先获取.c文件列表再替换生成得到.o文件列表:
1 2 3 4 5 6 7 8 9 10 TARGET := a.out OBJS := $(patsubst %.c,%.o,$(wildcard *.c) ) $(TARGET) : $(OBJS) cc -o $(TARGET) $(OBJS) clean: rm -f $(TARGET) $(OBJS)
内置变量
为了方便我们使用,make也内置了很多的内置变量,例如我们可以用$(CC)替换命令cc:
1 2 $(TARGET) : $(OBJS) $(CC) -o $(TARGET) $(OBJS)
这样方便我们在交叉编译时,指定编译器。诸如此类的内置变量还有很多,遇到了再学吧。
自动变量
在makefile中,经常会看到$@、$<之类的变量,这种变量称为自动变量,它们在一个规则中自动指向某个值。例如$@标识目标文件,$^表示所以依赖文件,所以我们也可以这样写:
1 2 3 4 5 a.out: hello.o main.o @echo '$$@ = $@ ' @echo '$$< = $< ' @echo '$$^ = $^ ' cc -o $@ $^
模式规则
我们前面提到隐式转换可以在必要时自动创建.o文件,但实际上隐式规则的命令是固定的:
1 $(CC) $(CFLAGS) -c -o $@ $<
我们只能修改编译器变量和编译选项变量,但没办法运行多条命令。
此时我们就要引入自定义模式规则,它使用make的匹配模式规则,如果匹配上了,就自动创建一条模式规则,我们可以把我们的makefile写成以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 TARGET := a.out OBJS := $(patsubst %.c,%.o,$(wildcard *.c) ) $(TARGET) : $(OBJS) $(CC) -o $(TARGET) $(OBJS) %.o: %.c @echo "Compiling $< to $@ " $(CC) -c $< -o $@ clean: rm -f $(TARGET) $(OBJS)
当程序执行a.out: hello.o main.o时,发现没有hello.o,于是查找以hello.o为目标文件的规则,结果匹配到模式规则*.o : *.c,于是模式规则会动态的创建以下规则:
1 2 3 hello.o: hello.c @echo "Compiling $< to $@ " $(CC) -c $< -o $@
我们可以尝试执行一下make:
1 2 3 4 5 6 ylin@Ylin:~/Program/test$ make Compiling hello.c to hello.o cc -c hello.c -o hello.o Compiling main.c to main.o cc -c main.c -o main.o cc -o a.out hello.o main.o
这样他可以比隐式的规则更灵活。但是我们现在也遇到一个问题,就是当我们修改hello.h头文件时,不会触发main.c重新编译的问题,我们之后在解决。
自动完成依赖
我们刚刚提到,在我们可以解决自动把.c编译成.o文件,但是难以解决.c文件对.h文件的依赖规则。因为要想知道.h的依赖关系,我们需要分析文件内容才能做到,并没有一个简单的文件名映射规则。
好在我们可以使用gcc提供的-MM参数,自动分析我们所需要的.h文件参数:
1 2 ylin@Ylin:~/Program/test$ cc -MM main.c main.o: main.c hello.h
因此,我们可以利用这个功能,为每个.c文件都生成一个依赖项,并把它保存到.d(中间文件中),再使用include将其导入makefile中,这样我们就精准的实现了.c文件的依赖分析。
我们可以更新我们的makefile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 TARGET := a.out SRCS := $(wildcard *.c) OBJS := $(SRCS:.c=.o) DEPS := $(SRCS:.c=.d) $(TARGET) : $(OBJS) $(CC) -o $@ $^ %.d: %.c rm -f $@ ; \ $(CC) -MM $< >$@ .tmp; \ sed 's,\($* \)\.o[ :]*,\1.o $@ : ,g' < $@ .tmp > $@ ; \ rm -f $@ .tmp %.o: %.c $(CC) -c $< -o $@ clean: rm -f $(TARGET) $(OBJS) $(DEPS) include $(DEPS)
当我们运行时,通过include会引入.d文件,但是一开始.d文件并不存在,这时会通过模式规则匹配到%.d: %.c。这里用了一个复杂的sed将.d文件创建了出来。我们运行make结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 ylin@Ylin:~/Program/test$ make rm -f main.d; \cc -MM main.c >main.d.tmp; \ sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.tmp > main.d; \ rm -f main.d.tmprm -f hello.d; \cc -MM hello.c >hello.d.tmp; \ sed 's,\(hello\)\.o[ :]*,\1.o hello.d : ,g' < hello.d.tmp > hello.d; \ rm -f hello.d.tmpcc -c hello.c -o hello.o cc -c main.c -o main.o cc -o a.out hello.o main.o
我们可以看到.d文件中类似:
1 main.o main.d : main.c hello.h
现在,我们文件的依赖就加入了头文件,当我们对头文件进行修改时,就会触发重新编译
1 2 3 4 5 6 7 ylin@Ylin:~/Program/test$ make rm -f main.d; \cc -MM main.c >main.d.tmp; \ sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.tmp > main.d; \ rm -f main.d.tmpcc -c main.c -o main.o cc -o a.out hello.o main.o
完善Makefile
我们现在对项目目录进行整理,我们将源码放入src目录,将编译生成的文件放入build目录:
1 2 3 4 5 6 7 8 ylin@Ylin:~/Program/test$ tree . . ├── build ├── Makefile └── src ├── hello.c ├── hello.h └── main.c
现在我们可以使用我们上述的操作,更新我们的makefile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 SRC_DIR := ./src BUILD_DIR := ./build TARGET = $(BUILD_DIR) /a.out CC = gcc CFLAGS = -Wall SRCS = $(wildcard $(SRC_DIR) /*.c) OBJS = $(patsubst $(SRC_DIR) /%.c,$(BUILD_DIR) /%.o,$(SRCS) ) DEPS = $(patsubst $(SRC_DIR) /%.c,$(BUILD_DIR) /%.d,$(SRCS) ) all: $(TARGET) $(TARGET) : $(OBJS) $(CC) -o $@ $^ $(BUILD_DIR) /%.d: $(SRC_DIR) /%.c @mkdir -p $(BUILD_DIR) rm -f $@ ; \ $(CC) -MM $< >$@ .tmp; \ sed 's,\($* \)\.o[ :]*,$(BUILD_DIR)\1 .o $@ : ,g' < $@ .tmp > $@ ; \ rm -f $@ .tmp $(BUILD_DIR) /%.o: $(SRC_DIR) /%.c @mkdir -p $(BUILD_DIR) $(CC) $(CFLAGS) -c -o $@ $< clean: @echo "Cleaning up..." rm -rf $(BUILD_DIR) include $(DEPS)
我们通过设置目录路径变量实现了对一个简单项目的自动化编译。至此我们的对makefile的基本使用就了解了
对于makefile,它还有很多的复杂的用法,但是之后我会更好的利用它去做更多的项目。