[toc]

简介

Makefile作为构建项目的利器, 几乎出现在任何项目中. 但是我除了学校里学习嵌入式的时候接触过make外, 之后的工作中都没有关注过它, 工作的重心全部都在堆叠业务代码, 项目搭建时就捉襟见肘了, 这次借着新项目启动, 重温并学习一把Makefile.

参考:

  • https://www.ruanyifeng.com/blog/2015/02/make.html
  • https://seisman.github.io/how-to-write-makefile/overview.html

make是一切的开始

make test
make build
make clean
# ...

当我们在终端运行make命令的时候, 按照Makefile文件内的命令, 一切就按部就班的开始了.

make命令会找到当前目录下文件名为Makefile的文件(建议首字母大写), 找到给定的target, 比如上面的test就是一个target, 其运行的所有内容就称为一个规则rule.

# 一条rule的形式
<target> : <prerequisites>
制表符 <command>
  • taget 目标: 可以理解为定义了一个规则的名称. 通常是文件名, 表示产出一个文件.
  • prerequisites 前置条件: 可选内容, 和command至少有一个即可. 用空格分隔, make时会先运行前置条件再运行自己. 如果前置条件是文件名则会复杂一些, 后面再说.
  • [tab] 制表符: 固定格式, 一定要以制表符tab起首
  • command 命令: 由一行或者多行的shell命令组成. 通常它的结果就是生成目标文件. 每行命令都是在独立shell中执行, 没有继承关系.

除了以上, make还有一些内置变量, 通过修改内置变量, 可以对make做一些配置修改, 比如修改制表符为自定义符号, 又或者声明一个伪目标, 指定命令在同一个shell中执行等 . 参考: https://www.gnu.org/software/make/manual/html_node/Special-Targets.html#Special-Targets

这一节以经典的print例子结束:

# Makefile
call.me:
        echo "hello!"
callback.stranger: call.me
        echo "hi!"
# 输出
➜  make
echo "hello!"
hello!

➜  make call.me
echo "hello!"
hello!

➜  make callback.stranger
echo "hello!"
hello!
echo "hi!"
hi!

其中, 只调用 make时默认运行文件中的第一个target.

进一步理解make

上一节中举例的print demo或者项目中可能见到的make test都不算是最典型的make用法. make命令最朴实的功能其实是构建, 也就是将项目代码一步一步的变成最终可执行文件. 那么其中的规则rule的主要作用主要就是打包和编译.

如果你能够理解程序/算法都由输入, 输出和有限的运算执行步骤组成, 那么make也是如此, 并且它的主要作用就是为了产生文件. 如果将target看做程序的输出, prerequisites看做程序的输入, command看做程序的执行方法. 那恭喜你, 对make的理解又深入了一层.

正是因为如此, make对于文件处理有很多特殊的逻辑. 比如targetprerequisites都会被默认当作文件名, 在当前目录检查, 如果对应的文件存在, 且时间戳符合先后关系, 则make就不会重新执行等, 而command也有相对应的一些列通配符用来获取文件相关的信息等.

# 最终打包成target.txt
target.txt: source.txt
	cp source.txt target.txt
	make clean

# 中间产生的文件
source.txt: source1.txt source2.txt
	# hello comment here, i will be printed too yehoo~
	cat source1.txt >> source.txt
	cat source2.txt >> source.txt

# 初始化, 模拟创建代码源文件
source1.txt source2.txt: 
	echo "this is $@ content" > $@
	
# 清理中间过程文件	
clean: 
	rm -f source.txt source1.txt source2.txt

# 清理打包后的文件
clean-target:
	rm -f target.txt

注意

初始化阶段可以看到出现了,两个target定义在同一个rule下的场景, 在产生中间文件阶段, prerequisite定义了他们两个. 然而, 他们并不是一次直接执行, 而是挨个执的. 等价于分别调用:

➜ make source1.txt
➜ make source2.txt

