器→工具, 工具软件

编译工具make与Makefile

钱魏Way · · 9 次浏览

make与Makefile简介

代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。make是最常用的构建工具,诞生于1977年,主要用于C语言的项目。但是实际上 ,任何只要某个文件有变化,就要重新构建的项目,都可以用make构建。

make是一个自动化构建工具,用于自动编译和链接程序。它主要用于维护大型程序的源代码。make通过读取名为“Makefile”或“makefile”的文件,来确定目标程序的依赖关系和生成规则。

Makefile是一种脚本文件,它描述了一个工程中的目标(通常是可执行文件或者库),以及如何从源代码生成这些目标。Makefile中定义了一组规则,每条规则说明了如何生成一个或多个目标。

一个简单的Makefile可能包含以下内容:

prog: main.o utility.o
    gcc -o prog main.o utility.o

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

utility.o: utility.c
    gcc -c utility.c

在这个例子中,prog是最终的目标,它依赖于main.o和utility.o。main.o和utility.o的生成规则也在Makefile中给出。当你在命令行运行make时,make会检查所有的依赖项是否是最新的,如果不是,就会执行相应的命令来生成最新的目标。

make和Makefile的主要优点是自动化和递增编译。当源代码的一部分发生变化时,make只会重新编译那些由这部分代码直接或间接依赖的目标,而不是重新编译整个项目。这大大加速了大型项目的编译速度。

流行的 C/C++ 替代构建系统是SConsCMakeBazelNinja。一些代码编辑器(如Microsoft Visual Studio)有自己的内置构建工具。

对于 Java,有AntMavenGradle。Go 和 Rust 等其他语言有自己的构建工具。Python、Ruby 和 Javascript 等解释型语言不需要类似于 Makefile。

Makefile的编译过程

当你执行make命令时,以下步骤将被执行:

  • make首先查找当前目录下的名为makefile或者Makefile的文件。
  • 然后,make开始解析这个文件,构建目标和依赖的关系图。
  • 如果没有指定目标,make将选择Makefile文件中的第一个目标作为默认目标。
  • make检查这个目标及其所有依赖项,如果有任何一个依赖项比目标新,或者目标不存在,make就会执行相应的命令来生成或更新目标。
  • 这个过程会递归进行,即对于每个依赖项,make也会检查它的依赖项,按照相同的规则执行命令。

例如,考虑以下的Makefile:

prog: main.o utility.o
    gcc -o prog main.o utility.o

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

utility.o: utility.c
    gcc -c utility.c

如果你执行make命令,make将执行以下步骤:

  • 检查prog的依赖项o和utility.o。
  • 如果o不存在,或者main.c比main.o新,执行gcc -c main.c命令生成main.o。
  • 如果o不存在,或者utility.c比utility.o新,执行gcc -c utility.c命令生成utility.o。
  • 如果o或utility.o是新生成的,或者任何一个比prog新,执行gcc -o prog main.o utility.o命令生成prog。

以上就是一个基本的make编译过程。需要注意的是,Makefile可以非常复杂,包含多个目标,条件编译,自动化测试等等。不过基本的编译过程还是遵循以上的规则。

默认的情况下,make 命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件。在这三个文件名中,最好使用“Makefile”这个文件名,因为,这个文件名第一个字符为大写,这样有一种显目的感觉。最好不要用“GNUmakefile”,这个文件是GNU 的make 识别的。有另外一些make只对全小写的“makefile”文件名敏感,但是基本上来说,大多数的make 都支持“makefile”和“Makefile”这两种默认文件名。 当然,你可以使用别的文件名来书写Makefile,比如:“Make.Linux”,如果要指定特定的Makefile,你可 以使用make 的“-f”和“–file”参数,如:make -f Make.Linux 或make –file Make.Linux。

关于Makefile编译的基本规则一般有以下几点:

  • 如果这个工程没有编译过,那么我们的所有C 文件都要编译并被链接。
  • 如果这个工程的某几个C 文件被修改,那么我们只编译被修改的C 文件,并链接目标程序。
  • 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C 文件,并链接目标程序。

make 命令参数

make命令的参数可以用来指定各种选项、目标等。以下是一些常用的make命令参数

目标:你可以在make命令后添加一个或多个目标,make将会构建这些目标。例:make clean all。

  • -f 或 –file:用于指定make应使用的Makefile。例:make -f MyMakefile。
  • -C 或 –directory:改变目录然后执行make。例:make -C src。
  • -j 或 –jobs:指定make可以同时运行的任务数量。这可以用来加速构建过程。例:make -j4。
  • -k 或 –keep-going:即使某些目标无法构建,make也会尽可能多地构建其他目标。
  • -n 或 –just-print:这个选项会让make只打印它会执行的命令,而不实际执行。
  • -s 或 –silent:这个选项会让make在执行时不打印命令。
  • -B 或 –always-make:无论目标的依赖是否更新,make都会尝试重新构建所有指定的目标。
  • -v 或 –version:打印make的版本信息。
  • -h 或 –help:显示make的帮助信息。

你可以在命令行中使用man make或make –help来查看所有可用的选项。

命令行参数和覆盖

make命令中,你可以通过命令行参数的形式来定义或者覆盖Makefile中的变量。

例如,让我们假设你有一个名为CC的变量在你的Makefile中被设置为gcc:

CC = gcc

你可以在命令行中使用make CC=clang来覆盖这个变量的值。在这个例子中,make命令将会使用clang而不是gcc。

需要注意的是,使用这种方式定义的变量将会覆盖Makefile中的所有设置,无论它们在Makefile中的位置。即使在Makefile中后面有改变这个变量的值,这个变量的值仍然是命令行参数中指定的值。

这提供了一种灵活的方式来改变make的行为,你可以在不改变Makefile的情况下使用不同的参数来运行make。例如,你可以使用make DEBUG=1来启用调试选项,或者使用make CC=clang来改变编译器。

Makefile的主要内容

