makefile强大之处是目标可以自动生成,这样面对大型项目时可以通过模式规则(pattern)来制定一类文件的通用规则。
假如现在有一个项目有10个c语言文件,我们可以在bash命令行中先生成10个.c文件,其中test10.c存储main函数:
touch test{1..10}.c
cat >test10.c << EOF
#include <stdio.h>int main(){printf("makefile test\n");return 0;
}
EOF
接下来我们可以这样编写makefile
TARGET := main
CC := gccSRC := $(wildcard *.c)
OBJ := $(patsubst %.c,%.o,${SRC}).PHONY: all
all: ${TARGET}${TARGET}: ${OBJ}${CC} $^ -o $@%.o:%.c${CC} -c $< -o $@
第 1-2 行:定义最终生成的可执行文件名为 main,并指定使用 gcc 编译器。
第 4-5 行:使用 wildcard 函数获取当前目录下所有 .c 文件(以空格分隔),
再通过 patsubst 函数将这些文件名中的 .c 替换为 .o。注意,这里仅是字符串替换,并不会实际生成文件。
此处使用立即赋值(:=)避免递归展开,虽然递归展开更为灵活,但立即赋值在此场景下更为合适。
首先看 all 目标:该目标被 .PHONY 修饰,表示它是一个伪目标,即不生成实际文件。all 目标依赖 ${TARGET}(即 main),
但自身不执行任何命令。它常用于提供一个标准编译入口,正如许多 GitHub 项目使用 make all 进行编译一样——通过伪目标来确认编译是否完成,
若未完成则根据规则继续编译。
接下来是 ${TARGET} 目标(即 main),它依赖 ${OBJ}(即 test1.o test2.o ... test10.o)。但当前目录中并不存在这些 .o 文件,
因此需要查看最后一个目标。最后一个目标是模式规则:它告诉 Makefile,每个 .o 文件都依赖于同名的 .c 文件。
这里的 % 是通配符,表示去掉后缀后的部分(例如 test1.o 中的 % 对应 test1)。因此,%.o: %.c 在此等价于:
test1.o:test1.c${CC} -c $< -o $@test2.o:test2.c${CC} -c $< -o $@test3.o:test3.c${CC} -c $< -o $@test4.o:test4.c${CC} -c $< -o $@test5.o:test5.c${CC} -c $< -o $@test6.o:test6.c${CC} -c $< -o $@test7.o:test7.c${CC} -c $< -o $@test8.o:test8.c${CC} -c $< -o $@test9.o:test9.c${CC} -c $< -o $@test10.o:test10.c${CC} -c $< -o $@
使用这种通配规则可以节省大量代码,也有很好的拓展性。值得一提的是这种通配规则也会在make中生成如上述具体的目标,
这是因为在makefile的语法中可以出现很多个目标名相同的目标,这些目标最多只有一个目标有具体的制作规则,
其他目标仅用于声明依赖关系,makefile会把所有相同名称的目标的依赖文件整合,最后执行制作规则。
例如:
main: test1.c
main: test2.c
main: test3.cgcc $^ -o $@
和
main: test1.c test2.c test3.cgcc $^ -o $@
是一样的。这样做也是有原因的有时一个文件的依赖文件可能不是那么容易确定需要makefile生成时会有用。
接下来你就可以用make all指令来编译这些文件了,但是你会发现一个问题所有生成的.o文件和.c文件在一个目录下面
非常的凌乱。在正常的工程中一般把生成的文件放到build目录下面,接下来我们就写一个更好的makefile
TARGET = main
CC = gccBUILD_DIR = buildSRC = $(wildcard *.c)
OBJ = $(patsubst %.c,${BUILD_DIR}/%.o,${SRC}).PHONY: all
all: ${TARGET}${BUILD_DIR}:mkdir ${BUILD_DIR}${TARGET}: ${OBJ}${CC} $^ -o $@${BUILD_DIR}/%.o:%.c | ${BUILD_DIR}${CC} -c $< -o $@
在这里我们增加了一个BUILD_DIR变量保存生成的目录,并且把OBJ变量修改了一下,将%.c替换为${BUILD_DIR}/%.o
这样接下来生成的时候就会把.o文件生成到build目录下面,在最后又修改了一下通配规则,为什么不能用%.o:%.c,
而是改成了${BUILD_DIR}/%.o%.c | ${BUILD_DIR}?因为如果写成%.o:%.c,这里的%就有了${BUILD_DIR}/也就是
build/,导致%.c也会有build/。例如build/test1.o的%是build/test1导致%.c就是build/test1.c,
但是.c文件并没有在build目录下面。而后面写的| ${BUILD_DIR}表示目标需要${BUILD_DIR},但是这个文件不直接参与
编译,也就是不会出现在$^变量上,其中|用来分割哪些文件不直接参与编译。
不过到这里还没有满足最基础要求的makefile,我们还差一个clean目标,用来清理生成的编译文件,方便重新编译。
TARGET = main
CC = gccBUILD_DIR = buildSRC = $(wildcard *.c)
OBJ = $(patsubst %.c,${BUILD_DIR}/%.o,${SRC}).PHONY: all
all: ${TARGET}${BUILD_DIR}:mkdir ${BUILD_DIR}${TARGET}: ${OBJ}${CC} $^ -o $@${BUILD_DIR}/%.o:%.c | ${BUILD_DIR}${CC} -c $< -o $@.PHONY: clean
clean:rm -rf ${BUILD_DIR} ${TARGET}
聪明的你也一定想到了,非常简单。clean目标是一个伪目标不会真正生成文件,执行clean目标删掉所有的.o和可执行文件就行了。
虽然最基础的makefile需要all和clean,如果需要安装到系统还需要install伪目标。不过还有更多的伪目标,比如你想用gdb去debug
你也可以写一个debug目标(但是别忘了给编译条件加上-g选项):
TARGET = main
CC = gccBUILD_DIR = buildSRC = $(wildcard *.c)
OBJ = $(patsubst %.c,${BUILD_DIR}/%.o,${SRC}).PHONY: all
all: ${TARGET}${BUILD_DIR}:mkdir ${BUILD_DIR}${TARGET}: ${OBJ}${CC} $^ -o $@${BUILD_DIR}/%.o:%.c | ${BUILD_DIR}${CC} -c -g $< -o $@clean:rm -rf ${BUILD_DIR} ${TARGET}debug:gdb ${TARGET} -ex "b main" -ex "r"
这样你就可以执行make debug使用gdb调试了,在这里我使用了gdb的-ex选项这样可以在启动的时候执行一句话
在这里执行的是b main给main函数打断点,并且"r"命令执行程序。
如果你有幸做的是嵌入式开发,那你也可以写一个烧录目标(这里使用stm32f103c8t6和stlink烧录协议为例子)
flash:openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program ${TARGET}.hex" -c "reset" -c "exit"
需要注意的是,嵌入式开发通常需要交叉编译工具链,相关技巧我们后续再讨论。
总之,Makefile 不仅是简单的编译脚本,更是开发过程中的得力助手。只要你有需求,就可以定制专属的伪目标来提升效率。