当然, 也可以仅仅只调用其中一个, 生成一个target:

➜ make source1.txt
echo "this is source1.txt content" > source1.txt

# 是结束更是开始

其实到此, 对make的总结已经基本结束了. make的目标就是构建项目, 过程就是通过rule中定义的输入输出和方法来产生目标项目文件. 现在不论拿到任何Makefile文件都不会觉得无从下手了. 关于shell命令是另外一回事儿, 本文不会介绍, 接下来就开始针对make本身的特性做解释. 当然, make由于和shell有千丝万缕的联系, 一些东西会很相似.

command 命令

每行的命令需要一个tab制表符作为开始, 后面接shell命令, 且每个条命令都会在独立的shell中执行, 没有继承关系, 比如声明变量(还记得.zshrc文件定义的变量吗? 每个新的shell都要定义后才能使用)

改变标记的字符

虽然通常不会这么做, 但是如果要改变制表符作为命令开始, 就需要用到make的内置变量.RECIPEPREFIX了, 比如:

.RECIPEPREFIX = >
say.hi:
> echo Hello, world

这里将制表符改为了>.

多行命令

如果多行命令希望在同一个shell中, 有两种方式:

  1. 需要使用另一个内置变量.ONESHELL
.ONESHELL:
var-kept:
        export foo=bar;
        echo "foo=[$$foo]"
  1. 换行符
var-kept:
        export foo=bar;\
        echo "foo=[$$foo]"

注意

如何执行一些全局有效的变量呢? 比如shell执行export xxx=xxx命令.如果希望它在所有的rules中都生效, 其实只要定义在rule之外就好了:

export AA = 000
export AA = 123
tar:
        echo "nothing here"
        echo $$AA
export AA = 456
➜ make
echo "nothing here"
nothing here
echo $AA
456

此时, 执行makeAA的结果是可以被成功打印出来的. 可以理解为每一个shell都会执行定义在rule之外的命令做为shell的初始化.

另外, 从AA的结果等于456可以看出, 外面的操作是全文件顺序执行一次后再供每一个rule调用的.

回声与消除

通常, make会将要执行的命令和shell输出到命令行的结果统统打印出来, 这种先打印再执行的情况叫回声echoing

rule中的注释也会被回声打印出来. 使用@可以取消回声:

var-kept:
        @export foo=bar;\
        echo "foo=[$$foo]"
# 输出, export这一行不再被打印
➜  make
foo=[bar]

通配符(wildcard)与模式匹配

通配符和bash一致. 支持*, ?和[...], ; 而对targetprerequisite则可以使用%进行模式匹配:

# 通配符demo
clean:
        rm -f *.o

使用模式匹配可以简化命令:

#	echo "there" > b.c
# 模式匹配demo
%.o: %.c
        echo "do out put"
        cat *.c >> $@

# output
➜ make b.o
echo "do out put"
do out put
cat *.c >> b.o
➜ ls
Makefile b.c      b.o
➜ make
make: *** No targets.  Stop.

可以看到, 此时make命令后面的.o文件名可以匹配任何名字, 并匹配prerequisites, 找到同名.c文件并执行命令.

命令做了什么不重要, 只需要注意, 这里会输出target的.o文件.

下次再执行make b.o时, 通配符会发现已经存在而stop.

注意

通配符模式的target是无法直接调用make来执行的, 一定要跟着参数. 否则会有两种情况:

  1. Makefile中有其他可以无参数执行的target时, 顺序执行第一个可以执行的;
  2. 无其他能执行的target时, 会直接报无可执行target信息.

自定义变量, shell变量与内置变量

自定义变量

Makefile中允许自定义变量, 自定义变量的声明和使用方式如下:

var1 = hello world
hello:
	echo $(var1)

可以注意到变量的定义在rule之外, 这是因为如果定义在rule内的command中, 会被当作shell脚本执行, 显然shell并不能解析这句话的含义. 同时, 使用变量的方式是$().