Makefile重要包含五部分内容:显式规则、隐晦规则、变量定义、文件指示和注释。

  • 变量定义。在Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点你C 语言中的宏,当Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
  • 显式规则。显式规则说明了,如何生成一个或多的的目标文件。这是由Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
  • 隐晦规则。由于我们的make 有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make 所支持的。
  • 文件指示。其包括了三个部分,一个是在一个Makefile 中引用另一个Makefile,就像C 语言中的include 一样;另一个是指根据某些情况指定Makefile 中的有效部分,就像C 语言中的预编译#if 一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
  • 注释。Makefile 中只有行注释,和UNIX 的Shell脚本一样,其注释是用“#”字符,这个就像C/C++中的“//”一样。如果你要在你的Makefile 中使用“#”字符,可以用反斜框进行转义,如:“#”。

需要注意的是如果Makefile的命令与target不在同一行,需要以[Tab]键开始。

Makefile变量定义

在Makefile中,变量被用来存储和操作值,这可以简化Makefile,并使其更易于维护。你可以在Makefile的任何地方定义变量,并在规则、命令或其他变量中使用它们。

变量的定义和引用

变量是把一个名字和任意长的字符串关联起来。基本语法如下:

MY_VAR=A text string

使用${}或$()来引用变量。示例:

MY_VAR=file1.c file2.c

all:
    echo ${MY_VAR}
    echo $(MY_VAR)

变量的求值时机

recursive 和 simply expanded

  • =recursive 仅在使用命名时解析变量值。
  • :=simply expanded 在定义时立即解析变量值。

示例:

# 在下面的echo命令执行时再求值,输出 "later"
one = one ${later_variable}

# 简单的扩展变量,由于 later_variable 未定义,下面不会输出 "later"
two := two ${later_variable}

later_variable = later

all: 
    echo $(one)
    echo $(two)

递归定义变量将产生无限循环错误,示例:

one = hello
# 简单的扩展变量,若将 := 改为 = 则产生无限循环错误。
one := ${one} there

all: 
    echo $(one)

变量是否覆盖

?= 仅在尚未设置变量时设置变量

one = hello
one ?= will not be set
two ?= will be set

all: 
    echo $(one)
    echo $(two)

变量中的前后空格

行尾的空格不会被删除,但开头的空格会被删除。

with_spaces = hello   # with_spaces 变量是 hello 末尾有三个空格
after = $(with_spaces)there

nullstring =
space = $(nullstring) # 创建只有一个空格的变量

all: 
    echo "$(after)"
    echo start"$(space)"end

未定义的变量实际上是一个空字符串!

all: 
    # 未定义的变量实际上是一个空字符串
    echo $(nowhere)

变量的追加

+= 用于追加

foo := start
foo += more

all: 
    echo $(foo)

内置变量(Implicit Variables)

