读懂和编写Makefile需掌握的关键基础知识(常用符号、先解析再执行、make命令的第一个动作、规则的概念、目标及伪目标的相关概念、文件更新机制、几个重要的自动化变量等知识)

01-符号=、符号:=、符号+=

区分符号=和符号:=

Makefile 中,符号 :== 都用于定义变量,但它们的行为方式不同,主要体现在变量值的计算和展开时机上。


1. :=(简单赋值,立即展开)

  • 特点:使用 := 定义的变量会在赋值时立即计算和展开右边的值,然后将结果赋给变量。
  • 用途:当变量的值在定义时已经确定,并且不会再依赖于其他变量的值发生变化时,适合使用 :=

示例

FOO := $(BAR)
BAR := hello
  • 这里 FOO 的值在定义时就会被计算。当 FOO := $(BAR) 被执行时,BAR 的值(hello)被立即展开并赋值给 FOO
  • 最终结果:FOO = hello

2. =(递归赋值,延迟展开)

  • 特点:使用 = 定义的变量不会立即计算右边的值,而是延迟到变量被引用时才计算和展开右边的内容。
  • 用途:当变量的值可能依赖于其他变量的值发生变化时,适合使用 =

示例

FOO = $(BAR)
BAR = hello
  • 这里 FOO 的值在定义时不会被计算,而是保留 $(BAR) 作为变量的值。
  • FOO 被引用时,BAR 的值才会被计算并展开。
  • 最终结果:FOO = hello

3. 两者的区别总结

符号行为展开时机典型用途
:=简单赋值,立即计算和展开定义时展开值已确定、不依赖其他变量的情况
=递归赋值,保留引用,延迟展开使用时展开依赖于其他变量或需要动态计算的情况

4. 对比示例

BAR = hello
FOO = $(BAR)
BAZ := $(BAR)
BAR = world

all:
    @echo FOO = $(FOO)
    @echo BAZ = $(BAZ)
  • 结果
    FOO = world
    BAZ = hello
    

解释

  • FOO 使用了 = 定义,它延迟展开,因此 FOO 的值在 BAR 被重新赋值后变成 world
  • BAZ 使用了 := 定义,它在赋值时立即展开 BAR 的值,因此固定为 hello

5. 性能方面的考虑

  • := 性能更高:由于变量值在定义时已展开,后续使用时无需再进行计算。
  • = 更灵活:允许变量的值依赖其他变量动态变化。

总结建议

  • 使用 :=:如果变量的值是固定的,或在定义时已确定,可以使用 := 提高效率和简洁性。
  • 使用 =:如果变量的值需要动态变化,或依赖于其他变量的值,使用 = 提供更多的灵活性。

根据具体需求选择适当的赋值方式,可以编写更高效、清晰的 Makefile

符号+=

符号+=为追加赋值,变量的值会被追加到现有的值后面。
比如:

FOO := hello
FOO += world

上面这条两条语句使得变量FOO的值为 hello world

02-需要知道的几个基本常识

02-1-Makefile文件并不是顺序执行,而是先解析再执行

关于这一点的最直接的例子就是伪目标的定义和声明语句:

PHONY := __build
......
......
......
.PHONY : $(PHONY)

变量PHONY用于存储Makefile中的所有伪目标名,但是直到文Makefile的最后一行才对存储在变量PHONY中的所有伪目标名进行声明,但是中间的语句会用到这些伪目标名,这就充分证明了Makefile文件并不是顺序执行,而是先解析再执行。
关于伪目标的详细解释,请看本文第03点的第2小点的第⑵点。

02-2-make命令执行时到底是执行哪个Makefile文件

如果make命令是下面这种形式:

make 目标名

那么make就会进入当前目录查找名为Makefile的文件开始执行构建。

如果make命令是下面这种形式:

make -C ./ -f $(TOPDIR)/Makefile.build
或者
make -C $@ -f $(TOPDIR)/Makefile.build

此时相当于指定了具体是执行哪个Makefile文件,-f 参数就是指定体是执行哪个Makefile文件的参数,在这里具体的Makefile文件是$(TOPDIR)/Makefile.build

02-3-make命令如果没有指定目标,那么执行哪一个目标?

这一点比较重要,请参看后面对这个问题的介绍,往后。搜索“如果make命令没有指定目标,那么执行哪一个目标”

04-规则的概念和介绍

规则是Makefile中执行具体动作的结构,是Makefile中最重要的结构。

Makefile 中的每个规则通常包含以下部分:

target: dependencies
    command
  • target(目标):这是规则中的第一个部分,表示你希望 make 构建或更新的文件或动作。它可以是一个文件,也可以是一个伪目标(如 all, clean 等),伪目标不代表文件,只表示某些特定操作。关于目标,下文有详细介绍。

  • dependencies(依赖项):这些是目标所依赖的文件或其他目标。make 会根据这些依赖项的时间戳来判断目标是否需要重新生成。如果任何依赖项的时间戳比目标的时间戳更新,make 就会执行该规则的命令来更新目标。

  • command(命令):这是一个或多个命令,通常是构建目标所需执行的操作。例如,编译源文件、复制文件或删除文件。命令通常以 TAB(制表符)开头。

05-目标(target)的概念和介绍

Makefile 中,目标(target) 是最重要的结构之一。目标是 Makefile 中用于执行操作的核心单元,它表示一个文件(通常是最终生成的文件),或者一个操作的步骤。在 make 运行时,它会根据目标的依赖关系来确定哪些目标需要更新或执行。

1. 目标的基本结构

目标是Makefile 中的每个规则中的第一个部分。

Makefile 中的每个规则通常包含以下部分:

target: dependencies
    command
  • target(目标):这是规则中的第一个部分,表示你希望 make 构建或更新的文件或动作。它可以是一个文件,也可以是一个伪目标(如 all, clean 等),伪目标不代表文件,只表示某些特定操作。

  • dependencies(依赖项):这些是目标所依赖的文件或其他目标。make 会根据这些依赖项的时间戳来判断目标是否需要重新生成。如果任何依赖项的时间戳比目标的时间戳更新,make 就会执行该规则的命令来更新目标。

  • command(命令):这是一个或多个命令,通常是构建目标所需执行的操作。例如,编译源文件、复制文件或删除文件。命令通常以 TAB(制表符)开头。

2. 目标的类型

Makefile 中,目标主要有两种类型:

(1) 文件目标

文件目标是 Makefile 中最常见的目标类型。它表示一个文件,make 会检查该文件的依赖项,决定是否需要重新生成这个文件。例如:

program: main.o utils.o
    gcc -o program main.o utils.o
  • program 是目标,表示最终生成的可执行文件。
  • main.outils.o 是该目标的依赖项,表示目标 program 依赖这两个目标的更新。
  • 命令 gcc -o program main.o utils.o 用于生成 program

在这个例子中,make 会检查 main.outils.o 的修改时间。如果它们比 program 新,make 就会执行命令来重新生成 program

(2) 伪目标

伪目标是没有实际文件对应的目标。它们用于执行特定的操作或命令,通常用来管理 Makefile 的构建过程。例如:

.PHONY: clean

clean:
    rm -f *.o program
  • .PHONY 是一个特殊的伪目标,告诉 make 该目标不对应于文件,而是一个纯粹的操作。
  • clean 目标用于清理构建过程中生成的文件,make clean 会执行命令 rm -f *.o program 删除所有 .o 文件和 program 可执行文件。
伪目标的必要性

如果你没有告诉make某个目标是伪目标,比如上面的clean目标,它就会认为clean目标是一个文件目标,此时如果当前目录中存在一个名叫clean的文件,那么make就会认为这个文件视为已经是“最新的”,从而跳过重新构建,也就是跳过对clean目标中command(命令)的执行。而如果你告诉了make,clean是一个伪目标,那它就知道这条目标与目录中的文件无关,从而不管怎么样,都可以执行目标中的command(命令)。
再说具体点,如果你没有声音clean是一个伪目标,你本想执行make clean命令来清除所有 .o 文件和 program 可执行文件,结果却因为目录中有一个名叫clean的文件而导致clean目标中的命令被跳过执行,从而达到自己想要的结果。

常见的伪目标
  • all:通常用于表示默认的构建目标。它可能依赖于多个其他目标,通常作为 make 执行时的默认目标。
  • clean:用于删除构建过程中生成的中间文件(例如 .o 文件)或输出文件。
  • install:通常用于安装程序或文件到特定位置。
  • test:用于执行测试。
伪目标的常见定义和声明形式
PHONY := __build
......
......
......
.PHONY : $(PHONY)

变量PHONY用于存储Makefile中的所有伪目标名,但是直到文Makefile的最后一行才对存储在变量PHONY中的所有伪目标名进行声明,但是中间的语句会用到这些伪目标名,这就充分证明了Makefile文件并不是顺序执行,而是先解析再执行。

