LV02-05-Makefile-02-变量和目标
本文主要是makefile——变量与目标相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
开发板 | 正点原子 i.MX6ULL Linux阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
STM32开发板 | 正点原子战舰V3(STM32F103ZET6) |
点击查看本文参考资料
参考方向 | 参考原文 |
Makefile | 跟我一起写Makefile |
一、 Makefile 变量
变量,我们并不陌生, Makefile 又不是编程语言,为什么也需要变量呢?我们什么需要创建变量呢?创建变量的目的其实就是用来代替一个文本字符串:
(1)系列文件的名字
(2)传递给编译器的参数
(3)需要运行的程序
(4)需要查找源代码的目录
(5)你需要输出信息的目录
(6)我们想做的其它事情。
1. 变量的定义
1.1 定义格式
变量定义的基本格式如下:
1 | variable=value |
【说明】
(1)变量的不需要使用数据类型。变量的名称可以由大小写字母、阿拉伯数字和下划线构成。
(2)等号左右的空白符没有明确的要求,因为在执行 make 的时候多余的空白符会被自动的删除。
(3) value 表示值列表,既可以是零项,又可以是一项或者是多项。例如,
1 | VALUE_LIST = one two three |
(4)变量定义时是可以换行书写的,只是需要在每行结尾处添加一个 \ 。
1.2 使用实例
点击查看换行书写实例
Makefile 文件内容如下:
1 | a = main.o \ |
然后在终端中执行 make test ,可以看到终端会有以下内容输出:
1 | a=main.o test.o test.h |
2. 变量的使用
2.1 使用格式
上边定义的变量如何引用呢?引用格式如下:
1 | $(VALUE_LIST) |
2.2 使用实例
现在我们来试验一下,修改 Makefile 文件为以下内容:
1 | OBJ = main.o test1.o test2.o |
点击查看相关文件内容
【注意】目前所有文件都在同一个目录下。
1 |
|
1 |
|
1 |
|
1 | void test1Fun(); |
这里我使用了两种引用方式,用以说明两种引用方式均可。然后执行 make 命令,会发现,最后依然正常生成了文件。当我们要添加或者是删除某个依赖文件的时候,我们只需要改变变量 OBJ 的值就可以了。
3. 变量的赋值
上边我们定义变量的时候使用的符号是 = ,但是其实在 Makefile 中,赋值方式有四种:
符号 | 说明 |
:= | 简单赋值,编程语言中常规理解的赋值方式,只对当前语句的变量有效。 |
= | 递归赋值,赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。 |
?= | 条件赋值,如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。 |
+= | 追加赋值,原变量用空格隔开的方式追加一个新值。 |
3.1 简单赋值 :=
我们在 Makefile 文件中添加以下内容:
1 | a:=fanhua |
然后在终端中执行:
1 | make test |
会看到输出的结果如下:
1 | a=qidaink |
用这种方式定义的变量,会在变量的定义点,按照被引用的变量的当前值进行展开 。
3.2 递归赋值 =
我们在 Makefile 文件中添加以下内容:
1 | a=fanhua |
然后在终端中执行:
1 | make test |
会看到输出的结果如下:
1 | a=qidaink |
这意味着,即便 a 是在 b 后边进行了修改,但是变量 b 依然引用了 a 修改之后的值。这种赋值方式优点是可以向后引用变量,缺点是不能对该变量进行任何扩展,例如:
1 | a=fanhua |
这样会造成一种死循环,应该是会直接报错的:
1 | Makefile:3: *** Recursive variable 'a' references itself (eventually)。 停止。 |
3.4 条件赋值 ?=
我们在 Makefile 文件中添加以下内容:
1 | a:=fanhua |
然后在终端中执行:
1 | make test |
会看到输出的结果如下:
1 | a=fanhua |
若是将第一行的 a:=fanhua 删除,则输出结果是这样的:
1 | a=qidaink |
这种赋值方式其实就等价于:
1 | ifeq ($(origin a), undefined) |
【说明】什么是 ifeq ?其实是 Makefile 中的条件判断,后边会详细解释,这里简单了解下就可以了。
3.5 追加赋值
我们在 Makefile 文件中添加以下内容:
1 | a:=fanhua |
然后在终端中执行:
1 | make test |
会看到输出的结果如下:
1 | a=fanhua fanhua 123456@qq.com fanhua |
4. 预定义变量
4.1 常见预定义变量
在 Makefile 中也存在着一些预定义变量:
AR | 库文件维护程序的名称,默认值为ar。AS汇编程序的名称,默认值为as。 |
CC | C编译器的名称,默认值为cc。CPP C预编译器的名称,默认值为$(CC) –E。 |
CXX | C++编译器的名称,默认值为g++。 |
FC | FORTRAN编译器的名称,默认值为f77 |
RM | 文件删除程序的名称,默认值为rm -f |
ARFLAGS | 库文件维护程序的选项,无默认值。 |
ASFLAGS | 汇编程序的选项,无默认值。 |
CFLAGS | C编译器的选项,无默认值。 |
CPPFLAGS | C预编译的选项,无默认值。 |
CXXFLAGS | C++编译器的选项,无默认值。 |
FFLAGS | FORTRAN编译器的选项,无默认值。 |
4.2 使用实例
我们在 Makefile 文件中添加以下内容:
1 | test: |
然后在终端中执行:
1 | make test |
会看到输出的结果如下:
1 | CC = cc |
5. 自动化变量
5.1 常用自动化变量
自动化变量可以理解为由 Makefile 自动产生的变量。
$* | 不包含扩展名的目标文件名称(当文件名中存在目录时,也会包含目录部分)。例如, (1)main.o: main.c test.h中 $* 就代表 main 。 (2)如果目标是"dir/a.foo.b",并且目标的模式是 "a.%.b",那么,"$*"的值就是"dir/a.foo"。 【说明】在模式规则和静态模式规则中,代表“茎”。“茎”是目标模式中“%”所代表的部分(当文件名中存在目录时, “茎”也包含目录部分)。 |
$@ | 表示规则的目标文件完整名称名。如果目标是一个文档文件(Linux 中,一般成 .a 文件为文档文件,也成为静态的库文件),那么它代表这个文档的文件名。在多目标模式规则中,它代表的是触发规则被执行的文件名。 |
$% | 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是"foo.a(bar.o)", 那么,"$%"就是"bar.o","$@"就是"foo.a"。如果目标不是函数库文件(Unix 下是[.a],Windows 下是[.lib]),那么,其值为空。 |
$< | 规则的第一个依赖的文件名。如果是一个目标文件使用隐含的规则来重建,则它代表由隐含规则加入的第一个依赖文件。如果依赖目标是以模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来执行的 |
$? | 所有比目标文件更新的依赖文件列表,空格分隔。如果目标文件时静态库文件,代表的是库文件(.o 文件)。 |
$^ | 代表的是所有依赖文件列表,使用空格分隔。如果目标是静态库文件,它所代表的只能是所有的库成员(.o 文件)名。一个可重复的文件出现在目标的依赖中,变量“$^”只记录它的第一次引用的情况。就是说变量“$^”会去掉重复的依赖文件。 |
$+ | 所有的依赖文件,以空格分开。类似“$^”,但是它保留了依赖文件中重复出现的文件。主要用在程序链接时库的交叉引用场合。 |
【说明】
(1)茎的概念在后边静态模式的笔记中会有说明。
(2)对于 $< ,为了避免产生不必要的麻烦,我们最好给 $ 后面的那个特定字符都加上圆括号。
点击查看更为细致的引用方式说明
变量名 | 功能 |
$(@D) | 表示"$@"的目录部分(不以斜杠作为结尾),如果"$@"值是"dir/foo.o",那么"$(@D)"就是"dir",而如果"$@"中没有包含斜杠的话,其值就是"."(当前目录)。 |
$(@F) | 表示"$@"的文件部分,如果"$@"值是"dir/foo.o",那么"$(@F)"就是"foo.o","$(@F)"相当于函数"$(notdir $@)"。 |
$(*D) | 分别代表 "茎" 中的目录部分和文件名部分。 |
$(*F) | |
$(%D) | 当以 "archive(member)" 形式静态库为目标时,分别表示库文件成员 "member" 名中的目录部分和文件名部分。踏 |
$(%F) | |
$(<D) | 表示第一个依赖文件的目录部分和文件名部分。 |
$(<F) | |
$(^D) | 分别表示所有依赖文件的目录部分和文件部分。(无相同的) |
$(^F) | |
$(+D) | 分别表示所有的依赖文件的目录部分和文件部分。(可以有相同的) |
$(+F) | |
$(?D) | 分别表示更新的依赖文件的目录部分和文件名部分。 |
$(?F) |
5.2 使用实例
修改 Makefile 文件如下:
1 | OBJ= main.o test1.o test2.o |
执行以下命令:
1 | make clean |
输出结果如下:
1 | gcc -c main.c -o main.o |
6. 变量高级用法
6.1 变量值的替换
我们可以在引用的时候直接替换变量的值,可以直接替换变量中的共有的部分,一般格式如下:
1 | # 先定义一个变量,并简单赋值 |
上边的含义就是,将变量 var1 中的所有以 string1 字符串结尾的变量值替换成以 string2 字符串结尾。
【注意】
(1)这里是结尾字符串的替换,其他位置的好像是不可以进行替换的。
(2)也可以使用模式规则进行替换,也就是通过 % 来匹配除需要替换的部分以外的字符串。
(3) var1:<string1>=<string2> 这一部分的 : 和 = 两端最好不要有空格,否则可能会出现问题。
点击查看实例
Makefile 文件内容如下:
1 | var1 := main.o test.o |
然后在终端运行 make test ,输出结果如下:
1 | var1=main.o test.o |
可以发现所有的 .o 都被替换为 .c 。
6.2 变量嵌套
变量的嵌套引用的具体含义是,我们可以在一个变量的赋值中引用其他的变量,并且引用变量的数量和和次数是不限制的。也就是说可以把变量的值再当成变量。例如,
1 | x = y |
然后在终端执行 make test ,会看到如下输出:
1 | x=y |
其实, $(x) 的值就是 y ,而外边还有一个 $ ,这样就会变成 $(y) 而 $(y)=z ,所以最后就是 a=$($(x))=$(y)=z 了。
【说明】遇到这种变量嵌套的情况吗,我们就从最里层的 $ 开始向外一层一层进行分析即可。
7. 目标变量
7.1 使用格式
我们可以为某个目标设置局部变量,这种变量被称为 Target-specific Variable 。一般语法格式如下:
1 | <target ...> : <variable-assignment> |
<variable-assignment> 可以是各种赋值表达式,如 = 、 := 、 += 或是 ?= 。
【注意】
目标变量可以和全局变量同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效,而不会影响规则链以外的全局变量的值。
7.2 使用实例
点击查看实例
Makefile 文件内容如下:
1 | a := a.o b.o c.o |
然后我们在终端中执行 make test ,会发现终端输出信息如下:
1 | a=main.o test.o |
接着我们在终端中再执行 make a ,会发现终端中输出的信息如下:
1 | a=a.o b.o c.o |
这也就说明了,目标变量仅仅在它自己的规则中有效,并且 make 会优先使用规则内定义的局部变量。
二、Makefile 目标
1. 特殊目标
Makefile 中有很多特殊的目标,很可能都不会接触到,但是还是在这里写下笔记吧,万一后边用到了呢。
点击查看特殊目标
名称 | 功能 |
.PHONY | 这个目标的所有依赖被作为伪目标。伪目标是这样一个目标:当使用 make 命令行指定此目标时,这个目标所在的规则定义的命令、无论目标文件是否存在都会被无条件执行。 |
.SUFFIXES | 这个目标的所有依赖指出了一系列在后缀规则中需要检查的后缀名 |
.DEFAULT | Makefile 中,这个特殊目标所在规则定义的命令,被用在重建那些没有具体规则的目标,就是说一个文件作为某个规则的依赖,却不是另外一个规则的目标时,make 程序无法找到重建此文件的规则,这种情况就执行 ".DEFAULT" 所指定的命令。 |
.PRECIOUS | 这个特殊目标所在的依赖文件在 make 的过程中会被特殊处理:当命令执行的过程中断时,make 不会删除它们。而且如果目标的依赖文件是中间过程文件,同样这些文件不会被删除。 |
.INTERMEDIATE | 这个特殊目标的依赖文件在 make 执行时被作为中间文件对待。没有任何依赖文件的这个目标没有意义。 |
.SECONDARY | 这个特殊目标的依赖文件被作为中过程的文件对待。但是这些文件不会被删除。这个目标没有任何依赖文件的含义是:将所有的文件视为中间文件。 |
.IGNORE | 这个目标的依赖文件忽略创建这个文件所执行命令的错误,给此目标指定命令是没有意义的。当此目标没有依赖文件时,将忽略所有命令执行的错误。 |
.DELETE_ON_ERROR | 如果在 Makefile 中存在特殊的目标 ".DELETE_ON_ERROR" ,make 在执行过程中,荣国规则的命令执行错误,将删除已经被修改的目标文件。 |
.LOW_RESOLUTION_TIME | 这个目标的依赖文件被 make 认为是低分辨率时间戳文件,给这个目标指定命令是没有意义的。通常的目标都是高分辨率时间戳。 |
.SILENT | 出现在此目标 ".SILENT" 的依赖文件列表中的文件,make 在创建这些文件时,不打印出此文件所执行的命令。同样,给目标 "SILENT" 指定命令行是没有意义的。 |
.EXPORT_ALL_VARIABLES | 此目标应该作为一个简单的没有依赖的目标,它的功能是将之后的所有变量传递给子 make 进程。 |
.NOTPARALLEL | Makefile 中如果出现这个特殊目标,则所有的命令按照串行的方式执行,即使是存在 make 的命令行参数 "-j" 。但在递归调用的子make进程中,命令行可以并行执行。此目标不应该有依赖文件,所有出现的依赖文件将会被忽略。 |
2.伪目标
还记得前边提到的 .PHONY:clean 把,当时说这是一个伪目标,但是并未说明啥叫伪目标,伪目标有啥用,接下来,就来探索一下吧。
2.1 一个问题?
还是以 clean 这个清除操作为例,我们在编译过后,会生成大量的中间文件,当我们定义了 clean 命令后,却并不需要依赖于任何文件,而且也不需要生成任何文件,我们只需要执行这个规则下边的命令即可.
正常情况下,我们执行 make clean 命令就可以执行 clean 目标下的文件,但是如果说 Makefile 文件所在目录下刚好有一个文件叫 clean ,由于这个文件不依赖于任何文件,也不会被修改,所以,它永远都是最新的。于是,我们除了第一次执行 make clean 命令有效外,再执行的时候,就会发现 make 一直会提醒:
1 | make: “clean”已是最新。 |
make 只会处理修改过的文件,为我们带来便利的同时,也为我们带来了隐藏的麻烦,也就是说我们的目标名不能与某一个文件名一致。
2.2 怎么办呢?
为了解决上边的问题,我们可以使用一种特殊的目标 .PHONY , make 不会去检查是否存在有文件名和依赖体中的一个名字相匹配的文件,而是直接执行与之相关的命令,于是这也就被称为伪目标。 make 后不会生成与伪目标同名的文件,伪目标只是一个标签。
总的来说,伪目标的作用其实就是为了避免目标名与文件名冲突。那么怎么声明伪目标呢?语法格式如下:
1 |
|
其实一开始的 clean 就是一个很好的例子:
1 |
|
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标说到底也算是一个目标,同样可以作为最终目标,只要将其放在第一个即可。
其实,只要目标名称不要与某一个文件名称一致,不用声明成伪目标也可以,但是建议还是注意一下这个问题。
2.3 成为依赖
伪目标可以成为依赖,这是什么意思呢?接下来我们来看一个例子:
1 |
|
这样的话,我们执行 make clean 命令会清除所有的 .o 文件和 main 可执行文件,执行 main clean-o 便会只清除所有的 .o 文件。这样我们就可以通过执行不同的命令来删除一部分文件,而保留不想删除的文件。
3. 多目标
啥又是多目标嘞?简单来说就是一个 Makefile 文件,直接生成多个可执行文件, Makefile 文件每次只能有一个最终目标,也就是说正常情况下,只会有一个可执行文件,加上一堆的中间文件。
3.1 单目标测试
其实我们可以试一下在一个 Makefile 中写上两个可执行文件的编译链接规则,然后执行 make 来看看最后输出多少个可执行文件。
点击查看Makefile 文件
1 | OBJ = main.o test1.o test2.o |
点击查看相关文件内容
【说明】目前所有文件都在同一个目录下。
1 |
|
1 |
|
1 |
|
1 |
|
1 | void test1Fun(); |
然后在终端执行 make 命令,会发现生成了与 main 目标相关的可执行文件和中间文件,但是 main-copy 目标没有生成,也没有生成中间文件。这个时候我们执行 make main-copy ,会发现,这个时候相应的目标才会生成。
所以说,一般情况下,一个 Makefile 文件只通过 make 命令一般只会生成一个最终目标,但是想要生成两个甚至多个也不是不可以。
3.2 多目标生成方式一
其实很容易想到,上边我们不是学习了伪目标嘛,它是不生成文件的,但是它是可以放在文件开头充当最终目标的,于是我们可以修改 Makefile 文件如下:
点击查看Makefile 文件
1 | all: main main-copy |
【说明】有的 Makefile 可能不会标明 .PHONY: all 这一句,如果能保证工程文件和生成文件没有与伪目标同名的文件的话,不写也是不会有问题的,不写的话可以理解为标签,写的话可以理解为伪目标(伪目标和标签都不会生成相应的文件)。
点击查看相关文件内容
【说明】目前所有文件都在同一个目录下。
1 |
|
1 |
|
1 |
|
1 |
|
1 | void test1Fun(); |
然后,我们在终端中再次执行 make ,这个时候就会发现,生成了两个可执行文件 main 和 main-copy 。
3.3 多目标生成方式二
上边的其实是通过伪目标的方式来达到生成多个目标文件的方式,总的来说还是一个规则只有一个目标,是多个单目标的规则。
而 Makefile 还支持一个规则中有多个目标,这个多目标规则所定义的命令对所有目标都生效,一个具有多目标的规则相当于定义了多个单目标规则,但是它们执行的命令类似。多目标规则意味着所有的目标具有相同的依赖文件。
多目标通常用于以下情况:
(1)仅需要一个描述依赖关系的规则,不需要在规则中定义命令。例如,
1 | 这个规则实现了同时给三个目标文件指定一个依赖文件 |
(2)对于多个具有类似重建命令的目标。重建这些目标的命令并不需要是完全相同,我们可以在命令行中使用自动化变量 $@ 来引用具体的目标,完成对它们的重建。
点击查看实例
【说明】其实这里我不是很理解😅,但是网上有一个《跟我一起写Makefile》的文档,还有很多资料上都举了这个例子,说实话,学到这里的时候我还是不理解这个例子,由于还没用到过,暂时先写在这,后续用到了再更新这里的笔记。
1 | bigoutput littleoutput : text.g |
上边的 generate 根据命令行参数来决定输出文件的类型。使用了 make 的字符串处理函数 subst 来根据目标产生对应的命令行选项。
在多目标的规则中,虽然可以根据不同的目标使用不同的命令(在命令行中使用自动化变量 $@ )。但是,多目标的规则并不能做到根据目标文件自动改变依赖文件(像上边例子中使用自动化变量 $@ 改变规则的命令一样)。需要实现这个目的的话,需要要用到 make 的静态模式。
4. 多规则目标
4.1 :: 规则
看完上边的,我会想,那要是我一个目标需要多个规则来完成怎么办?这个时候我们可以使用 :: 双冒号,这种规则也可以被称之为双冒号规则。
双冒号规则允许在多个规则中为同一个目标指定不同的重建目标的命令。
在 Makefile 中,一个目标可以出现在多个规则中,但是这些规则必须是同一类型的,要么都是普通规则,要么都是双冒号规则,坚决不允许一个目标出现在两种规则中。
- 双冒号规则与普通规则的不同
(1)双冒号规则中,当依赖文件比目标更新时,规则将会被执行。对于一个没有依赖而只有命令行的双冒号规则,当引用此目标时,规则的命令将会被无条件执行。而普通规则是,当规则的目标文件存在时,此规则的命令永远不会被执行(目标文件永远是最新的)。
(2)当同一个文件作为多个双冒号规则的目标时,这些不同的规则会被独立的处理,而不是像普通规则那样合并所有的依赖到一个目标文件。这就意味着对这些规则的处理就像多个不同的普通规则一样。也就是说多个双冒号规则中的每一个的依赖文件被改变之后, make 只执行此规则定义的命令,而其它的以这个文件作为目标的双冒号规则将不会被执行。
【注意】同一个目标出现在多个双冒号规则中时,规则的执行顺序和普通规则的执行顺序一样,按照其在 Makefile 文件中的书写顺序执行。
4.2 使用实例
其实学习到这里的时候我是很疑惑的,目前为止还没有遇到过这样的目标,下边的例子中,执行最终的可执行程序的时候,打印的内容是相同的,所以目前为止,我还不是很清楚这个规则究竟有什么用处,先写在这里吧,后边懂了再补充笔记。
点击查看实例
点击查看相关文件内容
1 |
|
1 |
|
Makefile 文件如下:
1 | main: main.c |
在终端执行 make ,会发现有如下输出内容:
1 | Makefile:4: 警告:覆盖关于目标“main”的配方 |
即便有警告,但是依然输出了相应的 main 可执行文件。
Makefile 文件如下:
1 | main:: main.c |
在终端执行 make ,会发现正常输出了,也没有警告。此时如果 main.c 文件被修改了, main 将会根据 main.c 被重建,如果 test.c 文件被修改了, main 将会根据 test.c 被重建。