Make命令提供一系列内置变量,比如,$(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的Make工具。这主要是为了跨平台的兼容性,详细的内置变量清单见手册

output:
    $(CC) -o output input.c

自动变量

$@ 是一个包含目标名称的自动变量。

all: f1.o f2.o

f1.o f2.o:
    echo $@
# 相当于:
# f1.o:
#	 echo f1.o
# f2.o:
#	 echo f2.o

这些变量具有根据规则的目标和先决条件为每个执行的规则重新计算的值。

  • $@规则目标的文件名。
  • $%目标成员名称,当目标是存档成员时。
  • $<第一个先决条件的名称。
  • $?比目标新的所有先决条件的名称,它们之间有空格。
  • $^所有先决条件的名称,它们之间有空格。
  • $+这就像$^,但不止一次列出的先决条件会按照它们在 makefile 中列出的顺序重复。
  • $|所有仅命令先决条件的名称,它们之间有空格。
  • $*隐式规则匹配的词干。

更多变量请参考: 自动变量 。

hey: one two
    # Outputs "hey", since this is the first target
    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

环境变量

make 运行时的系统环境变量可以在make 开始运行时被载入到Makefile 文件中,但是如果Makefile 中已定义了这个变量,或是这个变量由make 命令行带入,那么系统的环境变量的值将被覆盖。(如果make 指定了“-e”参数,那么,系统环境变量将覆盖Makefile 中定义的变量)。 因此,如果我们在环境变量中设置了“CFLAGS”环境变量,那么我们就可以在所有的Makefile 中使用这个变量了。这对于我们使用统一的编译参数有比较大的好处。如果Makefile 中定义了CFLAGS,那么则会使用Makefile 中的这个变量,如果没有定义则使用系统环境变量的值,一个共性和个性的统一,很像“全局变量”和“局部变量”的特性。当 make 嵌套调用时,上层Makefile中定义的变量会以系统环境变量的方式传递到下层的Makefile 中。当然,默认情况下,只有通过命令行设置的变量会被传递。而定义在文件中的变量,如果要向下层Makefile 传递,则需要使用exprot 关键字来声明。当然,我并不推荐把许多的变量都定义在系统环境中,这样,在我们执行不用的Makefile 时,拥有的是同一套系统变量,这可能会带来更多的麻烦。

特定目标变量

在Makefile中,你可以为特定的目标设置变量值。这被称为特定目标变量,或者叫做目标特定变量。

当你为一个目标指定一个特定的变量值时,这个值只会在构建那个目标时生效。这提供了对每个目标的细粒度控制,你可以为每个目标定义不同的编译器选项、依赖等。

下面是一个例子:

CC = gcc                      # 默认的编译器
CFLAGS = -Wall                # 默认的编译器选项

prog: CFLAGS += -DDEBUG       # 对于prog目标,添加-DDEBUG选项
prog: main.o util.o
    $(CC) $(CFLAGS) -o prog main.o util.o

在这个例子中,CFLAGS被设置为-Wall,它将对所有目标生效。但是,对于prog目标,CFLAGS将被追加-DDEBUG选项。这样,当你构建prog目标时,CFLAGS将会是-Wall -DDEBUG。

注意,你可以使用+=操作符来追加值,就像上面的例子中那样。你也可以直接使用=操作符来覆盖值,例如prog: CFLAGS = -DDEBUG将会使得CFLAGS只包含-DDEBUG,而不包含-Wall,当构建prog目标时。

特定于模式的变量

特定于模式的变量是 Makefile 中的一种特性,允许你根据目标文件的模式(即文件名的模式)来设置不同的变量值。这是一种非常强大的特性,可以让你进行更细粒度的控制。

下面是一个例子:

CFLAGS = -Wall
CFLAGS += -g

%.o: CFLAGS += -O2
%.dbg.o: CFLAGS += -DDEBUG -g3

%.o: %.c 
    $(CC) $(CFLAGS) -c $< -o $@

%.dbg.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

在这个例子中,

  • 所有以 .o 结尾的目标(例如o)的构建,将使用 -Wall -g -O2 作为编译器标志。
  • 所有以 .dbg.o 结尾的目标(例如dbg.o)的构建,将使用 -Wall -g -O2 -DDEBUG -g3 作为编译器标志。

这样,你可以根据不同的目标模式来调整编译器标志,例如添加调试标志或优化选项。

注意,当存在多个匹配的模式时,只有最后一个匹配的模式会生效。例如在上面的例子中,如果你尝试构建 main.dbg.o,那么将使用 -Wall -g -O2 -DDEBUG -g3,而不是 -Wall -g -O2。

Makefile显式规则

在Makefile中,显式规则是定义文件如何生成的规则。显式规则由两部分组成:目标和依赖项,以及一份或者多份命令。

一个基本的显式规则看起来如下所示:

target: dependencies
    commands
  • target: 是你想要生成的文件或者你想要执行的操作。
  • dependencies: 目标所依赖的文件。这意味着,如果任何一个依赖项比目标新,或者目标不存在,那么就会执行相应的命令。
  • commands: 这是一个或者多个命令,用来生成目标。

一个简单的显式规则的例子可能如下所示:

prog: main.o utility.o
    gcc -o prog main.o utility.o

在这个例子中,prog是目标,main.o和utility.o是依赖,而gcc -o prog main.o utility.o是用来生成目标的命令。

all

在Makefile中,”all”通常用作一个特殊的目标,代表了所有的主目标,用于构建项目的所有最终产品。在你输入 make 或 make all 命令时,”all” 目标会被默认构建。

例如,如果你的项目包含两个可执行文件 prog1 和 prog2,你可能会在Makefile中这样定义 “all” 目标:

all: prog1 prog2

prog1: prog1.o util.o
    $(CC) -o prog1 prog1.o util.o

prog2: prog2.o util.o
    $(CC) -o prog2 prog2.o util.o
...

在这个例子中,当你运行 make 或 make all 命令时,make 将会构建 prog1 和 prog2 这两个目标。如果它们或它们的依赖有任何改动,make 将会重新编译需要的文件。

注意,”all” 只是一个约定,你也可以使用其它名字,例如 “build” 或 “default”。但是,”all” 是最常见的名字,被广泛接受和使用。

Makefile隐式规则

Makefile的隐式规则是一种为那些遵循某种命名模式的文件自动创建编译命令的规则。

一个简单的隐式规则如下:

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

这个隐式规则告诉make如何从.c文件生成.o文件。在规则中,%是一个通配符,表示任意的字符串。在命令部分,$<代表依赖列表中的第一个文件,$@代表目标文件。

例如,如果你有一个名为main.c的文件,并且你在Makefile中指定main.o为目标,那么make会自动应用上面的隐式规则,执行gcc -c main.c -o main.o命令来生成main.o。

隐式规则的好处是可以减少Makefile中的重复代码。如果你有很多.c文件需要编译成.o文件,你不需要为每一个文件都写一个显式规则,只需要写一个隐式规则就可以了。

隐式规则基于文件的扩展名,默认支持的扩展名列表: .out .a .ln .o .c .cc .C .cpp .p .f .F .m .r .y .l .ym .lm .s .S .mod .sym .def .h .info .dvi .tex .texinfo .texi .txinfo .w .ch .web .sh .elc .el

以下是隐式规则列表:

  • 编译 C 程序:使用以下形式的命令o自动生成n.c$(CC) -c $(CPPFLAGS) $(CFLAGS)
  • 编译 C++ 程序:o由n.cc或n.cpp使用以下形式的命令自动生成$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)
  • 链接单个目标文件:通过运行命令n自动生成o$(CC) $(LDFLAGS) n.o $(LOADLIBES) $(LDLIBS)

隐式规则使用的重要变量是:

  • CC:用于编译 C 程序的程序;默认 cc
  • CXX: 用于编译 C++ 程序的程序;默认 g++
  • CFLAGS: 提供给 C 编译器的额外标志
  • CXXFLAGS: 提供给 C++ 编译器的额外标志
  • CPPFLAGS: 给 C 预处理器的额外标志
  • LDFLAGS: 当编译器应该调用链接器时提供额外的标志

让我们看看我们现在如何构建一个 C 程序,而无需明确告诉 Make 如何进行编译:

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*

Makefile文件指示

在Makefile中,您可以使用各种文件指示(file directive)对Makefile的处理进行更精细的控制。以下是一些常见的文件指示:

Makefile的引用另一个Makefile

在 Makefile 使用include 关键字可以把别的Makefile 包含进来,这很像C 语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。include 的语法是:

include <filename>

filename 可以是当前操作系统Shell 的文件模式(可以保含路径和通配符)在 include 前面可以有一些空字符,但是绝不能是[Tab]键开始。include和<filename>可以用一个或多个空格隔开。举个例子,你有这样几个Makefile:a.mk、b.mk、c.mk,还有一个文件叫foo.make,以及一个$(bar),其包含了e.mk 和f.mk,那么,下面的语句:

include foo.make *.mk $(bar)

等价于:

include foo.make a.mk b.mk c.mk e.mk f.mk

make 命令开始时,会寻找include 所指出的其它Makefile文件,并把其内容安置在当前的位置。就好像C/C++的#include 指令一样。如果文件都没有指定绝对路径或是相对路径的话,make首先会在当前目录下寻找,如果当前目录下没有找到,那么,make 还会在下面的几个目录下找:

  • 如果执行make命令时,有[-I]或[–include-dir]参数,那么make 就会在这个参数所指定的目录下去寻找。
  • 如果目录<prefix>/include(一般是:/usr/local/bin 或/usr/include)存在的话,make 也会去找。

include和-include都是Makefile的指令,用于包含其他Makefile文件。它们的工作方式基本相同,但处理包含文件不存在或不可读的情况时有所不同。

当使用 include 指令,如果所包含的文件不存在或者无法读取,make 会报错并停止执行。

例如,如果你写了如下指令:

include missing_file.mk

如果 missing_file.mk 并不存在,make 会显示一个错误信息并停止执行。

而 -include 指令在面对同样的情况时则更为宽容。如果 -include 指令指定的文件无法被找到或读取,make 不会报错,而是会继续执行。

例如,如果你这样写:

-include missing_file.mk

即使 missing_file.mk 不存在,make 也会继续执行。

因此,你可以使用 -include 来包含一些可能不存在但在存在的情况下需要被解析的文件,如一些自动生成的Makefile文件。

备注:语法前加上-可以忽略错误继续执行,它既可以用于Makefile语法,例如include,也可以用于“命令”中,例如:

clean:
    -rmdir obj
    -rmdir bin

如果obj不存在,也不会因报错而不进行第二个rmdir。

Makefile指定源文件路径-vpath

在Makefile中,vpath指令可以被用来指定搜索源文件的路径。这对于源文件被放置在不同目录下的大型项目特别有用。

vpath指令的语法如下:

vpath pattern directory-list
  • pattern 是一个包含%字符的模式,%表示任意数量的任意字符。例如,%.c匹配所有.c文件。
  • directory-list 是一个目录列表,每个目录由冒号分隔。

例如,以下的vpath指令告诉make在src和lib目录下查找所有.c文件:

vpath %.c src:lib

如果你只想在一个特定目录下查找特定的文件,你可以设置一个更具体的模式。例如,以下的vpath指令告诉make在src目录下查找main.c:

vpath main.c src

你也可以使用vpath指令无参数形式清除已经设置的搜索路径。

vpath

这将清除所有已设置的搜索路径。

特别注意,vpath是全局设置的,如果在多目标中使用可能会有冲突,需要谨慎使用。

define指示用于定义多行变量

define指令在Makefile中用于定义多行变量,这通常对于包含多行命令的情况非常有用。

以下是define指令的基本使用方式:

define VARIABLE_NAME
command1
command2
...
endef

在上面的例子中,VARIABLE_NAME就是你要定义的变量名,command1, command2等就是你要包含在变量中的命令或者文本。

例如,你可以定义一个变量来代表一个多行的shell脚本:

define RUN_TESTS_SCRIPT
echo "Starting tests..."
./run-tests
echo "Tests completed."
endef

然后,在你的Makefile的规则中,你可以使用$(RUN_TESTS_SCRIPT)来调用这个脚本:

test:
    $(RUN_TESTS_SCRIPT)

这样提供了一种方便的方式来组织和重用大段的命令或文本。注意,endef是必需的,用来标记多行变量定义的结束。

ifeq, ifneq, ifdef, ifndef条件处理

在Makefile中,ifeq, ifneq, ifdef, ifndef用于进行条件处理。这些指示可以让你根据特定条件来定义规则、设置变量等。

ifeq:ifeq用于测试两个值是否相等。如果相等,则执行ifeq和相应的endif之间的代码。例如:

ifeq ($(CC),gcc)
CFLAGS = -Wall
endif

在这个例子中,如果变量CC的值是gcc,则变量CFLAGS被设置为-Wall。

ifneq:ifneq和ifeq相反,用于测试两个值是否不相等。如果不相等,则执行ifneq和相应的endif之间的代码。

ifdef:ifdef用于测试一个变量是否已定义。如果已定义,则执行ifdef和相应的endif之间的代码。例如:

ifdef DEBUG
CFLAGS = -g
endif

在这个例子中,如果变量DEBUG已定义(无论其值是什么),则变量CFLAGS被设置为-g。

ifndef:ifndef和ifdef相反,用于测试一个变量是否未定义。如果未定义,则执行ifndef和相应的endif之间的代码。

在Makefile中使用这些条件指令可以让你根据特定条件来改变构建过程,这使得Makefile更加灵活和强大。

export指示用于将Makefile中的变量导出为环境变量

是的,export指令在Makefile中被用来将变量导出为环境变量。

当你在Makefile中定义了一个变量,这个变量默认只在Makefile中可见,也就是说它只是一个Makefile变量。但是有些时候,你可能希望在Makefile中启动的子进程中也能够使用这个变量,这就需要用到export指令。

以下是export的基本用法:

export VAR = value

在上面的例子中,VAR是你要导出的变量名,value是这个变量的值。

例如:

export CC = gcc

在这个例子中,CC被设置为gcc,然后使用export导出为环境变量。这样,你就可以在Makefile的命令中以及任何由Makefile启动的子进程中使用这个环境变量。

注意,export指令也可以单独使用,这样会将所有的Makefile变量都导出为环境变量:

export

export使得Makefile更灵活,因为你可以在Makefile的命令和子进程中使用环境变量。

Makefile注释