3. 目标的依赖关系

目标可以有多个依赖项,make 会根据这些依赖项的状态来判断目标是否需要更新。如果目标的依赖项发生变化(例如被修改或重新生成),则目标会被重新构建。例如:

all: program

program: main.o utils.o
    gcc -o program main.o utils.o

main.o: main.c
    gcc -c main.c

utils.o: utils.c
    gcc -c utils.c
  • all 是伪目标,依赖 program 目标。
  • program 依赖 main.outils.o,这些是编译的中间目标。
  • main.outils.o 分别依赖于它们各自的源文件(main.cutils.c)。

make 会根据文件的时间戳来决定是否需要执行命令。如果 main.cutils.cmain.outils.o 更新,make 会重新编译源文件,生成相应的目标。

4. 如果make命令没有指定目标,那么执行哪一个目标?

如果make命令没有指定目标,那么会默认执行 Makefile 中的第一个目标。如果第一个目标在后面有同名定义,那么会后面的会覆盖前面的目标,但是第一个目标仍然是这个同名目标,只是执行的是这个同名目标后面的定义。

比如下面这些Makefile语句:

__build:

all:start_recursive_build $(TARGET)

__build:$(subdir-y) built-in.o

第一个目标是__build,所以make没有指定目标时会执行目标__build,但由于__build在后面还有定义,后面的覆盖了之前的,所以真正执行的是后面的__build目标,即:

__build:$(subdir-y) built-in.o

在这个过程中,all目标并不会执行。

6. 同名目标是怎么处理?到底执行哪一个?

在 Makefile 中,同一个目标名可以出现多次,此时make 会覆盖先前定义的目标,并只执行最后一个规则。

例如,假设你有以下 Makefile

target:
    echo "This is the first rule"

target:
    echo "This is the second rule"

make 会执行第二个 target 规则(打印 “This is the second rul”),并忽略第一个定义的 target 规则。

又例如:

__build:
    # 空规则
__build: touch example

make 会最终使用最后的 __build 目标定义。
关于上面命令中的touch的介绍见博文 Makefile中遇到的touch命令是怎么回事儿?

7. 目标的总结

  • 目标是 Makefile 的基本构建单元,用于表示你想要构建或更新的内容。
  • 目标可以是文件或伪目标,伪目标通常用于执行某些特定的操作(如清理、安装等)。
  • 目标的依赖项决定了它是否需要更新,make 会根据依赖关系自动更新目标。
  • make 是基于时间戳来判断目标是否过时,如果目标的依赖项比目标本身更新,make 会重新执行目标的命令。

通过合理使用目标、依赖关系和命令,可以高效地管理和自动化项目的构建过程。

06-几个比较重要的约定俗成的变量

第01个:obj-y

关于这个变量的介绍,详见我的另一篇博文:
https://blog.youkuaiyun.com/wenhao_ir/article/details/144532544【搜索“obj-y”】

第02个:obj-m

关于这个变量的介绍,详见我的另一篇博文:
https://blog.youkuaiyun.com/wenhao_ir/article/details/144958830

第03个: obj-$(CONFIG_TOUCHSCREEN_GT9XX)

来源:https://blog.youkuaiyun.com/wenhao_ir/article/details/145883835

这条 Makefile 语句的意思是在编译内核模块时,依据 CONFIG_TOUCHSCREEN_GT9XX 配置选项,动态地决定是否编译 gt9xx/ 目录中的代码。

解释:

  • obj-$(CONFIG_TOUCHSCREEN_GT9XX): 这是一个条件表达式。CONFIG_TOUCHSCREEN_GT9XX 是一个内核配置项,它通常在内核配置(如 .config 文件)中定义。如果这个配置项被启用(即 CONFIG_TOUCHSCREEN_GT9XX 的值为 ym),那么 obj-$(CONFIG_TOUCHSCREEN_GT9XX) 就会被展开成 obj-yobj-m,从而指示需要编译相应的代码。

  • += gt9xx/: 这意味着将 gt9xx/ 目录添加到需要编译的目标中。具体来说,如果 CONFIG_TOUCHSCREEN_GT9XX 被启用,gt9xx/ 目录下的代码将被包含进最终的编译目标中。

具体工作流程:

  1. 如果在 .config 配置文件中,CONFIG_TOUCHSCREEN_GT9XX 被设置为 y(表示编译为内核的一部分)或者 m(表示编译为模块),那么 obj-$(CONFIG_TOUCHSCREEN_GT9XX) 就会展开成 obj-yobj-m
  2. += gt9xx/ 会将 gt9xx/ 目录中的源文件添加到编译列表中。
  3. 这意味着 gt9xx/ 目录下的代码会被编译进内核或者被编译成内核模块,具体取决于 CONFIG_TOUCHSCREEN_GT9XX 配置项的值。

