最近有同事写了个 shell 脚本,可以通过比较源文件和目标文件的时间戳,实现部分编译的功能。我觉得这个是 make 的本职工作,于是花了 “ 亿点点 “ 时间研究了 Makefile 怎么写,简单记录点笔记

make 主要用来做自动化构建工作,只要我们在项目文件夹内定义 makefile(或者 Makefile)文件,make 就会按照规则来做构建工作。makefile 的一个简单示例:

main: main.c
    gcc main.c -o main

这个例子里定义了一条规则 main: main.c,规则生成 main 可执行文件,我们称 main 为一个构建目标;该规则依赖 main.c 源文件,如果 main.c 发生了修改,就会自动重新编译 main 文件。gcc main.c -o main 表示一条构建命令,用于构建 main 目标,注意这里需要在行首添加一个 tab,不然没法区分是不是构建命令。我们在目录里执行 make,不需要指定目标,就会自动执行 main 的构建工作,因为 make 会自动执行遇到的第一个构建规则

在前面的 Makefile 基础上,我们再添加一个目标 clean:

main: main.c
    gcc main.c -o main

clean:
    rm main

以后敲 make clean 的时候,就会移除 main 文件了。但是 clean 并不是一个文件,一般对这种目标我们会定义一个伪目标,比如:

main: main.c
    gcc main.c -o main

.PHONY: clean
clean:
    rm main

简单的构建规则这样就够用了。困扰我的是这次需要编译的源文件,是不固定的,后期会新增,所以没法在 Makefile 里清晰指定,需要用到一些隐式规则和变量展开的知识,比如:

BINS := %.bin
OBJS := %.obj
$(BINS): $(OBJS)
    gen $(basename $*)

ALL_BINS := $(patsubst %.obj,%.bin,$(wildcard *.obj))

.PHONY: map
map: $(ALL_BINS)

上面的例子里,定义了一个隐式的规则 $(BINS): $(OBJS),表示 .bin 文件依赖同名的 .obj 文件,比如 base.bin 依赖 base.obj 文件。然后定义了一个伪目标 map,这个目标依赖所有的 bin 文件。问题在于初始化的时候,还没有生成 bin,我怎么要生成哪些 bin 呢?这里就用到了一些文件名处理的工具函数了。$(wildcard *.obj) 表示找出当前目录里所有的 .obj 结尾的文件,patsubst 表示 pattern substitute(模式替换),将 .obj 替换成 .bin,就得到了我们想要构建的所有 .bin 文件的文件名了。最后一个点在于生成工具 gen,只接受不含后缀的文件名,所以需要用 basename 处理一下,比如 base.bin 只会取出 base

最后执行的结果多多少少有点出乎意料,gen 工具本身其实是支持多个输入参数的,但是按这个规则构建时,每次只会获得一个参数进行构建。相当于 make 自动找出需要构建的 .bin 文件,然后每次只构建一个,好处是可以用 make -j 做并行化处理,同时用上所有的核心吧

参考资料: