基础规则
makefile基于声明式依赖关系如下所示
target:prerequisite prerequisite2
command
target2:target
command2
如果运行target2那么由于声明了依赖关系所以会先运行target
需要注意的是Makefile默认target即是一个目标也是一个本地文件,除非你使用PHONY去声明仅是一个目标。
up to date 机制
up to date(已是最新)是Make的核心机制。
我们看下Make规则命令语法如下
target:prerequisite prerequisite2
command
在操作系统中每个文件都存储了最后修改的时间戳,如果target修改时间大于所有prerequisite 那么本次任务就会跳过。
#Makefile
hell:hell.o
hell.c:
echo "int main(){return 0;}" > hell.c
.
└── Makefile
0 directories, 1 file
我们第一次运行make命令输出
echo "int main(){return 0;}" > hell.c
cc -c -o hell.o hell.c
cc hell.o -o hell
.
├── hell
├── hell.c
├── hell.o
└── Makefile
0 directories, 4 files
第二次运行
make: 'hell' is up to date.
因为目录下target对应的hell文件且修改时间大于先决条件hell.o所以不会执行。
//以时间倒序排序文件
-rwxrwxr-x 1 jack jack 15776 Mar 10 10:44 hell
-rw-rw-r-- 1 jack jack 1224 Mar 10 10:44 hell.o
-rw-rw-r-- 1 jack jack 22 Mar 10 10:44 hell.c
-rw-rw-r-- 1 jack jack 59 Mar 10 10:43 Makefile
假设我们此时用touch hell.c会不会迫使make再次运行?
touch hell.c;make
输出
cc -c -o hell.o hell.c
cc hell.o -o hell
很显然hell.c时间大于hell.o时间导致make感知到需要重新编译hell.o,最后在重新生成可执行文件hell
自动变量
- $< 第一个先决条件名字
- $^ 所有先决条件
- $@ 目标名字
- $? 所有比目标更新的先决条件
Makefile
all: hello1.c hello2.c
@echo '$$< = ' $<
@echo '$$? = ' $?
@echo '$$@ = ' $@
@echo '$$^ = ' $^
touch all
当前目录
.
├── Makefile
├── hello1.c
└── hello2.c
1 directory, 3 files
第一次make输出:
$< = hello1.c
$? = hello1.c hello2.c
$@ = all
$^ = hello1.c hello2.c
touch all
第二次执行执行touch hello1.c;make 输出:
$< = hello1.c
$? = hello1.c
$@ = all
$^ = hello1.c hello2.c
touch all
常用命令
- make -s 静默所有执行命令的打印(不是静默输出,而是静默命令)
one:
echo "hello"
make执行输出
echo "hello"
hello
make -s 输出
hello
- make -i 忽略错误继续运行
tree:one two
echo "hello tree"
one:
#抛出错误 hello one无法打印
false
echo "hello one"
two:
echo "hello two"
make 输出
#抛出错误 hello one无法打印
false
make: *** [one] Error 1
make -i 输出
#抛出错误 hello one无法打印
false
make: [one] Error 1 (ignored)
echo "hello one"
hello one
echo "hello two"
hello two
echo "hello tree"
hello tree
- make -k 忽略先决目标错误继续执行其他先决错误
tree:one two
echo "hello tree"
one:
#抛出错误 hello one无法打印
false
echo "hello one"
two:
echo "hello two"
make输出
#抛出错误 hello one无法打印
false
make: *** [one] Error 1
make -k
#抛出错误 hello one无法打印
false
make: *** [one] Error 1
echo "hello two"
hello two
make: Target `tree' not remade because of errors.
默认隐式规则
因为Makefile设计的目的是用于管理C工程的依赖,所以未来简化Makefile的编写定义一堆魔化规则。当然这些规则会使人带来很多困惑。
# Makefile
all:
执行:make
(1) all默认会默认prerequisite为all.o. 并执行对应的编译命令$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@
(2) all.o的prerequisite为all.c all.cc all.cpp(优先级排序,如果有c后缀就不会选cc。有cc就不会选中cpp) 并执行对应的编译命令$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
(3) 如果当前目录不存在all.c all.cc all.cpp那么会选择Makefile中是否存在all.o或者all.c all.cc all.cpp目标规则
案例1
证明 all目标隐式依赖all.o和all.o隐式依赖all.c all.cc all.cpp
目录如下:
.
├── Makefile
└── all.c
# Makefile
all:
执行命令后输出:
learnMake make
c++ all.cpp -o all
其实上面的MakeFile等价如下
# Makefile
all: all.o
$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@
# 通用规则,用于从.c、.cc或.cpp文件编译.o文件
all.o: all.c
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
all.o: all.cc
$(CXX) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
all.o: all.cpp
$(CXX) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
案例2
证明命令是可选的 %.c到%.o ,以及%.c到target命令都是可选,MakeFile会隐式执行
目录如下:
.
├── Makefile
└── all.c
# Makefile
all: all.o
# 通用规则,用于从.c、.cc或.cpp文件编译.o文件
all.o: all.c
案例3
如果目录下存在target.c,target.o文件或MakeFile存在对应的target那么默认会选上
.
├── Makefile
└── all.c
#默认会选中all.o 虽然你没有写。
#然后all.o先决条件为all.c由于目录下存在all.c所以会自动触发
all: hell.o
hell.c:
echo ""> hell.c
clean:
-rm all hell.c hell.o
输出:
make
echo ""> hell.c
cc -c -o hell.o hell.c
cc all.c hell.o -o all
这一情况对于target为*.o同样使用
#由于规则或者目录下存在hell.c那么默认会带上hell.c all.c一起编译为hell.o
hell.o:all.c
# 生成一个默认的hell.c
hell.c:
touch "hell.c"
输出
make hell.o
touch "hell.c"
cc -c -o hell.o hell.c
PHONY用法
如果你想屏蔽这一个规则可以给target声明为.PHONY
all:
.PHONY:all
声明后all不会被视为一个文件target(即使本地有all文件也不会判断all的新旧)而是一个纯粹的意图target,并且也不会关联默认C编译的隐含规则。
.
├── Makefile
└── all.c
1 directory, 2 files
all:
.PHONY:all
make输出
make: Nothing to be done for `all'.
如果我们去掉PHONY后
all:
make输出
cc all.c -o all
环境变量
在运行make我们环境变量都会转化为Makefile内嵌变量。
为了增加说教性这里添加一些前置知识。
#Makefile
all:
@#这里输出的是shell中的环境变量
@echo $$testMyVar
@#这里输出shell环境变量转化为的Makefile变量
@echo $(testMyVar)
export testMyVar=123;make
输出
123
123
这里解释一下为什么shell环境变量用$$
输出而Makefile
变量用$()
输出。
在Makefile如果你想转义$
进行输出需要在前面在拼接一个$
即$$
。
echo
是一个shell命令如果你想传给shell一个原始的$变量名
那么就需要先转义前面的$
.
为了让大家更加理解这个知识点,我举例了一个更为复杂的案例:
#仅设置Makefile环境变量
myvar1 = hello myvar1
#设置环境变量和Makefile变量
export myvar2 = hello myvar2
all:
@#由于打印Makefile变量所有有输出 hello myvar1
@echo "\$$(myvar1)" = $(myvar1)
@#由于打印环境变量,但是myvar1是设置Makefile变量所以无输出
@echo "\$$\$$(myvar1)" = $$myvar1
@#myvar2变量为环境变量Makefile变量有输出hello myvar2
@echo "\$$(myvar2)" = $(myvar2)
@#myvar2变量为环境变量Makefile变量有输出hello myvar2
@echo "\$$\$$(myvar2)" = $$myvar2
.PHONY:all
输出:
$(myvar1) = hello myvar1
$$(myvar1) =
$(myvar2) = hello myvar2
$$(myvar2) = hello myvar2
环境变量的特性在对于子Makefile
任务的时候十分重要。环境变量才可以透传到子make任务
#Makefile
# 这个变量会在子make可以感知并输出
export myExportvar = hello myExportvar
# 这个变量在子make无法感知
mynonExportvar = hello mynonExportvar
new_contents = "hello:\n\t@echo myExportvar=\$$(myExportvar)\n\t@echo mynonExportvar=\$$(mynonExportvar)"
all:
@mkdir -p subdir
@printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
@cd subdir && $(MAKE)
clean:
rm -rf subdir
上面的make运行会在subdir创建一个makefile文件并允许
.
├── Makefile
└── subdir
└── makefile
#subdir/makefile
hello:
@echo myExportvar=$(myExportvar)
@echo mynonExportvar=$(mynonExportvar)
对应的输出:
myExportvar=hello myExportvar
mynonExportvar=
单双引号
- 在Makefile单双引号在定义变量是没有任何作用的。
myvar = hello world
myvar2 = "hello world2"
myvar3 = 'hello world3'
all:
echo $(myvar)
echo $(myvar2)
echo $(myvar3)
输出:
echo hello world
hello world
echo "hello world2"
hello world2
echo 'hello world3'
hello world3
- 但是调用函数的时候有一定区别
#Makefile
myvar = hello world
all:
@#单引号除了$$不会转义其他特殊字符都会帮我们转义。这里传给echo命令收到的字符串为\$(myvar)
echo '$$(myvar)'=$(myvar)
@#双引号不会帮我们转义需要我们自行转义,这里传给echo命令收到的字符串为 \$(myvar)。注意我们手动在$多加了一个\转义符
echo "\$$(myvar)"=$(myvar)
echo '$(myvar)'=hello world
$(myvar)=hello world
echo "\$(myvar)"=hello world
$(myvar)=hello world
变量
在Makefile中有以下几种风格赋值变量风格:
- 递归展开变量赋值(Recursively Expanded Variable Assignment)
:=
- 简单展开赋赋值(Simply Expanded Variable Assignment)
=
- 条件变量赋值变量赋值 (Conditional Variable Assignment)
?=
- 立即展开变量赋值(Immediately Expanded Variable Assignment)
:::=
其中第四种立即展开变量赋值
使用较少不做讲解,且不是太多make版本都支持这个语法。
- 递归展开变量赋值
递归展开变量赋值 是指在使用这个变量的时候对后面引用的变量进行展开(仅第一次的时候)
ugh = Huh2?
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:;echo $(foo)
make 输出
echo Huh?
Huh?
在执行all的时候ugh为Huh?所以此时在展开foo变量所用引用的数值就为Huh?这种风格和我们日常编程思维有所违背,且有死递归问题(自身引用自身)
ugh = Huh2?
foo = $(bar) $(foo)
bar = $(ugh)
ugh = Huh?
all:;echo $(foo)
make输出:
Makefile:2: *** Recursive variable `foo' references itself (eventually). Stop.
- 简单展开赋赋值
这个赋值和我们日常编程极为接近,在声明这个变量的时候就展开了引用变量
bar := hello
#:=会立即展开变量进行求值。此时由于bar定义为hello所以结果hello world
foo := $(bar) world
bar := world
all:;echo $(foo)
make 输出:
echo hello world
hello world
这个赋值允许自身引用而不会发生死递归问题
bar := hello
#:=会立即展开变量进行求值。此时由于bar定义为hello所以结果hello world
foo := $(bar) world
bar := world
#拼接自身的字符串
foo :=$(foo) $(foo)
all:;echo $(foo)
输出
echo hello world hello world
hello world hello world
- 条件变量赋值变量赋值
这个比较简单,仅当变量未被定义才会对变量赋值(注意是定义而非参考是否有值)
bar := hello
#由于bar被赋值了hello 所以不允许再次赋值
bar ?= world
#由于变量未被定义过所以此处可以成功赋值 假设前面有一个foo = 也不会赋值,那么foo未被赋值
foo ?= world
all:;echo $(bar) $(foo)
复写命令行变量数值
在默认情况下make命令传入的变量数值是不允许改变的,只有当你加入overide才允许复写
#强行复写
override bar := overrideBar
#未加关键字不允许复写
foo := overrideBar foo
all:;echo $(bar) $(foo)
make foo=123 bar=456 输出
echo overrideBar 123
overrideBar 123
%通配符的使用
用于编写泛化能力举例如下:
hell:a.o b.o c.o d.o
a.c:
echo "int main(){return 0;}" > $@
#利用%通配符实现泛化能力.
%.o:%.c
$(CC) -c $^ -o $@
#利用%通配符实现泛化能力,快速创建所需的C文件
%.c:
touch $@