小结:
如果你在内核配置中启用了 CONFIG_TOUCHSCREEN_GT9XX,那么这条 Makefile 语句会确保 gt9xx/ 目录下的代码被编译。如果没有启用这个配置选项,这部分代码就不会被编译。

07-几个重要的自动化变量:$@$<$^

Makefile 中,$@$<$^自动变量,用于规则中命令部分,提供构建目标和依赖项的相关信息。这些自动变量可以让 Makefile 更加简洁、动态。以下是它们的详细介绍和用法。


1. $@ - 当前目标的名称

  • 含义$@ 表示当前规则中目标的完整名称
  • 用途:当需要在命令中引用目标文件时,可以使用 $@
示例
program: main.o utils.o
    gcc -o $@ main.o utils.o
  • 解释
    • 目标是 program
    • $@ 的值是 program
    • 最终命令等价于:gcc -o program main.o utils.o

2. $< - 第一个依赖项的名称

  • 含义$< 表示规则中第一个依赖项的名称
  • 用途:通常用于隐式规则或显式规则中,引用第一个输入文件。
示例
%.o: %.c
    gcc -c $< -o $@
  • 解释
    • 这是一个模式规则,表示所有的 .o 文件都依赖于对应的 .c 文件。
    • $< 的值是第一个依赖项(例如,file.c)。
    • $@ 的值是目标文件(例如,file.o)。
    • 最终命令等价于:gcc -c file.c -o file.o

3. $^ - 所有依赖项的列表

  • 含义$^ 表示规则中所有依赖项的完整列表,并且会去重。
  • 用途:当需要在命令中引用所有依赖项时,可以使用 $^
示例
program: main.o utils.o
    gcc -o $@ $^
  • 解释
    • 目标是 program
    • $^ 的值是 main.o utils.o(所有依赖项)。
    • 最终命令等价于:gcc -o program main.o utils.o

4. 对比总结

自动变量含义典型用途
$@当前规则的目标文件名称引用生成的目标文件
$<当前规则的第一个依赖项的名称引用第一个输入文件,通常用于编译
$^当前规则的所有依赖项的名称列表引用所有依赖项,通常用于链接

5. 综合示例

以下是一个完整的 Makefile 示例,展示 $@$<$^ 的结合使用:

program: main.o utils.o
    gcc -o $@ $^

main.o: main.c
    gcc -c $< -o $@

utils.o: utils.c
    gcc -c $< -o $@

clean:
    rm -f program main.o utils.o
解释
  1. program 规则

    • 目标是 program
    • $@program
    • $^main.o utils.o
    • 命令:gcc -o program main.o utils.o
  2. main.outils.o 规则

    • $@ 是目标文件,例如 main.outils.o
    • $< 是第一个依赖项,例如 main.cutils.c
    • 命令:
      • gcc -c main.c -o main.o
      • gcc -c utils.c -o utils.o
  3. clean 规则

    • 没有依赖项。
    • 删除生成的目标文件和中间文件。

6. 自动变量常见用途

场景使用的自动变量示例命令
生成目标文件$@gcc -o $@ $^
使用第一个依赖$<gcc -c $< -o $@
使用所有依赖$^ar rcs $@ $^

7. 注意事项

  1. 依赖项列表去重$^ 会去除重复的依赖项。
  2. 隐式规则中常用 $<$@:例如 .o 文件生成规则,通常是基于 .c 文件的。
  3. 手动管理依赖项时需注意顺序:依赖项的顺序可能会影响构建的结果。

通过灵活运用这些自动变量,可以显著减少重复代码,让 Makefile 更加简洁高效!

08-include语句

假设文件Makefile.build中有下面这句话:

include Makefile

Makefile中的include语句是把被包含的文件的内容包含当前文件中,类似于C语言中的include关键词。
但是使用时要特别注意避免陷入无限循环的死循环中。
详情见我的另一篇博文
Makefile中使用include语句时要特别注意避免陷入无限循环的死循环中

98-实际示例

详见下面这篇博文
https://blog.youkuaiyun.com/wenhao_ir/article/details/145547005

99-一些不是很重要的知识

ZZ-01-注释符

注释符是#,与Python相同。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昊虹AI笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值