Makefile 用法
基本用法
直接在命令行执行 make
命令即可。
make
命令会在当前目录下查找 makefile
文件,找到后就去解析该文件中的第一个目标。最简单的 makefile
内容如下:
# 要生成 main.exe 这个目标文件: 需要依赖 main.c 这个文件
main.exe: main.c
# 用于生成 main.exe 的命令
gcc main.c -o main.exe
make
命令找到 makefile
之后,会解析其中的第一个目标 main.exe
,发现该目标需要 main.c
文件,就在当前目录中查找 main.c
文件,如果找到,就执行后面的 gcc main.c -o main.exe
来生成 main.exe
,如果找不到,就报错。
我们也可以在 makefile
中指定 main.c
文件的生成方式,比如:
# 要生成 main.exe 这个目标文件: 需要依赖 main.c 这个文件
main.exe: main.c
# 用于生成 main.exe 的命令
gcc main.c -o main.exe
# 要生成 main.c 这个文件: 不需要依赖任何其它文件
main.c:
# 用于生成 main.c 的命令
echo "int main() { return 0; }" > main.c
这样,当 make
程序找不到 main.c
文件时,就会在 makefile
中找 main.c
目标,找到后,就通过该目标生成 main.c
文件,然后再继续编译 main.exe
文件。
格式要求
makefile
以制表符(tab)作为命令的识别符号,所以任何命令前面都必须有一个制表符,不能使用空格代替。另外,注释前面不要添加制表符,否则会被当作命令执行。这里的命令就是任何可以在命令行中执行的命令。
执行原则
如果一个目标所对应的文件已经存在,并且该目标的所有依赖项都没有发生变更,则该目标中的命令不会被执行。比如上面的例子,如果 main.exe
已经存在,并且 main.c
没有被修改,则 make
命令将不会做任何操作。
make
通过对比 main.exe
和 main.c
的“最后修改时间”来判断 main.c
是否被修改,如果 main.c
比 main.exe
新,则判断为 main.c
被修改,否则未被修改。
多个目标
一个目标可以依赖多个其它目标,各个依赖之间用空格隔开。
任何一个目标的名称都可以作为参数传递给 make
,让 make
从指定的目标开始解析:
# 依赖多个目标
main.exe: main.c foo.o bar.o
gcc main.c foo.o bar.o -o main.exe
foo.o: foo.c
gcc -c foo.c
bar.o: bar.c
gcc -c bar.c
# 清理临时文件
clean:
# - 表示遇到错误不要停止
# @ 表示不显示所执行的命令(不显示 rm *.o)
-@rm *.o
执行 make clean
可以让 make
从 clean
开始执行,而不是从 main
开始执行。
三个测试文件的内容如下:
// main.c
#include <stdio.h>
void foo();
void bar();
int main() {
printf("hello main\n");
foo();
bar();
return 0;
}
// foo.c
#include <stdio.h>
void foo() {
printf("hello foo\n");
}
// bar.c
#include <stdio.h>
void bar() {
printf("hello bar\n");
}
使用变量
# 定义变量
CC = gcc
main.exe: main.c foo.o bar.o
$(CC) main.c foo.o bar.o -o main.exe
foo.o: foo.c
$(CC) -c foo.c
bar.o: bar.c
$(CC) -c bar.c
clean:
rm *.o
指定输出目录
下面的脚本将临时文件输出到 output 目录中统一存放:
main.exe: main.c output/foo.o output/bar.o
gcc main.c output/foo.o output/bar.o -o main.exe
# 依赖 output 目录
output/foo.o: output foo.c
gcc -c foo.c -o output/foo.o
# 依赖 output 目录
output/bar.o: output bar.c
gcc -c bar.c -o output/bar.o
# 创建 output 目录
output:
-@mkdir output
clean:
-@rm output/*.o
注意“目标名称”中也需要指定路径,如果写成下面这样,虽然也能使用,但每次都会重新编译 foo.o
和 bar.o
这两个文件:
main.exe: main.c foo.o bar.o
gcc main.c output/foo.o output/bar.o -o main.exe
# 依赖 output 目录
foo.o: output foo.c
gcc -c foo.c -o output/foo.o
# 依赖 output 目录
bar.o: output bar.c
gcc -c bar.c -o output/bar.o
# 创建 output 目录
output:
-@mkdir output
clean:
-@rm output/*.o
因为 make
命令无法在当前目录中找到 foo.o
和 bar.o
这两个文件,所以每次都会执行 foo.o
和 bar.o
这两个目标中的命令。
执行子目录中的 makefile
修改 makefile
,内容如下:
# 依赖 foo bar 两个目标,注意 output 要在 foo bar 的后面
main.exe: main.c foo bar output
gcc main.c output/foo.o output/bar.o -o main.exe
# 创建输出目录
output:
-@mkdir output
# 将 foo bar 两个目标合在一起写,因为它们的命令相同
# 依赖一个永远不存在的文件 null,相当于让本目标每次都执行
foo bar: null
# 这里的 $@ 就是目标名称 foo bar,相当于 make -C foo bar
# 依次解析 for bar 中的 makefile 文件
-@make -C $@
# 一个永远不存在的文件
null:
将前面的 foo.c
和 bar.c
分别放入 foo
和 bar
两个子目录中,然后在两个子目录中各创建一个 makefile
来编译各自的文件,内容如下:
# foo/makefile 的内容
OUTPUT = ../output
# 生成 ../output/foo.o: 依赖 ../output 和 foo.c
$(OUTPUT)/foo.o: $(OUTPUT) foo.c
gcc -c foo.c -o $(OUTPUT)/foo.o
# bar/makefile 的内容
OUTPUT = ../output
# 生成 ../output/bar.o: 依赖 ../output 和 bar.c
$(OUTPUT)/bar.o: $(OUTPUT) bar.c
gcc -c bar.c -o $(OUTPUT)/bar.o
在这个例子中,foo
、bar
两个目录的“修改时间”比最后生成的 main.exe
要旧,按道理说,再次执行 make
时,main.exe
不会被重新编译,所以也不会再执行 foo bar:
中的命令了,但事实上,每次执行 make
时都会执行 foo bar:
中的命令,但 main.exe
却不会被重新编译,个人理解原因如下:
main.exe:
依赖foo
、bar
,所以要去检查foo bar:
是否需要更新。- 检查发现
foo bar:
所依赖的文件null
不存在,所以导致null
需要被更新。 null
更新后,还要将foo
、bar
与null
做新旧比较,看foo
、bar
是否需要更新。结果发现null
还是不存在,就默认foo
、bar
比null
旧,需要更新。- 所以就执行
-@make -C $@
去更新foo bar:
,导致foo
、bar
两个目录中的makefile
被解析,由于foo.c
和bar.c
没有被更改,所以此次解析没有改变任何内容。解析完毕后foo bar:
就算更新完了。 - 然后还需要将
main.exe
和foo
、bar
做新旧比较,而foo
、bar
的时间戳没有发生变化,所以main.exe
依然比foo
、bar
新,结果导致main.exe
不会被更新。
# 2 -- foo bar 没有发生变化,所以 main.exe 不需要更新
main.exe: main.c foo bar output
gcc main.c output/foo.o output/bar.o -o main.exe
output:
-@mkdir output
# 1 -- null 不存在,所以 foo bar 需要更新,更新后 foo bar 也没变化
foo bar: null
-@make -C $@
null:
需要注意的是,在 Windows
中,修改了 foo.c
或 bar.c
的内容后,foo
和 bar
两个目录的时间戳并不会发生变化,只有当 foo.c
或 bar.c
被重新编译到 output
目录中时,output
的时间戳才会发生变化,而 foo
和 bar
的时间戳依然没有变化,所以在上面的 makefile
中,需要将 main.exe:
的依赖项 output
放到 foo
和 bar
之后,以保证 foo
和 bar
中的内容被编译后,能及时在 output
中被检测到。
向子目录传递变量
OUTDIR = output
# 导出 OUTDIR,让子目录中的 makefile 也可以使用该变量
export OUTDIR
main.exe: main.c $(OUTDIR) foo bar
gcc main.c $(OUTDIR)/foo.o $(OUTDIR)/bar.o -o main.exe
$(OUTDIR):
-@mkdir $(OUTDIR)
foo bar: null
-@make -C $@
null:
# foo/makefile 的内容
OUTPUT = ../$(OUTDIR)
# 生成 $(OUTPUT)/foo.o: 依赖 $(OUTPUT) 和 foo.c
$(OUTPUT)/foo.o: $(OUTPUT) foo.c
gcc -c foo.c -o $(OUTPUT)/foo.o
# bar/makefile 的内容
OUTPUT = ../$(OUTDIR)
# 生成 $(OUTPUT)/bar.o: 依赖 $(OUTPUT) 和 bar.c
$(OUTPUT)/bar.o: $(OUTPUT) bar.c
gcc -c bar.c -o $(OUTPUT)/bar.o
其它用法
bin/main.exe: bin foo bar foo bar
# 目标名称
@echo $@
# 目标的目录部分
@echo $(@D)
# 目标的文件名部分
@echo $(@F)
# 所有依赖(保留重复部分)
@echo $+
# 所有依赖(去掉重复部分)
@echo $^
# 第一个依赖
@echo $<
# 所有比目标新的依赖
@echo $?
bin:
foo:
bar:
# 查找当前目录中的所有 *.c 文件,存入变量 FILES 中
FILES = $(wildcard *.c)
# 将 FILES 中的文件后缀 .c 改为 .o 存入变量 FILES1 中
FILES1 = $(patsubst %.c, %.o, $(FILES))
# 去掉 FILES 中的文件后缀 .c 存入变量 FILES2 中
FILES2 = $(patsubst %.c, %, $(FILES))
# 显示变量
echo:
@echo $(FILES)
@echo $(FILES1)
@echo $(FILES2)
# 查找当前目录中的所有 *.c 文件,并将后缀 .c 改为 .o 存入 FILES 中
FILES = $(patsubst %.c, %.o, $(wildcard *.c))
# 相当于 main.o: main.c
$(FILES): %.o : %.c
@echo $@
@echo $+
OUTPUT = output
SUBDIR = foo bar
all:
# 递归执行子目录中的 make
@for i in $(SUBDIR); do make -C $$i; done
clean:
-@rm $(OUTPUT) -R
# 递归执行子目录中的 make clean
@for i in $(SUBDIR); do make clean -C $$i; done