在Makefile中,你可以使用井号(#)来添加注释。从#字符至该行的结束处,所有内容都将被认为是注释。

以下是一个例子:

# 这是一个注释
CC = gcc  # 设置CC变量为gcc

在上面的例子中,第一行完全是注释,第二行中,从#开始的部分是注

Makefile 语法规则

构建规则都写在Makefile文件里面,要学会如何Make命令,就必须学会如何编写Makefile文件。

Makefile文件由一系列规则(rules)构成。每条规则的形式如下。

<target>: <prerequisites>
[tab]<commands>
  • 目标 targets 是文件名,以空格分隔。通常,每条规则只有一个。
  • 命令 command 是生成目标的一系列步骤。以制表符开头,不能用空格开头。
  • 先决条件 prerequisites 是文件名(也称为依赖项),以空格分隔。这些文件需要在执行命令之前存在。

“目标”是必需的,不可省略;”前置条件”和”命令”都是可选的,但是两者之中必须至少存在一个。每条规则就明确两件事:构建目标的前置条件是什么,以及如何构建。下面就详细讲解,每条规则的这三个组成部分。

目标(target)

在Makefile中,”目标”通常指的是你希望构建的文件,例如一个可执行程序或一个对象文件。Makefile的主要作用是描述如何从源文件和其它依赖项生成这些目标。

一个典型的Makefile规则看起来像这样:

target: dependencies
    commands

这里的target就是所谓的”目标”,它通常是一个文件名。dependencies是一个文件列表,当这些文件发生变化时,commands就会被执行以更新target。

例如,假设你有一个C程序,它的源代码文件是main.c,你要生成名为main的可执行文件。你可以在Makefile中写下如下的规则:

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

在这个例子中,main是目标,main.c是依赖项,gcc -o main main.c是用来生成目标的命令。

Makefile中的目标不一定是实际的文件名,它也可以是一个”伪目标”(或称为”虚拟目标”)。伪目标用来执行一些特定的操作,例如清理项目中的临时文件。

伪目标

在Makefile中,“伪目标”(也称为“虚拟目标”或“phony target”)通常指的是那些并不代表实际文件名的目标。它们不是由命令生成的文件,而是作为一组命令的名字。

伪目标通常用于执行清理工作(如删除临时文件),生成文档,或者执行其它不生成文件的任务。

例如,你可能会在Makefile中看到如下的规则:

.PHONY: clean
clean:
    rm -f *.o myprogram

在这个例子中,“clean”是一个伪目标。当你运行 make clean 命令时,Make会执行 rm -f *.o myprogram 命令,删除所有的 .o 文件和 myprogram 程序。

.PHONY 是一个特殊的目标,用来声明一个或多个伪目标。在上述例子中,.PHONY: clean 声明了 clean 是一个伪目标。

声明伪目标是有必要的,因为如果在你的项目中有一个名为 clean 的文件,make clean 将不会执行 rm -f *.o myprogram 命令,除非 clean 文件是过时的。但是,如果 clean 是一个伪目标,make clean 命令总是会执行 rm -f *.o myprogram 命令,无论是否存在名为 clean 的文件。

多个目标

在Makefile中,你可以定义一个规则有多个目标。这意味着,当任何一个目标需要更新时,关联的命令就会被执行。

例如,考虑以下的Makefile规则:

file1.o file2.o: header.h source.c
    gcc -c source.c

在这个例子中,file1.o 和 file2.o 是这个规则的目标,当 header.h 或 source.c 中的任何一个改变时,gcc -c source.c 这个命令就会被执行。

但是请注意,这并不是说 gcc -c source.c 会被执行两次,分别为 file1.o 和 file2.o,而是说,当 file1.o 或 file2.o 中任何一个不存在,或者它们的依赖文件更新时,命令就会被执行。

如果你希望对每个目标单独执行命令,你需要使用模式规则或者静态模式规则。例如:

file1.o file2.o: %.o: %.c
    gcc -c $< -o $@

在这个例子中,gcc -c $< -o $@ 对每个目标(这里是 file1.o 和 file2.o)都会被执行一次。$< 是自动变量,它代表依赖列表中的第一个文件,$@ 代表当前的目标。

前置条件(prerequisites)

在Makefile中,前置条件(prerequisites)也被称为依赖项(dependencies),它们是一个目标构建所需要的文件或其他目标。

一个基本的Makefile规则看起来像这样:

target: prerequisites
    commands

其中,target是你想要构建的目标,prerequisites是构建该目标所需的文件或其他目标,而commands是需要执行的命令行,以从prerequisites生成target。

例如,如果你有一个C程序,它的源代码文件是main.c,并且你想生成一个名为main的可执行文件。你可能会在Makefile中写下如下的规则:

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

在这个例子中,main是目标,main.c是前置条件或依赖项,而gcc -o main main.c是用来生成目标的命令。

当前置条件比目标文件新,或者目标不存在时,关联的命令就会被执行。这是Makefile的基本工作原理:只有当目标需要更新时,才会执行命令。

order-only依赖

在Makefile中,大多数的依赖关系是基于时间戳的:如果依赖项比目标新,那么目标就需要被重新构建。然而,有时候我们可能想要指定一些依赖项,它们的改变并不会触发目标的重新构建,但是如果它们不存在,我们希望它们被创建。这种类型的依赖被称为”order-only”依赖。

你可以在规则中使用管道符(|)来指定order-only依赖。

例如,假设我们有一个目录output,我们想要把所有的.o文件复制到这个目录。我们可以写一个如下的规则:

output/%.o: %.o | output
    cp $< $@

在这个规则中,output是一个order-only依赖。如果output目录不存在,make会创建它。然而,一旦创建之后,即使我们修改了output目录,也不会触发.o文件的复制操作。

这种类型的依赖在很多情况下都很有用,特别是当你有一些输出目录或者中间文件,而你并不想因为它们的改变而触发目标的重新构建。

命令(commands)

在Makefile中,命令(commands)是一行或多行用于生成目标(target)的shell命令,它们在目标和前置条件(prerequisites)声明之后被指定。

一个基本的Makefile规则看起来像这样:

target: prerequisites
    commands

其中,target是你想要构建的目标,prerequisites是构建该目标所需的文件或其他目标,而commands是需要执行的命令行,以从prerequisites生成target。

例如,如果你有一个C程序,它的源代码文件是main.c,并且你想生成一个名为main的可执行文件。你可能会在Makefile中写下如下的规则:

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

在这个示例中,gcc -o main main.c就是用于生成目标main的命令。当main.c发生更改时,这条命令将被执行,从而重新编译main。

在Makefile中,每个命令都必须以一个Tab字符开始。如果你尝试使用空格代替Tab,make会给出错误提示。这是GNU make的一个特性,旨在明确区分命令和其他Makefile元素。

命令列表

在Makefile中,每个规则后面都可以跟一个命令列表,用来产生这个规则的目标。命令列表中的每一行都必须以Tab字符开始。这是Makefile的一个重要的语法规定。

一个基本的Makefile规则和命令列表可以如下所示:

target: dependencies
    command1
    command2
    command3

在这个例子中,target是规则的目标,dependencies是目标依赖的文件或其他目标,command1、command2和command3构成了命令列表。

当make target被运行时,Make会首先检查dependencies列表中的每个文件或目标。如果任何一个依赖比target新,或者target不存在,那么Make就会执行命令列表,来生成或更新target。

在命令列表中,每一行都是一个独立的命令。Make会按照它们在列表中的顺序来执行这些命令。如果任何一个命令失败(即返回一个非零的退出状态),那么Make会停止执行后面的命令,而且make命令本身也会返回一个错误状态。

如果你想让Make忽略一个命令的错误状态,你可以在这个命令前面加一个减号(-)。例如,-rm foo命令会尝试删除文件foo,但即使这个文件不存在(所以rm命令会返回一个错误状态),Make也不会因此停止。

正如上面所说,命令列表中的每一行都必须以Tab字符开始。如果你使用空格而不是Tab,Make会给出一个错误信息。这是一个常见的初学者错误。对于这个问题的一个常见解决方案是,配置你的文本编辑器,让它在Makefile中把Tab键转换为真正的Tab字符,而不是空格。

默认 shell

在Makefile中,所有的命令默认都是在一个新的shell进程中执行。这个shell进程是由Makefile的SHELL变量指定的。如果你没有设置SHELL变量,那么默认的shell通常是/bin/sh。

例如,以下的Makefile规则:

target:
    echo "Building target"

在执行时,make实际上会创建一个新的shell进程,然后在这个进程中执行echo “Building target”命令。

你可以通过设置SHELL变量来改变Makefile使用的shell。例如,如果你想使用bash,你可以这样设置SHELL变量:

SHELL = /bin/bash

target:
    echo "Building target"

然而请注意,改变SHELL变量并不会改变Makefile中变量赋值和条件表达式的语法,因为这些都是由make本身,而不是shell进程来解析的。

Makefile 的实例

首先我们需要以下三个文件:

hellomake.c 源文件:

// hellomake.c
#include <hellomake.h>

int main() {
  // call a function in another file
  myPrintHelloMake();

  return(0);
}

hellofunc.c 源文件:

// hellofunc.c
#include <stdio.h>
#include <hellomake.h>

void myPrintHelloMake(void) {

  printf("Hello makefiles!\n");

  return;
}

hellomake.h 头文件:

/*
example include file
*/

void myPrintHelloMake(void);

现在,你可以使用以下命令编译这些代码:

gcc -o hellomake hellomake.c hellofunc.c -I.

gcc 编译器会编译两个 C 源文件并把可执行程序命名为 hellomake。参数“-I.”用以指示 gcc 在当前目录“.”下寻找头文件 hellomake.h。如果没有 makefile 文件,每次当我们修改过源文件后,如果要重新编译代码,我们都需要重新输入编译命令(虽然可以使用 UP 箭头找到历史命令),当要编译的源文件很多时,这样做就很没有效率。

不幸的是,这样做还有另外两个问题。首先,如果编译命令丢失了,你就需要重新输入。其次,如果你只对一部分源文件做了修改,每次都重新编译所有的文件耗时且低效。因此,我们最好花点时间看一下 makefile 可以为我们做什么。

makefile 由一系列的规则组成,规则的结构如下:

目标文件:依赖文件
     命令1
     命令2
     ...
     命令n

你能创建的最简单的 makefile 可能像这样:

hellomake: hellomake.c hellofunc.c
     gcc -o hellomake hellomake.c hellofunc.c -I.

将此文件命名为 Makefile 或者 makefile,然后在命令行输入 make,它会执行你在 makefile 里所写的编译指令(如果报错请检查第二行的命令前是否是一个 tab)。注意,不带参数的 make 会执行 makefile 文件中的第一条规则。此外,通过将编译 hellomake 所依赖的源文件放在 “: ” 之后,make 知道如果其中任何依赖文件发生更改,则需要执行 hellomake 规则,即需要重新编译此文件。现在 makefile 已经帮助我们解决了之前提出的问题一,但是现在系统依然不会只编译更改过的文件。

需要注意的是,makefile 中的命令必须以 tab 开始,不能使用空格。

为了使编译过程更加有效率,我们试一下以下的代码:

CC=gcc
CFLAGS=-I.

hellomake: hellomake.o hellofunc.o
     $(CC) -o hellomake hellomake.o hellofunc.o

如你所见,我们现在定义了两个常量(也可称之为 macro,宏) CC 和 CFLAGS,这些特殊的常量用来告诉 make 我们要如何编译源文件。特别地,CC 指定了我们要使用的 C 编译器,而 CFLAGS 则是要传递给编译命令的标志的列表。通过把目标文件 hellomake.o 和 hellofunc.o 放在依赖项中,make 知道它首先需要编译出 .o 目标文件,然后才能编译可执行文件 hellomake。

这种形式的 makefile 已经适用于大多数小型项目了。然而,我们还少了一个东西:头文件。举个例子,如果你对头文件 hellomake.h 做了修改,make 不会重新编译 hellomake.c,尽管实际上需要重新编译。为了解决这个问题,我们需要告诉 make 所有的 .c 源文件依赖于特定的一些 .h 头文件。我们所要做的就是在 makefile 中添加一条简单的规则:

CC=gcc
CFLAGS=-I.
DEPS = hellomake.h

%.o: %.c $(DEPS)
    $(CC) -c -o $@ $< $(CFLAGS)

hellomake: hellomake.o hellofunc.o 
    $(CC) -o hellomake hellomake.o hellofunc.o 

我们添加了一个新的常量:DEPS,其指定了一些 .c 源文件所依赖的 .h 头文件。然后我们定义了一条新的规则,其应用于所有以 .o 结尾的目标文件。这条规则表明了 .o 文件依赖于相应的 .c 文件和 DEPS 中包含的头文件。之后此规则表示,要生成 .o 目标文件,make 需要使用 CC 中指定的 C 编译器编译 .c 源文件。参数 -c 指示编译器产生 .o 目标文件;参数 $@ 和 $^ 分别指代规则第一行中的冒号的左右两边的内容,参数 $< 表示第一个依赖文件。

接下来我们使用 OBJ 来表示我们需要的目标文件,这样可以简化我们之前的 makefile 文件:

CC=gcc
CFLAGS=-I.
DEPS = hellomake.h
OBJ = hellomake.o hellofunc.o 

%.o: %.c $(DEPS)
    $(CC) -c -o $@ $< $(CFLAGS)

hellomake: $(OBJ)
    $(CC) -o $@ $^ $(CFLAGS)

如果我们想把 .h 文件放在 include 目录下,把 .c 文件放在 src 目录下以及把一些本地的库文件放在 lib 目录下要怎么做呢?还有,我们可不可以把烦人的 .o 文件通过某种方法隐藏呢?下面的 makefile 文件可以解决这两个问题。注意此 makefile 应放在 src 目录下。

IDIR =../include
CC=gcc
CFLAGS=-I$(IDIR)

ODIR=obj
LDIR =../lib

LIBS=-lm

_DEPS = hellomake.h
DEPS = $(patsubst %,$(IDIR)/%,$(_DEPS))

_OBJ = hellomake.o hellofunc.o 
OBJ = $(patsubst %,$(ODIR)/%,$(_OBJ))


$(ODIR)/%.o: %.c $(DEPS)
    $(CC) -c -o $@ $< $(CFLAGS)

hellomake: $(OBJ)
    $(CC) -o $@ $^ $(CFLAGS) $(LIBS)

.PHONY: clean

clean:
    rm -f $(ODIR)/*.o *~ core $(INCDIR)/*~ 

现在你应该可以为中小型项目编写 makefile 文件了。你可以向 makefile 里添加很多条规则,甚至可以创建调用其他规则的规则。

Makefile高级用法

Makefile特殊字符

在Makefile中,有一些特殊字符具有特定的含义。以下是一些常见的特殊字符和它们的含义:

  • #:此字符用于标注注释,从#开始到行尾的所有字符都将被Makefile解析器忽略。
  • $:此字符用于引用Makefile变量。例如,$(CC)引用了一个名为CC的变量。$@和$<这样的特殊符号用于引用目标和依赖。
  • ::此字符用于在规则中分隔目标和依赖。例如,在规则foo: bar中,:左边的foo是目标,右边的bar是依赖。
  • =:此字符用于赋值操作。例如,CC = gcc将gcc赋值给变量CC。
  • *、?、[]和%:这些字符是通配符,用于匹配文件名或者字符串。其中,%在模式规则中特别常见。
  • \:此字符用于转义特殊字符,使其失去特殊含义,成为文字字符。例如,\\表示一个文字的反斜杠,\#表示一个文字的#。
  • @:在命令行前加此字符,该命令就不会在终端中显示。
  • -:在命令行前加此字符,即使该命令出错,Make也不会停止。
  • +:在命令行前加此字符,该命令总是会被执行,即使你给Make指定了-n或-t选项。

记住,这些特殊字符在Makefile中可能会有特定的含义,如果你需要在Makefile中使用这些字符作为文字字符,你可能需要对它们进行转义。

转义字符

在Makefile中,一些特殊字符(如#,$)有特殊的含义。如果你想在Makefile中直接使用这些字符,而不触发它们的特殊含义,你需要对它们进行转义。

  • #:在Makefile中,#是注释的开始。如果你想在命令或其他地方使用#,你需要用\#来转义它。
  • $:在Makefile中,$用于引用变量。如果你想直接使用$,你需要用$$来转义它。

例如,下面的Makefile规则打印$HOME环境变量:

print-home:
    echo $$HOME

在这个命令中,我们使用了$$来打印$字符。如果我们只写$HOME,Make会试图找一个叫做HOME的Make变量,而不是环境变量。

另外,如果你在命令中使用了反斜杠(\),你要注意它可能会被Make和shell两次解析,所以有时你需要写两个反斜杠(\\)来得到一个反斜杠。这取决于你的命令是如何使用反斜杠的。

行分割

在编程中,行分隔符是一种特别的字符或字符序列,用来标记一行的结束。不同的操作系统使用的行分隔符可能不同。

  • 在Unix和Linux系统中,行分隔符是换行符(\n)。
  • 在老的Mac OS系统中,行分隔符是回车符(\r)。但是在Mac OS X及其后续版本中,也采用了Unix风格的换行符(\n)作为行分隔符。
  • 在Windows系统中,行分隔符是两个字符的序列,回车符跟着换行符(\r\n)。

注意,在文本文件中直接查看这些字符是看不到的,它们通常会被文本编辑器翻译成换行。

在Makefile中,每一行通常表示一个独立的命令。如果你想把一个长命令分成多行写,你可以在行尾使用反斜杠(\),Makefile会把反斜杠和它后面的换行符一起看作一个空格。例如:

target:
    command -option1 -option2 \
    -option3 -option4

这里,command -option1 -option2 -option3 -option4被看作一个整体的命令。

Makefile函数

Makefile函数提供了一些用于字符串操作、文件名操作、控制流等操作的功能。函数可以用在变量定义和命令列表中。一个函数调用看起来像这样:

$(function arguments)

或者:

${function arguments}

这里的 function 是函数名,arguments 是函数的参数,参数之间用逗号分隔。函数和参数之间的空格会被忽略。

下面是一些常用的函数:

  • $(wildcard pattern):返回匹配模式 pattern 的所有文件名。
  • $(patsubst pattern,replacement,text):将 text 中匹配模式 pattern 的部分替换为 replacement。
  • $(subst from,to,text):将 text 中的 from 替换为 to。
  • $(strip string):去除 string 开头和结尾的空格。
  • $(filter pattern…,text):从 text 中选出匹配模式 pattern 的单词。
  • $(filter-out pattern…,text):从 text 中选出不匹配模式 pattern 的单词。

例如,下面的规则用于编译所有的 .c 文件:

SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(SRCS))

all: $(OBJS)

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

在这个例子中,$(wildcard *.c) 返回所有的 .c 文件,$(patsubst %.c,%.o,$(SRCS)) 将 .c 文件名替换为 .o 文件名。然后,对于每个 .o 文件,都有一个对应的规则来编译它。

Makefile通配符

在Makefile中,你可以使用通配符(wildcards)来匹配一类文件名,这会大大简化Makefile的编写。以下是一些常用的通配符:

  • *:匹配零个或多个字符。例如,*.c会匹配所有.c文件。
  • ?:匹配任意单个字符。例如,?.c会匹配c,但不会匹配main.c。
  • […]:匹配方括号内的任意一个字符。例如,[ab].c会匹配c和b.c,但不会匹配其他任何文件。

另外,Makefile还提供了wildcard函数,用于在规则中使用通配符。例如:

SRCS := $(wildcard *.c)

以上规则中,$(wildcard *.c)会展开为当前目录下所有的.c文件。

有时,你可能还想使用patsubst函数和通配符一起,以生成新的文件名列表。例如:

SRCS := $(wildcard *.c)
OBJS := $(patsubst %.c,%.o,$(SRCS))

在这个例子中,如果SRCS为main.c foo.c bar.c,那么OBJS将会被设置为main.o foo.o bar.o。这是一种常用的模式,用于指定所有.c文件对应的.o文件。

% 通配符

在Makefile中,%号是一个特殊的通配符,用于定义模式规则。在模式规则中,%可以匹配任意长度的字符串。

例如,下面的Makefile规则:

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

这里的%.o是目标模式,%.c是依赖模式。这个规则表示如何从一个.c文件(例如,foo.c)创建一个.o文件(例如,foo.o)。

在命令中,$@代表目标文件名,$<代表第一个依赖文件名。所以,这个规则的意思是,对于每个.o文件,Make都会找到对应的.c文件,并执行gcc -c -o target dependency命令来编译.c文件。

这种模式规则可以极大地简化Makefile,使其更易于维护。因为你不需要为每一个源文件写一个单独的规则,只需要写一个模式规则就可以覆盖所有同类型的文件。

模式匹配

在Makefile中,模式匹配主要通过模式规则和通配符(特别是%通配符)来实现。

模式规则是一种特殊的规则,它可以匹配多个目标,并且它的命令中可以使用自动变量。模式规则的形式为:

%.o: %.c
    command

在这个规则中,%被称为模式字符,它可以匹配任意数量的任意字符。在目标模式和依赖模式中,%必须出现在同一位置,这样Make才能正确地找到目标和依赖。

例如,如果你有foo.c和bar.c两个源文件,你可以使用以下模式规则来生成对应的目标文件foo.o和bar.o:

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

在这个命令中,$<是自动变量,表示第一个依赖(即对应的.c文件),$@是自动变量,表示目标(即对应的.o文件)。

注意:模式规则不仅可以用于文件名,也可以用于目标名。例如,你可以定义一个模式规则,匹配所有以_test结尾的目标。

自动生成头文件依赖

在C或C++项目中,源代码文件(.c或.cpp)通常会包含一些头文件(.h)。当头文件发生变化时,依赖于它的源代码文件需要被重新编译。因此,在Makefile中,除了源文件和目标文件的依赖关系,正确地指定头文件的依赖关系也非常重要。

然而,手动在Makefile中为每个源文件指定头文件的依赖关系是一项繁琐且容易出错的任务。幸运的是,GCC和许多其他编译器提供了自动生成头文件依赖关系的功能。

例如,GCC提供了-MMD和-MP选项来自动生成头文件依赖关系。-MMD选项告诉GCC为每个源文件生成一个.d文件,这个文件中包含了源文件的头文件依赖关系。-MP选项生成一个或多个空目标,解决了删除或重命名头文件时,遗留的.d文件可能导致的问题。

在Makefile中,我们可以如下使用这些选项:

CFLAGS = -MMD -MP

然后,我们可以在Makefile的规则中包含这些.d文件:

-include $(wildcard *.d)

这行命令告诉Make包含所有.d文件。-include指令和include指令类似,但如果一个文件不存在,-include不会产生错误。

这样,当我们编译源文件时,GCC会自动生成头文件的依赖关系,Make会自动包含这些依赖关系。当头文件发生变化时,Make会知道哪些源文件需要被重新编译。

Makefile命令回显

在Makefile中,当make命令执行时,默认情况下,它会打印出它正在执行的每个命令。这被称为命令回显,它可以帮助我们了解make正在做什么。

然而,有时候我们可能不想看到所有的命令回显。例如,一些命令的输出可能会淹没其他更重要的信息,或者我们只想看到命令的结果,而不是命令本身。

在Makefile中,我们有几种方式可以控制命令回显:

在命令前加@:当我们在一个命令前加上@字符,那么这个命令就不会被make回显。例如:

target:
    @echo This command will not be echoed

在上面的例子中,make target会打印This command will not be echoed,但它不会打印命令本身。

使用.SILENT特殊目标:我们可以把.SILENT作为一个目标,它的依赖是我们不想回显的目标。例如:

.SILENT: target
target:
    echo This command will not be echoed

在上面的例子中,make target会行为和上一个例子一样。我们也可以把.SILENT没有依赖,那么make就不会回显任何命令。

命令行选项-s或–silent:我们可以在运行make时使用这些选项,它们会让make不回显任何命令。

Makefile静态模式规则

在Makefile中,静态模式规则是一种特殊类型的规则,它允许你为多个目标定义相同的构建规则,而无需为每个目标单独定义规则。静态模式规则的一般形式如下:

targets : target-pattern: dep-patterns
    commands
  • targets 是你想要应用规则的目标列表。
  • target-pattern 是一个包含%的模式,它匹配targets列表中的目标。
  • dep-patterns 是一个或多个包含%的模式,它们定义了每个目标的依赖。
  • commands 是一个命令列表,它定义了如何构建目标。

例子:

objects = file1.o file2.o file3.o

$(objects) : %.o : %.c
    $(CC) -c $(CFLAGS) $< -o $@

在这个例子中,$(objects) : %.o : %.c是一个静态模式规则。它定义了如何从.c文件构建.o文件。%.o是目标模式,%.c是依赖模式。

在命令列表中,$<表示规则的第一个依赖(在这个例子中,就是对应的.c文件),$@表示规则的目标(在这个例子中,就是.o文件)。所以,这个规则的意思是:对于objects列表中的每个目标,使用$(CC) -c $(CFLAGS) $< -o $@命令从对应的.c文件构建它。

参考链接:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注