以vitis的makefile为例学习

好的,完全没问题!我们彻底回归本源,从“第一性原理”出发来理解 Makefile,然后再将这些原理应用到你提供的那个优雅的 Vitis Makefile 上。

第一部分:Makefile 的第一性原理

想象一下,你不是在写代码,而是在厨房里烤蛋糕。

  • 目标 (Target):你最终想要的东西,比如一个“巧克力蛋糕”。
  • 依赖 (Prerequisites):为了做出蛋糕,你先需要准备好的东西,比如“面粉”、“鸡蛋”、“巧克力酱”。
  • 指令 (Command):你具体要执行的动作,比如“把面粉和鸡蛋混合”、“加入巧克力酱并搅拌”、“放入烤箱烤30分钟”。

Makefile 的核心思想就是描述这种关系:

要想得到【目标】,必须先确保【依赖】都已就绪,然后执行【指令】。

make 这个工具最聪明的地方在于,它会检查文件的时间戳。如果“巧克力蛋糕”比“巧克力酱”更新,说明你已经用最新的酱做过蛋糕了,make 就什么也不做,以节省时间。但如果你刚买了一瓶新的“巧克力酱”(文件时间戳更新了),make 就知道必须重新烤一个蛋糕。

1. 基本语法:规则 (Rule)

Makefile 的所有内容都由一条条的“规则”构成。

1
2
3
4
目标: 依赖1 依赖2 ...
	<Tab>指令1
	<Tab>指令2
	...
  • 目标 (target): 通常是一个文件名,是你希望生成的文件。
  • :: 分隔符。
  • 依赖 (prerequisites): 其他文件名,目标文件依赖于它们。
  • 指令 (command): 必须以一个 Tab 字符开头!这是最常见的初学者错误。这些是 Shell 命令,比如 gcc, cp, rm

最简单的例子:

1
2
3
4
5
# hello.c 是源代码
# hello 是编译后的可执行文件

hello: hello.c
	<Tab>gcc hello.c -o hello

当你输入 make 时,它会:

  1. 找到默认目标 hello
  2. 检查它的依赖 hello.c
  3. 比较 hellohello.c 的修改时间。
    • 如果 hello 文件不存在,或者 hello.chello 更新,那么…
  4. 执行指令 gcc hello.c -o hello 来重新生成 hello

2. 变量 (Variables)

为了不重复写同样的东西,我们使用变量。

1
2
3
4
5
6
7
# 定义变量
COMPILER = gcc
FLAGS = -Wall -g

# 使用变量 $(VARIABLE_NAME)
hello: hello.c
	$(COMPILER) $(FLAGS) hello.c -o hello

这样做的好处是,如果想换编译器(比如 clang)或者修改编译选项,只需要改一行变量定义即可。


第二部分:让 Makefile 变得“优雅”的进阶语法

如果项目里有几十个 .c 文件,为每个文件都写一条规则太痛苦了。所以 make 提供了一些强大的工具来自动化这个过程。

1. 自动化变量 (Automatic Variables)

在规则的指令部分,你可以使用一些特殊的变量,它们会自动被 make 替换:

  • $@: 代表目标的名字。 (The **@**t symbol, the target)
  • $<: 代表第一个依赖的名字。 (The < left-arrow, input)
  • $^: 代表所有依赖的名字,用空格隔开。

现在,我们的规则可以写得更通用:

1
2
hello: hello.c
	$(COMPILER) $(FLAGS) $< -o $@

$< 会被替换为 hello.c$@ 会被替换为 hello。效果和之前完全一样,但更具可读性和通用性。

2. 模式规则 (Pattern Rules)

这是 Makefile 的精华所在!它允许你定义一个“模板”规则来处理一整类的文件。% 是一个通配符。

1
2
3
# 这个规则告诉 make 如何从任意一个 .c 文件生成一个对应的 .o 文件
%.o: %.c
	$(COMPILER) $(FLAGS) -c $< -o $@
  • %.o: 匹配任何以 .o 结尾的目标,比如 xspi.o, main.o
  • %.c: 匹配与目标同名的 .c 依赖,比如 xspi.c, main.c
  • 指令 $(COMPILER) $(FLAGS) -c $< -o $@ 现在可以处理任何 xxx.c -> xxx.o 的编译!

3. 函数 (Functions)

Makefile 提供了类似编程语言的函数来处理文本。你提供的 Makefile 中用到了几个关键函数:

  • $(wildcard PATTERN): 查找并返回匹配 PATTERN 的所有文件名。
    • SRCFILES := $(wildcard *.c) 会找到当前目录下所有 .c 文件,并把它们的列表赋值给 SRCFILES 变量。
  • $(basename NAMES): 去掉文件名中的后缀。
    • $(basename xspi.c xspi_g.c) 会返回 xspi xspi_g
  • $(addsuffix SUFFIX, NAMES): 给列表中的每个名字添加后缀。
  • $(addprefix PREFIX, NAMES): 给列表中的每个名字添加前缀。

第三部分:用第一性原理解读你的 Vitis Makefile

