[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对于文件处理有很多特殊的逻辑. 比如target
和prerequisites
都会被默认当作文件名, 在当前目录检查, 如果对应的文件存在, 且时间戳符合先后关系, 则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中, 有两种方式:
- 需要使用另一个内置变量
.ONESHELL
.ONESHELL:
var-kept:
export foo=bar;
echo "foo=[$$foo]"
- 换行符
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
此时, 执行make
时AA
的结果是可以被成功打印出来的. 可以理解为每一个shell都会执行定义在rule之外的命令做为shell的初始化.
另外, 从AA
的结果等于456
可以看出, 外面的操作是全文件顺序执行一次后再供每一个rule调用的.
回声与消除
通常, make会将要执行的命令和shell输出到命令行的结果统统打印出来, 这种先打印再执行的情况叫回声echoing
rule中的注释也会被回声打印出来. 使用@
可以取消回声:
var-kept:
@export foo=bar;\
echo "foo=[$$foo]"
# 输出, export这一行不再被打印
➜ make
foo=[bar]
通配符(wildcard)与模式匹配
通配符和bash一致. 支持*, ?和[...]
, ; 而对target
和prerequisite
则可以使用%
进行模式匹配:
# 通配符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
来执行的, 一定要跟着参数. 否则会有两种情况:
- Makefile中有其他可以无参数执行的
target
时, 顺序执行第一个可以执行的; - 无其他能执行的
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
格式被修改了.