Makefile 学习笔记

发布于:2024-03-11 ⋅ 阅读:(51) ⋅ 点赞:(0)

基础规则

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

自动变量

  1. $< 第一个先决条件名字
  2. $^ 所有先决条件
  3. $@ 目标名字
  4. $? 所有比目标更新的先决条件
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中有以下几种风格赋值变量风格:

  1. 递归展开变量赋值(Recursively Expanded Variable Assignment) :=
  2. 简单展开赋赋值(Simply Expanded Variable Assignment) =
  3. 条件变量赋值变量赋值 (Conditional Variable Assignment) ?=
  4. 立即展开变量赋值(Immediately Expanded Variable Assignment):::=

GUN 变量风格文档

其中第四种立即展开变量赋值使用较少不做讲解,且不是太多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 $@

参考文献

why_makefile
makefiletutorial

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到