Makefile learn
@think3r
2019-08-13 22:31:16 参考链接:
0x00 基础
- 编译和链接:
- 一般来说,无论是 C 还是 C++,首先要把源文件编译成中间代码文件,在 Windows 下也就是 .obj 文件,UNIX 下是 .o 文件,即 Object File,这个动作叫做 编译(compile)。
- 编译时,编译器需要的是语法的正确,函数与变量的声明的正确。
- 如果函数未被声明,编译器会给出一个警告,但可以生成 Object File。
- 然后再把大量的 Object File 合成执行文件,这个动作叫作 链接(link)。
- 链接时,主要是链接函数和全局变量。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File).
- 而在链接程序时,链接器会在所有的 Object File 中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error)
- 一般来说,无论是 C 还是 C++,首先要把源文件编译成中间代码文件,在 Windows 下也就是 .obj 文件,UNIX 下是 .o 文件,即 Object File,这个动作叫做 编译(compile)。
- Makefile 机制:
- 如果这个工程没有编译过,那么我们的所有 c 文件都要编译并被链接
- 如果这个工程的某几个 c 文件被修改,那么我们只编译被修改的 c 文件,并链接目标程序.
- 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的 c 文件,并链接目标程序。
- Makefile 格式:
-
即: prerequisites 中如果有一个以上的文件比 target 文件要新的话, command 所定义的命令就会被执行。
#makefile 规则 target ... : prerequisites ... command ... # target 可以是一个 object fle(目标文件),也可以是一个执行文件,还可以是一个标签(label)。可以有多个, 可以有通配符, # prerequisites 生成该 target 所依赖的文件和/或 target # command 该 target 要执行的命令(任意的 shell 命令) 一定要以 tab 开头!
- make 并不管命令是怎么工作的,他只管执行所定义的命令。 make 会比较 targets 文件和 prerequisites 文件的修改日期,如果 prerequisites 文件的日期要比 targets 文件的日期要新,或者target 不存在的话,那么, make 就会执行后续定义的命令。
- 若冒号后什么也没有,那么, make 就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在 make 命令后明显得指出这个 label 的名字。
- 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么 make 就会直接退出,并报错,
- 而对于所定义的命令的错误,或是编译不成功, make 根本不理。
-
- **`make`** 是如何工作的 / `make` 的执行步骤 :
- make 会在当前目录下找名字叫
Makefle
或makefle
的文件。大多数的 make 都支持 “makefle” 和 “Makefle” 这两种默认文件名。- 也可以指定文件:
make -f Make.Linux
- 也可以指定文件:
- 如果找到,读入被 include 的其它 Makefle。
- 当该文件不存在时,make 会寻找是否有生成它的规则,如果有,则执行其生成命令,然后再尝试读入。(其规则的使用, 参考 `自动生成依赖章节` )
- 在 include 前加减号
-
可以上 make 忽略其产生的错误,并不输出任何错误信息。
- 初始化文件中的变量。
- 它会找文件中的第一个目标文件(target),并把这个文件作为最终的目标文件。
- Makefle 中只应该有一个最终目标,其它的目标都是被这个目标所连带出来的,所以一定要让 make 知道你的最终目标是什么。
- 根据 Makefile 机制递归执判断文件依赖, 并执行相关命令;
- 推导隐晦规则,并分析所有规则
- 为所有的目标文件创建依赖关系链
- 根据依赖关系,决定哪些目标要重新生成
- 执行生成命令
- make 会在当前目录下找名字叫
- Makefile 里包含什么
- 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由 Makefle 的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
- 隐晦规则。由于我们的 make 有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefle,这是由 make 所支持的。
- 变量的定义。在 Makefle 中我们要定义一系列的变量,变量一般都是字符串,这个有点像你 C 语言中的宏,当 Makefle 被执行时,其中的变量都会被扩展到相应的引用位置上。
- 文件指示。其包括了三个部分,一个是在一个 Makefle 中引用另一个 Makefle,就像 C 语言中的
#include
一样;另一个是指根据某些情况指定 Makefle 中的有效部分,就像 C 语言中的预编译#if
一样;还有就是定义一个多行的命令。 - 注释。 Makefle 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用
#
字符,这个就像 C/C++ 中的//
一样。如果你要在你的 Makefle 中使用#
字符,可以用反斜杠进行转义,如:\#
。
- makefile 的自动推导:
- 只要 make 看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中,如果 make 找到一个
whatever.o
,那么whatever.c
就会是whatever.o
的依赖文件。
- 只要 make 看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中,如果 make 找到一个
0x01 变量,函数,命令,条件判断
命令
- 每条规则中的命令和操作系统 Shell 的命令行是一致的。 make 会一按顺序一条一条的执行命令,每条命令的开头必须以 Tab 键开头,除非,命令是紧跟在依赖规则后面的分号后的。在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以 Tab 键开头的,那么 make 会认为其是一个空命令。
- 如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令
- 通常, make 会把其要执行的命令行在命令执行前输出到屏幕上。当我们用
@
字符在命令行前,那么,这个命令将不被 make 显示出来,echo
使用-e
可以打印出带颜色的输出;
- 而在
rm
命令前面加了一个小减号-
的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。但 make 仍会报错:-mkdir test
的第二次输出:mkdir: 无法创建目录 “test”: 文件已存在; make: [clean] 错误 1 (忽略)
- make 的参数的是
-k
或是--keep-going
,这个参数的意思是,如果某规则中的命令出错了,那么就终止该规则的执行,但继续执行其它规则。
- 调试 :
- 如果 make 执行时,带入 make 参数
-n
或--just-print
,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的 Makefle,看看我们书写的命令是执行起来是什么样子的或是什么顺序的。 - 使用 autotools 或者 cmake 时, 你还可以使用
make VERBOSE=1
或make V=1
来获取到全部的命令.
- 如果 make 执行时,带入 make 参数
- make 参数
-s
或--silent
或--quiet
则是全面禁止命令的显示。 - 大型工程中的嵌套 make :
cd subdir && $(MAKE)
或者$(MAKE) -C subdir
- 如果你要传递所有的变量,那么,只要一个
export
就行了。后面什么也不用跟,表示传递所有的变量。 - 有两个变量,一个是
SHELL
,一个是MAKEFLAGS
,这两个变量不管你是否 export,其总是要传递到下层 Makefle 中,特别是MAKEFLAGS
变量,其中包含了 make 的参数信息, - `MAKEFLAGS = -j` 启动默认多核编译!
变量
- Makefle 中的变量其实就是 C/C++ 中的宏。如果你要让通配符在变量中展开,也就是让 objects 的值是所有 .o 的文件名的集合(
objects = *.o
),那么,你可以这样:- 列出一确定文件夹中的所有 .c 文件:
objects := $(wildcard *.c)
- 列出 (1) 中所有文件对应的 .o 文件:
objects := $(patsubst %.c,%.o,$(wildcard *.c))
- 变量是大小写敏感的
- 推荐使用大小写搭配的变量名,如: MakeFlags。这样可以避免和系统的变量冲突,而发生意外的事情。
- 变量在声明时需要给予初值,而在使用时,需要给在变量名前加上 $ 符号
- 变量可以使用在许多地方,如规则中的“目标”、“依赖”、“命令”以及新的变量中。
- 列出一确定文件夹中的所有 .c 文件:
- make 支持三个通配符:
*
,?
和~
。- 波浪号(~ )字符在文件名中也有比较特殊的用途。如果是 ~/test ,这就表示当前用户的 $HOME 目录下的 test 目录。而 ~hchen/test 则表示用户 hchen 的宿主目录下的 test 目录。(这些都是 Unix 下的小知识了, make 也支持)
- 通配符代替了你一系列的文件,如 *.c 表示所有后缀为 c 的文件。\
- 模式:
- 在模式规则中,目标名中需要包含有模式字符
%
(一个),包含有模式字符%
的目标被用来匹配一个文件名,“%”可以匹配任何非空字符串。 - 规则的依赖文件中同样可以使用
%
,依赖文件中模式字符“%”的取值情况由目标中的%
来决定。例如:对于模式规则%.o : %.c
,它表示的含义是:所有的 .o 文件依赖于对应的 .c 文件。 - 要注意的是:模式字符
%
的匹配和替换发生在规则中所有变量和函数引用展开之后,变量和函数的展开一般发生在 make 读取 Makefile。 - 在模式规则中,目标文件是一个带有模式字符
%
的文件,使用模式来匹配目标文件。文件名中的模式字符%
可以匹配任何非空字符串,除模式字符以外的部分要求一致。例如:%.c
匹配所有以 “.c” 结尾的文件(匹配的文件名长度最少为 3 个字母),s%.c
匹配所有第一个字母为 “s”,而且必须以 “.c” 结尾的文件,文件名长度最小为 5 个字符(模式字符%
至少匹配一个字符)。
- 在模式规则中,目标名中需要包含有模式字符
- Makefile 中
:= ?= += =
的区别 =
是最基本的赋值, 变量的值是整个 makefil e中 最后 被指定的值。:=
是覆盖之前的值, 直接赋值,赋予当前位置的值。?=
是如果没有被赋值过就赋予等号后面的值+=
是添加等号后面的值- 有时候需要实现 Makefile 中没有的
-=
功能, 这时候可以采用 shell 命令来实现 :C_Flags -= -Wconversion
等价于下方的 :C_Flags:=$(shell echo $(C_Flags)|sed -s 's/-Wconversion//g')
- 我们可以替换变量中的共有的部分,其格式是
$(var:a=b)
或是${var:a=b}
,其意思是,把变量 “var” 中所有以 “a” 字串 “结尾” 的 “a” 替换成 “b” 字串。这里的 “结尾” 意思是 “空格” 或是 “结束符” 。bar := $(foo:%.o=%.c)
- 注意 var 和 : 之间不能有空格;
- 目标变量:
- 某个目标设置局部变量,这种变量被称为“Target-specifc Variable”,它可以和“全局变量”同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值。
prog : C_Flags = -g
: 对 prog 目标添加-g
调试选项;../calc_base/cpu_calc.o : C_Flags += -O2
: 在编译时, 对cpu_calc.o
单独进行-O2
优化.
- 某个目标设置局部变量,这种变量被称为“Target-specifc Variable”,它可以和“全局变量”同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值。
- 模式变量:
- 模式变量的好处就是,我们可以给定一种“模式”,可以把变量定义在符合这种模式的所有目标上。
- 如:
%test.o : C_Flags += -O2
, 针对所有*test.o
文件专门进行-O2
优化;
- 如:
- 模式变量的好处就是,我们可以给定一种“模式”,可以把变量定义在符合这种模式的所有目标上。
- 自动化变量:
$@
: 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么, $@ 就是匹配于目标中模式定义的集合。$%
: 仅当目标是函数库文件中,表示规则中的目标成员名。- 例如,如果一个目标是
foo.a(bar.o)
,那么,$%
就是 bar.o ,$@
就是 foo.a 。如果目标不是函数库文件(Unix 下是 .a , Windows下是 .lib ),那么,其值为空。
- 例如,如果一个目标是
$<
: 依赖目标中的第一个目标名字。如果依赖目标是以模式(即 % )定义的,那么$<
将是符合模式的一系列的文件集。注意,其是一个一个取出来的。$?
: 所有比目标新的依赖目标的集合。以空格分隔。$^
: 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。$+
: 这个变量很像$^
,也是所有依赖目标的集合。只是它不去除重复的依赖目标。
判断
ifeq/ifneq/ifdef、 else 和 endif
,- 在
这一行上,多余的空格是被允许的,但是不能以 Tab 键作为开始(不然就被认为是命令)。
- 在
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif
- make 是在读取 Makefle 时就计算条件表达式的值,并根据条件表达式的值来选择语句,
- 所以, 你最好不要把自动化变量(如 $@ 等)放入条件表达式中,因为自动化变量是在运行时才有的。
函数
- 函数调用后,函数的返回值可以当做变量来使用.
- 函数的使用方法:
$(<function> <arguments>)
<function>
就是函数名,<arguments>
为函数的参数,参数间以逗号 , 分隔,而函数名和参数之间以“空格”分隔。- 函数调用以
$
开头,以圆括号或花括号把函数名和参数括起。
- 字符串处理函数
$(subst <from>,<to>,<text>)
- 名称:字符串替换函数
- 功能:把字串
<text>
中的<from>
字符串替换成<to>
。 - 返回:函数返回被替换过后的字符串。
- demo:
$(subst ee,EE,feet on the street)
- 结果:
fEEt on the strEEt
- 结果:
$(patsubst <pattern>,<replacement>,<text>)
$(strip <string>)
- 去掉
<string>
字串中开头和结尾的空字符。
- 去掉
$(filter <pattern...>,<text>)
- 以
<pattern>
模式过滤<text>
字符串中的单词,保留符合模式<pattern>
的单词。可以有多个模式, 以空格分隔。
- 以
- 文件名操作函数
$(dir <names...>)
- 从文件名序列
<names>
中取出目录部分。目录部分是指最后一个反斜杠(/
)之前的部分。如果没有反斜杠,那么返回./
- 从文件名序列
$(notdir <names...>)
- 从文件名序列
<names>
中取出非目录部分。非目录部分是指最後一个反斜杠(/
)之后的部分。
- 从文件名序列
$(join <list1>,<list2>)
- 把
<list2>
中的单词对应地加到<list1>
的单词后面。如果<list1>
的单词个数要比<list2>
的多,那么,<list1>
中的多出来的单词将保持原样。如果<list2>
的单词个数要比<list1>
多,那么,<list2>
多出来的单词将被复制到<list1>
中。
- 把
$(foreach <var>,<list>,<text>)
- 这个函数的意思是,把参数
<list>
中的单词逐一取出放到参数<var>
所指定的变量中,然后再执行<text>
所包含的表达式。每一次<text>
会返回一个字符串,循环过程中,<text>
的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>
所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。
- 这个函数的意思是,把参数
- shell 函数:
cur-dir := $(shell pwd)
- 这个函数会新生成一个 Shell 程序来执行命令,
- 所以你要注意其运行性能,如果你的 Makefle 中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。
-$(subst output,,$@)
中的$
表示执行一个 Makefle 的函数,函数名为 subst,后面的为参数。$@
这个变量表示着目前规则中所有的目标的集合, 就像一个数组.
静态模式 demo
-
下面的例子中,
#静态模式 objects = foo.o bar.o all: $(objects) $(objects): %.o: %.c $(CC) -c $(CFLAGS) $< -o $@
- 指明了我们的目标从 $object 中获取, %.o 表明要所有以 .o 结尾的目标,也就是 foo.o bar.o ,也就是变量 $object 集合的模式,
- 而依赖模式 %.c 则取模式 %.o 的 % ,也就是 foo bar,并为其加下 .c 的后缀,于是,我们的依赖目标就是 foo.c bar.c 。
-
而命令中的 $< 和 $@ 则是自动化变量,
$<
表示第一个依赖文件,$@
表示目标集(也就是“foo.o bar.o”)。files = foo.elc bar.o lose.o $(filter %.o,$(files)): %.o: %.c $(CC) -c $(CFLAGS) $< -o $@ $(filter %.elc,$(files)): %.elc: %.el emacs -f batch-byte-compile $<
自动生成依赖
- 在 Makefle 中,我们的依赖关系可能会需要包含一系列的头文件,但如果是一个比较大型的工程,你必需清楚哪些 C 文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改 Makefle,这是一个很没有维护性的工作。
- 为了避免这种繁重而又容易出错的事情,我们可以使用 C/C++ 编译的一个功能。大多数的 C/C++ 编译器都支持一个
-M
的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。
- 为了避免这种繁重而又容易出错的事情,我们可以使用 C/C++ 编译的一个功能。大多数的 C/C++ 编译器都支持一个
- 下面介绍 GCC
-M
相关选项的用法:-M
: 生成文件关联的信息。包含目标文件所依赖的所有源代码, 包含了库的 头文件;-MM
: 与-M
相比, 忽略了库文件;-MMD
: 和-MM
相同,但是输出将导入到.d
的文件里面, 且编译源文件:-MD
: 与-MMD
类似,只是导出的内容有所不一样,内容与-M
是一样的, 也会编译源文件;-MF
: 指一个文件用于存放生成文件的关联信息,这些信息与-M
或-MM
是一样的,所以要与-M
或-MM
一些使用,否则会报错,-MT
: 指定目标文件名
- 从上面make的执行过程中可看出,要动态生成依赖关系,只能利用第 2 步读入其它 Makefile 的机制。
- 那么,我们是否可以先把生成的依赖关系保存到文件,然后再把该文件的内容包含进来?
- 答案是 Yes! 要利用 makefile 的 include 机制。
示例:
include $(sources:.c=.d)
#所有的 .d 文件依赖于 .c 文件,
%.d: %.c
# @关键字告诉 make 不输出该行命令;set -e 的作用是,当后面的命令的返回值非0时,立即退出。
@set -e; \
# 删除所有的目标,也就是 .d 文件
rm -f $@; \
# 为每个依赖文件 $< ,也就是 .c 文件生成依赖文件,$@ 表示模式 %.d 文件, \
# $$$$ 为字符串 "$$", 而 $$ 是 shell 的特殊变量,它的值为当前进程号 \
# (使用进程号为后缀的名称创建临时文件,是shell编程常用做法,这样可保证文件唯一性。)
$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
# 使用 sed 命令做了一个替换,
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
#第四行就是删除临时文件。
rm -f $@.$$$$
- 下面介绍下上述
sed
的用法:- 这条 sed 命令的结构是
s/match/replace/g
。有时为了清晰,可以把每个 / 写成逗号,即这里的格式 s,match,replace,g 。 - 该命令表示把源串内的 match 都替换成 replace,s 指示 match 可以是正则表达式。
- g 表示把每行内所有 match 都替换,如果去掉 g,则只有每行的第1处 match 被替换(实际上不需要 g,因为一个 .d 文件中,只会在开头有一个 main.o:)。
- 这里 match 是正则式;
- 这条 sed 命令的结构是
- 总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入 .d 文件的依赖,
- 即:
main.o : main.c defs.h
->main.o main.d : main.c defs.h
- 即:
- 我们的 .d 文件也会自动更新了,并会自动生成了,当然,你还可以在这个 .d 文件中加入的不只是依赖关系,包括生成的命令也可一并加入,让每个 .d 文件都包含一个完赖的规则。
技巧总结
-
你希望第二条命令得在 cd 之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。
- 通常, make 会把其要执行的命令行在命令执行前输出到屏幕上。当我们用
@
字符在命令行前,那么,这个命令将不被 make 显示出来,echo
使用-e
可以打印出带颜色的输出;
-
而在 rm 命令前面加了一个小减号
-
的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。但 make 仍会报错:-mkdir test mkdir: 无法创建目录 "test": 文件已存在; make: [clean] 错误 1 (忽略)
- clean 的规则不要放在文件的开头,不然,这就会变成 make 的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean 从来都是放在文件的最后”.
- 反斜杠(
\
)是换行符的意思。这样比较便于 makefle 的阅读。- 如果命令太长,你可以使用反斜杠(\ )作为换行符。 make 对一行上有多少个字符没有限制。
- 一般来说, make 会以 UNIX 的标准 Shell,也就是 /bin/sh 来执行命令。