现在,我们用刚刚学到的知识来逐段分析这个 Makefile。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# --------------------
# 1. 变量定义区 (Principles: Variables)
# --------------------
# 这些变量被故意留空,等待外部传入。
# 这就像一个函数有多个参数,等待调用者提供具体的值。
COMPILER=
COMPILER_FLAGS=
EXTRA_COMPILER_FLAGS=

# 定义输出目录和头文件目录
RELEASEDIR=../../../lib/
INCLUDEDIR=../../../include/

# 定义 include 路径
INCLUDES=-I./. -I$(INCLUDEDIR)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# --------------------
# 2. 自动化文件处理 (Principles: Functions, Variables)
# --------------------
# 自动查找当前目录下所有 .c 文件
SRCFILES:=$(wildcard *.c)

# 这是一行优雅的核心代码,我们来分解它:
# 假设 SRCFILES 是 "xspi.c xspi_options.c"
# 1. $(basename $(SRCFILES))       -> "xspi xspi_options"
# 2. $(addsuffix .o, ...)          -> "xspi.o xspi_options.o"
# 3. $(addprefix $(RELEASEDIR), ...) -> "../../../lib/xspi.o ../../../lib/xspi_options.o"
OBJECTS = $(addprefix $(RELEASEDIR), $(addsuffix .o, $(basename $(wildcard *.c))))

这一行代码就自动计算出了所有需要生成的目标文件(.o 文件)以及它们应该存放的位置。

1
2
3
4
5
6
# --------------------
# 3. 定义最终目标 (Principles: Rules)
# --------------------
# 定义了一个叫 libs 的目标,它依赖于所有我们刚刚计算出的 .o 文件。
# 当你运行 `make` 或 `make libs` 时,make 会确保所有 $(OBJECTS) 都被生成。
libs: $(OBJECTS)

这是一个“聚合目标”,它本身没有指令,它的作用就是触发所有依赖项的构建。

1
2
3
4
5
6
7
8
# --------------------
# 4. 定义核心编译规则 (Principles: Pattern Rules, Automatic Variables)
# --------------------
# 这条模式规则是整个 Makefile 的“引擎”。
# 它告诉 make 如何从一个 .c 文件(在当前目录)
# 生成一个 .o 文件(在 RELEASEDIR 目录)。
$(RELEASEDIR)%.o: %.c
	${COMPILER} $(CC_FLAGS) $(ECC_FLAGS) $(INCLUDES) $(DEPENDENCY_FLAGS) $< -o $@
  • $(RELEASEDIR)%.o: %.c: 模式。例如,为了生成目标 ../../../lib/xspi.omake 会自动寻找依赖 xspi.c
  • $<: 自动替换为依赖,即 xspi.c
  • $@: 自动替换为目标,即 ../../../lib/xspi.o
  • $(COMPILER) 等都是外部传入的变量,提供了极大的灵活性。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# --------------------
# 5. 定义头文件安装规则 (Principles: Rules, Pattern Rules)
# --------------------
# .PHONY: include  <-- 告诉make,"include" 不是一个文件名,而是一个动作的名称。
.PHONY: include
# 'make include' 这个动作,依赖于所有头文件被拷贝到目标目录。
include: $(addprefix $(INCLUDEDIR),$(wildcard *.h))

# 这是拷贝头文件的模式规则:
# 如何生成一个在 INCLUDEDIR 里的 .h 文件?从当前目录的同名 .h 文件拷贝。
$(INCLUDEDIR)%.h: %.h
	$(CP) $< $@

第四部分:如何具体使用这个 Makefile

这个 Makefile 被设计成一个“组件”,由一个更上层的系统来调用和配置。

  1. 配置变量: 你不会直接修改这个 Makefile。而是在调用 make 命令时,从命令行传入变量的值。
  2. 执行目标: 你告诉 make 你想要什么目标。

一个典型的使用场景会是这样:

在一个终端里,cd 到这个 src 目录下,然后执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 编译所有 .o 文件
make \
  COMPILER="arm-none-eabi-gcc" \
  COMPILER_FLAGS="-O2 -c" \
  DEPENDENCY_FLAGS="-MMD -MP" \
  libs

# 安装所有头文件到 include 目录
make \
  INCLUDEDIR=../../../include \
  include

# 清理所有生成的文件
make \
  RELEASEDIR=../../../lib/ \
  clean
  • COMPILER="arm-none-eabi-gcc": 为变量 COMPILER 赋值,指定了交叉编译器。
  • COMPILER_FLAGS="-O2 -c": 传入了编译选项(优化等级2,只编译不链接)。
  • libs: 告诉 make 我们想要的目标是 libsmake 就会自动找到 libs 规则,然后去构建它的所有依赖(即所有的 .o 文件)。

总结起来,这个 Makefile 的优雅之处在于:

  • 声明式: 你只描述了“什么依赖什么”以及“如何生成”,而不是一步步的命令。
  • 自动化: 自动发现源文件、自动计算目标文件、自动应用编译规则。
  • 可配置和可复用: 通过外部变量进行配置,使得这个 Makefile 本身可以被用在任何 C 语言库的编译中,无需修改一个字。
Built with Hugo
Theme Stack designed by Jimmy