Makefile教程1 快速入门
1 快速入门
1.1 为什么存在 Makefile?
Makefile用于帮助决定大型程序的哪些部分需要重新编译。在绝大多数情况下,都会编译C或C++文件。 其他语言通常有自己的工具,其用途与Make类似。当您需要根据已更改的文件运行一系列指令时,Make也可以在编译之外使用。 本教程将重点介绍C/C++编译。
下面是您可以使用Make构建的示例依赖关系图。如果任何文件的依赖项发生更改,则该文件将被重新编译:
1.2 Make有哪些替代?
流行的C/C++替代构建系统有SCons、CMake、Bazel 和 Ninja。 一些代码编辑器(例如 Microsoft Visual Studio)有自己的内置构建工具。 对于Java,有Ant、Maven和Gradle。 其他语言(例如 Go、Rust 和 TypeScript)都有自己的构建工具。
Python、Ruby 和Javascript等解释性语言不需要与Makefile之类的东西。 Makefile的目标是根据已更改的文件来编译需要编译的任何文件。 解释语言中的文件发生更改时,不需要重新编译任何内容。 程序运行时,将使用该文件的最新版本。
1.3 Make的版本和类型
Make有多种实现,但本指南的大部分内容都适用于您使用的任何版本。 然而,它是专门为GNU Make编写的,GNU Make是Linux和MacOS上的标准实现。 所有示例都适用于Make版本3和4,除了一些个别差异之外,它们几乎相同。
1.4 运行示例
要运行这些示例,您需要一个终端并安装“make”。 对于每个示例,将内容放入名为Makefile的文件中,然后在该目录中运行命令make。 让我们从最简单的Makefile开始:
hello:
echo "Hello, World"
注意:Makefile必须使用TAB缩进,不能使用空格,否则make将失败。
以下是运行上述示例的输出:
$ make
echo "Hello, World"
Hello, World
1.5 Makefile语法
生成文件语法
Makefile 由一组规则组成。 规则通常如下所示:
targets: prerequisites
command
command
command
- targets是文件名,以空格分隔。 通常每条规则只有一个。
- 这些command是通常用于创建目标的一系列步骤。
- prerequisites 也是文件名,以空格分隔。 在运行target的命令之前,这些文件需要存在。 这些也称为依赖项
1.6 Make的本质
让我们从hello world示例开始:
hello:
echo "Hello, World"
echo "This line will print if the file hello does not exist."
- 我们有一个名为 hello 的目标
- 该目标有两个命令
- 该目标没有先决条件
然后我们将运行 make hello。 只要hello文件不存在,命令就会运行。 如果hello存在,则不会运行任何命令。
让我们创建更典型的Makefile:编译单个C文件。
blah.c
int main() { return 0; }
然后创建 Makefile(一如既往地称为 Makefile):
blah:
cc blah.c -o blah
运行make,由于没有将目标作为参数提供给make命令,因此将运行第一个目标。 在这种情况下,只有一个目标(blah)。 第一次运行它时,将会创建blah。 第二次,你会看到“make: 'blah' is up to date”。 那是因为blah文件已经存在。 但有一个问题:如果我们修改blah.c 然后运行make,则不会重新编译任何内容。
我们通过添加先决条件来解决这个问题:
blah: blah.c
cc blah.c -o blah
当我们再次运行make 时,会发生以下步骤:
- 选择第一个目标,因为第一个目标是默认目标
- 这有blah.c的先决条件
- Make决定是否应该运行blah目标。 仅当blah不存在或blah.c比 blah新时才会运行
最后一步很关键,也是make的精髓。它试图做的是确定自上次编译blah以来blah的先决条件是否发生了变化。也就是说,如果blah.c被修改,运行make应该重新编译该文件。 相反,如果blah.c没有更改,则不应重新编译它。
为了实现这一点,它使用文件系统时间戳来确定是否发生了更改。 这是一个合理的启发式方法,因为文件时间戳通常仅在文件被修改时才会更改。 但情况并非总是如此。 例如,您可以修改文件,然后将该文件的修改时间戳更改为旧的时间戳。 如果这样做,Make会错误地猜测该文件没有更改,因此可以被忽略。
唷,真是拗口啊。 确保您理解这一点。 这是 Makefile 的关键,您可能需要几分钟才能正确理解。 如果事情仍然令人困惑,请尝试上面的示例或观看上面的视频。
参考资料
- 软件测试精品书籍文档下载持续更新 https://github.com/china-testing/python-testing-examples 请点赞,谢谢!
- 本文涉及的python测试开发库 谢谢点赞! https://github.com/china-testing/python_cn_resouce
- python精品书籍下载 https://github.com/china-testing/python_cn_resouce/blob/main/python_good_books.md
- Linux精品书籍下载 https://www.cnblogs.com/testing-/p/17438558.html
- https://makefiletutorial.com/
1.6 更多示例
以下Makefile最终运行所有三个目标。 当您在终端中运行make时,它将通过一系列步骤构建名为blah的程序:
- Make选择目标blah,因为第一个目标是默认目标
- blah需要blah.o,因此搜索blah.o目标
- blah.o需要blah.c,因此搜索blah.c目标
- blah.c 没有依赖项,因此运行echo命令
- 然后运行 cc -c 命令,因为所有blah.o依赖项都已完成
- 运行顶部cc命令,因为所有blah依赖都完成了
- 最终:blah是已编译的c程序
blah: blah.o
cc blah.o -o blah # Runs third
blah.o: blah.c
cc -c blah.c -o blah.o # Runs second
# Typically blah.c would already exist, but I want to limit any additional required files
blah.c:
echo "int main() { return 0; }" > blah.c # Runs first
如果删除blah.c,所有三个目标都将重新运行。如果您运行touch blah.o (从而将时间戳更改为比 blah 更新),则只有第一个目标会运行。如果您不进行任何更改,则所有目标都不会运行。
下一个示例没有做任何新内容,但仍然很好的补充示例。它将始终运行两个目标,因为some_file依赖于other_file,而other_file从未创建。
some_file: other_file
echo "This will always run, and runs second"
touch some_file
other_file:
echo "This will always run, and runs first"
1.7 Make clean
clean经常被用作删除其他目标输出的目标,你可以运行make和make clean来创建和删除some_file。
clean在这里做了两件新事情:
- 它不是第一目标(默认),也不是先决条件。这意味着除非你明确调用 make clean,否则它永远不会运行。
- 它不是一个文件名。如果你碰巧有一个名为clean的文件,这个目标就不会运行,这不是我们想要的。
some_file:
touch some_file
clean:
rm -f some_file
1.8 变量
变量只能是字符串。通常要使用 :=,但 = 也可以。
下面是一个使用变量的示例:
files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file
file1:
touch file1
file2:
touch file2
clean:
rm -f file1 file2 some_file
单引号或双引号对Make没有任何意义。它们只是分配给变量的字符。不过,引号对shell/bash很有用,在printf等命令中需要用到它们。在本例中,两个命令的行为是一样的:
a := one two # a is set to the string "one two"
b := 'one two' # Not recommended. b is set to the string "'one two'"
all:
printf '$a'
printf $b
使用 ${} 或 $() 引用变量
x := dude
all:
echo $(x)
echo ${x}
# Bad practice, but works
echo $x
2 Target
2.1 all target
如果调用 make 而未指定目标,它将默认运行。
all: one two three
one:
touch one
two:
touch two
three:
touch three
clean:
rm -f one two three
2.2 多target
当一条规则有多个目标时,将针对每个目标运行命令。$@ 是一个自动变量,包含目标名称。
all: f1.o f2.o
f1.o f2.o:
echo $@
# Equivalent to:
# f1.o:
# echo f1.o
# f2.o:
# echo f2.o
3 自动变量和通配符
3.1 * 通配符
在 Make 中,* 和 % 都被称为通配符,但它们的含义完全不同。* 在文件系统中搜索匹配的文件名。我建议你始终将其封装在通配符函数中,否则你可能会陷入下面描述的一个常见陷阱。
# Print out file information about every .c file
print: $(wildcard *.c)
ls -la $?
*可以在目标文件、先决条件或通配符函数中使用。
不要在变量定义中直接使用 *。当 * 不能匹配任何文件时,将保持原样(除非在通配符函数中运行)
thing_wrong := *.o # Don't do this! '*' will not get expanded
thing_right := $(wildcard *.o)
all: one two three four
# Fails, because $(thing_wrong) is the string "*.o"
one: $(thing_wrong)
# Stays as *.o if there are no files that match this pattern :(
two: *.o
# Works as you would expect! In this case, it does nothing.
three: $(thing_right)
# Same as rule three
four: $(wildcard *.o)
3.2 %通配符
%非常有用,但由于其使用场合多种多样,所以有点令人困惑。
- 在匹配模式下使用时,它会匹配字符串中的一个或多个字符。这种匹配称为词干。
- 在替换模式下使用时,它会将匹配到的词干替换到字符串中。
- % 通常用于规则定义和某些特定函数中。
3.3 自动变量
自动变量有很多,但通常只显示几个:
hey: one two
# Outputs "hey", since this is the target name
echo $@
# Outputs all prerequisites newer than the target
echo $?
# Outputs all prerequisites
echo $^
touch hey
one:
touch one
two:
touch two
clean:
rm -f hey one two
CC = gcc # Flag for implicit rules
CFLAGS = -g # Flag for implicit rules. Turn on debug info
# Implicit rule #1: blah is built via the C linker implicit rule
# Implicit rule #2: blah.o is built via the C compilation implicit rule, because blah.c exists
blah: blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
clean:
rm -f blah*
4.2 静态模式规则
静态模式规则是在Makefile中少写代码的另一种方法,但我认为它更有用,而且不那么"神奇"。下面是它们的语法:
targets...: target-pattern: prereq-patterns ...
commands
其本质是,给定的目标与 target-pattern(通过%通配符)匹配。匹配到的内容称为词干。然后将词干代入先决条件模式,生成目标的先决条件。
一个典型的用例是将 .c 文件编译成 .o 文件。下面是手动方法:
objects = foo.o bar.o all.o
all: $(objects)
# These files compile via implicit rules
foo.o: foo.c
bar.o: bar.c
all.o: all.c
all.c:
echo "int main() { return 0; }" > all.c
%.c:
touch $@
clean:
rm -f *.c *.o all
下面是使用静态模式规则的更有效方法:
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
# Note: PHONY is important here. Without it, implicit rules will try to build the executable "all", since the prereqs are ".o" files.
.PHONY: all
# Ex 1: .o files depend on .c files. Though we don't actually make the .o file.
$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $<"
# Ex 2: .result files depend on .raw files. Though we don't actually make the .result file.
$(filter %.result,$(obj_files)): %.result: %.raw
echo "target: $@ prereq: $<"
%.c %.raw:
touch $@
clean:
rm -f $(src_files)
4.3 静态模式规则和过滤器
过滤器函数可用于静态模式规则,以匹配正确的文件。在本例中,我创建了 .raw 和 .result 扩展名。
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
# Note: PHONY is important here. Without it, implicit rules will try to build the executable "all", since the prereqs are ".o" files.
.PHONY: all
# Ex 1: .o files depend on .c files. Though we don't actually make the .o file.
$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $<"
# Ex 2: .result files depend on .raw files. Though we don't actually make the .result file.
$(filter %.result,$(obj_files)): %.result: %.raw
echo "target: $@ prereq: $<"
%.c %.raw:
touch $@
clean:
rm -f $(src_files)
4.4 模式规则
模式规则经常被使用,但很容易混淆。你可以从两个方面来看待它们:
- 定义自己的隐式规则
- 更简单的静态模式规则
我们先来看一个例子:
# Define a pattern rule that compiles every .c file into a .o file
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
模式规则的目标中包含一个"%"。这个"%"匹配任何非空字符串,而其他字符则自行匹配。模式规则先决条件中的"%"代表与目标中的"%"匹配的同一词干。
下面是另一个例子:
# Define a pattern rule that has no pattern in the prerequisites.
# This just creates empty .c files when needed.
%.c:
touch $@
4.5 双冒号规则
双冒号规则很少使用,但可以为同一目标定义多个规则。如果这些规则是单冒号,则会打印警告,并且只会运行第二组命令。
all: blah
blah::
echo "hello"
blah::
echo "hello again"
5 命令和执行
5.1 命令回显/静默
在命令前添加 @ 以阻止命令被打印
make -s相当于在每一行前添加 @ 号
all:
@echo "This make line will not be printed"
echo "But this will"
5.2命令执行
每条命令都在一个新的 shell 中运行(至少效果是这样的)
all:
cd ..
# The cd above does not affect this line, because each command is effectively run in a new shell
echo `pwd`
# This cd command affects the next because they are on the same line
cd ..;echo `pwd`
# Same as above
cd ..; \
echo `pwd`
5.3默认shell
默认shell 是/bin/sh。你可以通过修改变量SHELL来改变它:
SHELL=/bin/bash
cool:
echo "Hello from bash"
5.4双美元符号
如果想让字符串带有美元符号,可以使用 $$。这就是如何在 bash 或 sh 中使用 shell 变量。
请注意下一个示例中 Makefile 变量与 Shell 变量的区别。
make_var = I am a make variable
all:
# Same as running "sh_var='I am a shell variable'; echo $sh_var" in the shell
sh_var='I am a shell variable'; echo $$sh_var
# Same as running "echo I am a make variable" in the shell
echo $(make_var)
5.5 使用 -k、-i 和 - 处理错误
运行 make 时添加 -k,即使出现错误也能继续运行。如果你想一次性看到 Make 的所有错误,这很有用。
在命令前添加 - 来抑制错误
在 make 中添加 -i 可让每条命令都出现这种情况。
one:
# This error will be printed but ignored, and make will continue to run
-false
touch one
5.6中断或杀死 make
注意:如果你按住 ctrl+c make,它会删除刚刚生成的新目标。
5.7递归使用 make
要递归调用 makefile,请使用特殊的 $(MAKE) 代替 make,因为它会为你传递 make 标志,而自身不会受其影响。
new_contents = "hello:\n\ttouch inside_file"
all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
cd subdir && $(MAKE)
clean:
rm -rf subdir
5.7导出、环境和递归 make
当 Make 启动时,它会自动从执行时设置的所有环境变量中创建 Make 变量。
# Run this with "export shell_env_var='I am an environment variable'; make"
all:
# Print out the Shell variable
echo $$shell_env_var
# Print out the Make variable
echo $(shell_env_var)
export 指令将一个变量设置为所有配方中所有 shell 命令的环境:
shell_env_var=Shell env var, created inside of Make
export shell_env_var
all:
echo $(shell_env_var)
echo $$shell_env_var
因此,当你在 make 中运行 make 命令时,可以使用 export 指令使子 make 命令可以访问该变量。在本例中,cooly 被导出,以便子目录中的 makefile 可以使用它。
new_contents = "hello:\n\techo \$$(cooly)"
all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
@echo "---MAKEFILE CONTENTS---"
@cd subdir && cat makefile
@echo "---END MAKEFILE CONTENTS---"
cd subdir && $(MAKE)
# Note that variables and exports. They are set/affected globally.
cooly = "The subdirectory can see me!"
export cooly
# This would nullify the line above: unexport cooly
clean:
rm -rf subdir
你需要导出变量,使它们也能在 shell 中运行。
one=this will only work locally
export two=we can run subcommands with this
all:
@echo $(one)
@echo $$one
@echo $(two)
@echo $$two
.EXPORT_ALL_VARIABLES 会为你导出所有变量。
.EXPORT_ALL_VARIABLES:
new_contents = "hello:\n\techo \$$(cooly)"
cooly = "The subdirectory can see me!"
# This would nullify the line above: unexport cooly
all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
@echo "---MAKEFILE CONTENTS---"
@cd subdir && cat makefile
@echo "---END MAKEFILE CONTENTS---"
cd subdir && $(MAKE)
clean:
rm -rf subdir
5.8 make的参数
make 有一系列可以运行的选项。查看--dry-run、--touch、--old-file。
你可以为 make 设置多个目标,例如,make clean run test 运行 clean 目标,然后运行,最后测试。
变量 Pt.
变量的种类和修改
变量有两种类型
递归(使用 =)--只在使用命令时查找变量,而不是在定义变量时。
简单扩展(使用 :=)--就像普通的命令式编程--只有那些目前已定义的变量才会被扩展
简单扩展(使用 :=)允许你追加变量。递归定义将导致无限循环错误。
只有在变量尚未被设置的情况下,......= 才会设置变量
行尾的空格不会被删除,但行首的空格会被删除。要使用单空格创建变量,请使用 $(nullstring)
未定义的变量实际上是一个空字符串!
使用 += 添加
字符串替换也是修改变量的一种常用且有用的方法。还可以查看文本函数和文件名函数。
命令行参数和覆盖
使用 override 可以覆盖命令行变量。在这里,我们使用 make option_one=hi 运行 make
命令列表和 define
define 指令并不是函数,尽管它看起来像函数。我见过它的使用频率很低,所以就不细说了,但它主要用于定义罐装配方,也可以与 eval 函数很好地搭配使用。
define/endef 简单地创建一个变量,并将其设置为一系列命令。请注意,这与在命令之间使用分号有点不同,因为每个命令都会在单独的 shell 中运行。
特定目标变量
可以为特定目标设置变量
特定模式变量
可以为特定目标模式设置变量
Makefile 的条件部分
条件 if/else
检查变量是否为空
检查变量是否已定义
ifdef 不会扩展变量引用;它只是查看是否定义了某个变量
$(MAKEFLAGS)
本例向您展示了如何使用 findstring 和 MAKEFLAGS 测试 make 标志。使用 make -i 运行此示例,可以看到它打印出 echo 语句。
函数
第一个函数
函数主要用于文本处理。使用 $(fn, arguments) 或 ${fn, arguments} 调用函数。Make 有大量内置函数。
如果要替换空格或逗号,请使用变量
不要在第一个参数之后的参数中包含空格。这将被视为字符串的一部分。