Makefile 用法

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.exemain.c 的“最后修改时间”来判断 main.c 是否被修改,如果 main.cmain.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 可以让 makeclean 开始执行,而不是从 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.obar.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.obar.o 这两个文件,所以每次都会执行 foo.obar.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.cbar.c 分别放入 foobar 两个子目录中,然后在两个子目录中各创建一个 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

在这个例子中,foobar 两个目录的“修改时间”比最后生成的 main.exe 要旧,按道理说,再次执行 make 时,main.exe 不会被重新编译,所以也不会再执行 foo bar: 中的命令了,但事实上,每次执行 make 时都会执行 foo bar: 中的命令,但 main.exe 却不会被重新编译,个人理解原因如下:

  1. main.exe: 依赖 foobar,所以要去检查 foo bar: 是否需要更新。
  2. 检查发现 foo bar: 所依赖的文件 null 不存在,所以导致 null 需要被更新。
  3. null 更新后,还要将 foobarnull 做新旧比较,看 foobar 是否需要更新。结果发现 null 还是不存在,就默认 foobarnull 旧,需要更新。
  4. 所以就执行 -@make -C $@ 去更新 foo bar:,导致 foobar 两个目录中的 makefile 被解析,由于 foo.cbar.c 没有被更改,所以此次解析没有改变任何内容。解析完毕后 foo bar: 就算更新完了。
  5. 然后还需要将 main.exefoobar 做新旧比较,而 foobar 的时间戳没有发生变化,所以 main.exe 依然比 foobar 新,结果导致 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.cbar.c 的内容后,foobar 两个目录的时间戳并不会发生变化,只有当 foo.cbar.c 被重新编译到 output 目录中时,output 的时间戳才会发生变化,而 foobar 的时间戳依然没有变化,所以在上面的 makefile 中,需要将 main.exe: 的依赖项 output 放到 foobar 之后,以保证 foobar 中的内容被编译后,能及时在 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章