Makefile自定义的变量之间也可以互相赋值, 这里涉及到动态扩展和静态扩展, 涉及到4种不同的赋值运算符, 本文暂略, 详情可以参考:

https://stackoverflow.com/questions/448910/what-is-the-difference-between-the-gnu-makefile-variable-assignments-a

TODO: 自定义变量如何在command中赋值?

shell变量

shell变量获取时, 由于shell命令是写在Makefile中的, 因此需要转义$符, 使用双$来获取变量:

# in terminal shell
echo $var_a

# in Makefile command
	echo $$var_a

内置变量(implicit variables)

Make提供了一些获取系统环境等信息的内置变量, 用来实现跨平台兼容性等. 参考: https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html

# 列举一些
AR
Archive-maintaining program; default ‘ar’.
AS
Program for compiling assembly files; default ‘as’.
CC
Program for compiling C programs; default ‘cc’.
CXX
Program for compiling C++ programs; default ‘g++’.

自动变量

这里算是make命令的另一个难点. 自动变量的值与当前的规则有关. 比如$@指代当前的待构建的目标target等.

$@

表示当前rule中的target目标. 或者换一个思维, make后面跟着什么命令它就代表什么.

make test

此时, $@就代表test , 也就是他的值是test

$<

表示当前rule中的第一个prerequisite:

# echo "a" > sourcea
# echo "b" > sourceb
res: sourcea sourceb
	echo $<

此时, $<的值就是sourcea

$?

表示比target的时间戳更加新一些的prerequisites的列表.

tar: p1 p2
	echo $?

此时, 如果p1和p2的文件时间戳都比tar更早, 则$?就代表p1和p2, 中间用空格分隔.

注意

看到这个规则是不是有一些困惑? 比如如果p1和p2, tar都不是文件, 怎么比较时间戳呢?

tar: p1 p2
        echo $?

p1:
        echo "p1"
p2:
        echo "p2"
# 输出
➜ make
echo "p1"
p1
echo "p2"
p2
echo p1 p2
p1 p2

这里其实是将prerequisite全部打印出来了, 再次说明make是针对文件构建而设计的, 对于这种非文件的场景其实定义的就很宽松了.

$^

表示所有前置条件, 和上面的例子结果类似, 不再赘述.

$*

还记得模式匹配部分吗? 这里表示匹配符%匹配到的部分, 比如:

%.o: 
	echo $*
# 输出
➜ make hello.o
echo hello
hello

这个例子中匹配到的就是"hello"

$(@D)$(@F)

首先, 回顾上面, $@表示target, 而这里的两个自动变量就分别表示$@的目录名和文件名.

tar.txt: a.txt
        echo $(@D)
        echo $(@F)

比如上例中, tar.txt$(@D).$(@F)tar.txt.

相同的还有$(<D)$(<F) , 道理类似, 不再赘述.

判断

make判断是使用的bash语法.

tar:
ifeq (1,1)
        echo "hello1"
else
        echo "hello2"
endif

这里特别说明下, if语句是不需要用tab的, 只有if中包裹的command才需要.

循环

list = 2 4 6 8 10
tar:
        for i in 1 3 5 7 9; do \
                echo $$i; \
        done;\

        for i in $(list); do \
                echo $$i; \
        done\

关于for的使用其实还是需要更多实践. 一些变量赋值等操作并不是很熟悉, 另外for的写法, 每行都有一个换行符, 看起来很难受.

函数

这里是指make的内指函数, 主要有执行shell, 替换通配符, 文本替换, 模式替换, 后缀名替换等, 除了shell命令外全都是处理文件名相关的. 调用形式如:

$(functionName arguments)
# 或者
${functionName arguments}

例子:

out := $(shell echo "hello world") # 执行shell脚本

srcfiles := $(wildcard src/*.txt) # 替换通配符`%`

注意:

运行过程中会出现: make: Nothing to be done for XXX 这时很可能是命令前的制表符tab格式被修改了.