Makefile核心语法

快速上手makefile...

Makefile

这篇文章我们围绕Go项⽬中常⽤的Makefile语法规则,来盘一盘Makefile工具的使用。

Makefile有很多语法,但不是所有语法都需要熟练掌握的,部分语法在Go项⽬中是很少⽤到。要编写⼀个⾼质量的Makefile,⾸先应该掌握⼀些核⼼的最常⽤的语法知识。

话不多说,开始进入今天的主题吧~

Makefile是什么?

Makefile本质是包含一系列规则shell命令的文本文件,用于指导如何编译源代码,构建可执行文件。

Makefile⽂件由3部分组成:

  • Makefile规则

    描述构建过程的结构,用于指定如何从源文件生成目标文件。

  • Makefile语法

    Makefile文件的编写规范和格式,包含一系列的语法规则。

  • Makefile命令

    可以是Linux 命令,也可以是可执⾏的脚本⽂件

文章也会按照这个顺序依次展开,并辅以案例。让我们正式开始吧~

规则

规则是Makefile中的重要概念,也是我们学习Makefile的核心。它⼀般由⽬标依赖命令组成,⽤来指定源⽂件编译的先后顺序。Makefile之所以受欢迎,核⼼原因就是Makefile规则,因为 Makefile规则可以⾃动判断是否需要重新编译某个⽬标,从⽽确保⽬标仅在需要时编译。

规则语法

规则的语法部分主要包括targetprerequisitescommand,⽰例如下:

1
2
3
target ...: prerequisites ...
command
...
  1. target

    target是指规则所要构建的目标文件目标名称,描述了构建的最终结果。

  2. prerequisites

    ⽣成target所需的依赖项。当有多个依赖项时,依赖项之间⽤空格分隔。

  3. command

    生成target要执⾏的命令,可以是任意的shell命令

    • 在执⾏command之前,默认会打印出该命令,然后再输出命令的结果;如果不想打印出命令,可在各个command前加上@
    • command可以为多条,也可以分⾏写,但每⾏都要以tab键开始。另外,如果后⼀条命令依赖前⼀条命令,则这两条命令需要写在同⼀⾏,并⽤分号进⾏分隔。
    • 如果要忽略命令的出错,可以在各个command之前加上减号-

只要targets不存在,或者prerequisites文件有更新,那么command所定义的命令就会被执⾏,重新生成targets文件。

我们来看个栗子,串联一下规则语法

1
2
my_program: main.c helper.c
    gcc -o my_program main.c helper.c

在这个栗子中,my_program就是target。这个规则表明,如果main.c或者helper.c中的任何一个文件发生了变化,那么my_program将被重新构建。构建的命令是gcc -o my_program main.c helper.c

伪⽬标

Makefile的管理能⼒基本上都是通过伪⽬标来实现的。

伪⽬标不是⽂件,make ⽆法⽣成它的依赖关系,也⽆法决定是否要执⾏它。 因此,无法为伪目标生成任何文件。

通常情况下,我们需要显式地标识某个⽬标为伪⽬标。在Makefile中可以使⽤.PHONY来标识⼀个⽬标为伪⽬标

1
2
3
.PHONY: clean
clean:
rm hello.o

伪⽬标可以有依赖⽂件,也可以作为“默认⽬标”,例如:

1
2
.PHONY: all
all: lint test build

因为伪⽬标总是会被执⾏,所以其依赖总是会被决议。通过这种⽅式,可以达到同时执⾏所有依赖项的⽬的。

order-only依赖

在上⾯介绍的规则中,只要prerequisites中有任何⽂件发⽣改变,就会重新构造target。但是有时候,我们希望只有当prerequisites中的部分⽂件改变时,才重新构造target。这时,你可以通过order-only prerequisites实现。

order-only prerequisites的形式如下:

1
2
3
targets : normal-prerequisites | order-only-prerequisites
command
...

在上⾯的规则中,只有第⼀次构造targets时,才会使⽤order-only-prerequisites。后⾯即使order-onlyprerequisites发⽣改变,也不会重新构造targets。

只有normal-prerequisites中的⽂件发⽣改变时,才会重新构造targets。这⾥,符号“ | ”后⾯的 prerequisites就是order-only-prerequisites。

语法

Makefile语法⽐较多,这里只介绍核⼼的命令变量条件语句函数。掌握了这些之后,再在实践中多加运⽤,就可以写出⾮常复杂、功能强⼤的Makefile⽂件了。

命令

Makefile⽀持Linux命令,调⽤⽅式跟在Linux系统下调⽤命令的⽅式基本⼀致。默认情况下,make会把正在执⾏的命令输出到当前屏幕上,可以通过在命令前加@符号的⽅式,禁⽌make输出当前正在执⾏的命令。

来看⼀个例⼦,现在有这么⼀个Makefile:

1
2
3
.PHONY: test
test:
echo "hello world"

执⾏make命令

1
2
3
$ make test
echo "hello world"
hello world

可以看到,make输出了执⾏的命令。很多时候,我们不需要这样的提⽰,因为我们更想看的是命令产⽣的⽇志,⽽不是执⾏的命令。这时就可以在命令⾏前加@,禁⽌make输出所执⾏的命令:

1
2
3
.PHONY: test
test:
@echo "hello world"

再次执⾏make命令:

1
2
$ make test
hello world

可以看到,make只是执⾏了命令,⽽没有打印命令本⾝。这样make输出就清晰了很多。

这⾥,我建议在命令前都加@符号,禁⽌打印命令本⾝,以保证你的Makefile输出易于阅读的、有⽤的信息。

默认情况下,每条命令执⾏完make就会检查其返回码。如果返回成功(返回码为0),make就执⾏下⼀条指令;如果返回失败(返回码⾮0),make就会终⽌当前命令。很多时候,命令出错(⽐如删除了⼀个不存在的⽂件)时,我们并不想终⽌,这时就可以在命令⾏前加 - 符号,来让make忽略命令的出错,以继续执 ⾏下⼀条命令,⽐如:

1
2
clean:
-rm hello.o

变量

变量,可能是Makefile中使⽤最频繁的语法了,Makefile⽀持变量赋值多⾏变量环境变量。另外, Makefile还内置了⼀些特殊变量⾃动化变量

我们先来看下最基本的变量赋值功能。

变量赋值

Makefile也可以像其他语⾔⼀样⽀持变量。在使⽤变量时,会像shell变量⼀样原地展开,然后再执⾏替换后的内容。

Makefile可以通过变量声明来声明⼀个变量,变量在声明时需要赋予⼀个初值,⽐如ROOT_PACKAGE=github.com/marmotedu/iam

引⽤变量时可以通过$()或者${}的⽅式。这里建议:

  1. ⽤$()⽅式引⽤变量,例如 $(ROOT_PACKAGE);
  2. 整个makefile的变量引⽤⽅式保持⼀致。

变量会像bash变量⼀样,在使⽤它的地⽅展开。⽐如:

1
2
3
GO=go
build:
$(GO) build -v .

展开后为:

1
2
3
GO=go
build:
go build -v .

接下来,我给你介绍下Makefile中的4种变量赋值⽅法。

  1. =最基本的赋值⽅法。例如:

    1
    
    BASE_IMAGE = alpine:3.10
    

    注意,=右边为变量时,值取最终值

    1
    2
    3
    4
    
    A = a
    # B最后的值为 c b,⽽不是a b
    B = $(A) b
    A = c
    
  2. :=直接赋值,赋予当前值,可以避免=赋值带来的潜在的不⼀致。例如:

    1
    2
    3
    4
    
    A = a
    # B最后的值为 a b
    B := $(A) b
    A = c
    
  3. ?= 表⽰如果该变量没有被赋值,则赋予等号后的值。例如:

    1
    2
    3
    
    # 如果之前已经定义了 PLATFORMS 变量
    # 这个赋值语句就会被忽略,不会覆盖先前的定义
    PLATFORMS ?= linux_amd64 linux_arm64
    
  4. +=表⽰将等号后⾯的值添加到前⾯的变量上。 例如:

    1
    
    MAKEFLAGS += --no-print-directory
    

多行变量

Makefile中,可以通过define-endef关键字定义多⾏变量,变量中允许换⾏:

1
2
3
4
define 变量名
变量内容
...
endef

变量内容被赋值给define定义的变量名,可以包含函数命令⽂字或是其他变量。

看个栗子:

1
2
3
4
5
6
7
define USAGE_OPTIONS
Options:
DEBUG Whether to generate debug symbols. Default is 0.
BINS The binaries to build. Default is all of cmd.
...
V Set to 1 enable verbose build. Default is 0.
endef

环境变量

Makefile中有两种环境变量,分别是Makefile预定义的环境变量和⾃定义的环境变量。

其中,⾃定义的环境变量可以覆盖Makefile预定义的环境变量。默认情况下,Makefile中定义的环境变量只在当前Makefile有效,如果想向下层传递(Makefile中调⽤另⼀个Makefile),需要使⽤export关键字来声明。

下⾯的例⼦声明了⼀个环境变量,并可以在下层Makefile中使⽤:

1
2
3
...
export USAGE_OPTIONS
...

内置变量

Makefile支持两种内置变量:特殊变量⾃动化变量

特殊变量

特殊变量是make提前定义好的,可以在makefile中直接引⽤。特殊变量列表如下:

变量 含义
MAKE 当前make解释器的文件名
MAKECMDGOALS 命令行中指定的目标名(make的命令行参数)
CURDIR 当前make解释器的工作目录
MAKE_VERSION 当前make解释器的版本
MAKEFILE_LIST make所需处理的makefile文件列表,当前makefile的文件名总是位于列表的最后,文件名之间以空格进行分隔
.DEFAULT_GOAL 指定如果在命令行中未指定目标,应该构建哪个目标,即使这个目标不是在第一行
.VARIABLES 所有已经定义的变量名列表(预定义变量和自定义变量)
.FEATURES 列出本版本支持的功能,以空格隔开
.INCLUDE_DIRS make查询makefile的路径,以空格隔开

自动化变量

⾃动化变量可以提⾼我们编写Makefile的效率质量

在Makefile的模式规则中,⽬标依赖⽂件都是⼀系列的⽂件,那么我们如何书写⼀个命令,来完成从不同的依赖⽂件⽣成相对应的⽬标呢?

这时就可以⽤到⾃动化变量。所谓⾃动化变量,就是这种变量会把模式中所定义的⼀系列的⽂件⾃动地挨个取出,⼀直到所有符合模式的⽂件都取完为⽌。这种⾃动化变量只应出现在规则的命令中。Makefile中⽀持的⾃动化变量⻅下表。

变量 含义
$@ 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么$@就是匹配于目标中模式定义的集合
$% 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是foo.a(bar.o),那么$%就是bar.o,$@就是foo.a。如果目标不是函数库文件(Unix下是.a,Windows下是.lib),那么其值为空
$< 依赖目标中的第一个目标名字。如果依赖目标是以模式(即%)定义的,那么$<将是符合模式的一系列的文件名
$? 所有比目标新的依赖目标的集合,以空格分隔
$^ 所有以来的目标的集合,以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份
$+ 这个变量很像$^,也是所有依赖目标的集合,只是它不去除重复的依赖目标
$| 所有的order-only依赖目标的集合,以空格分隔
$* 这个变量表示目标模式中%及其之前的部分。如果目标是dir/a.foo.b,并且目标的模式是a.%.b,那么,$*的值就是dir/a.foo

上⾯这些⾃动化变量中,$*是⽤得最多的。$* 对于构造有关联的⽂件名是⽐较有效的。如果⽬标中没有模式的定义,那么$*也就不能被推导出。但是,如果⽬标⽂件的后缀是make所识别的,那么 $* 就是除了后缀的那⼀部分。例如:如果⽬标是foo.c ,因为.c是make所能识别的后缀名,所以 $* 的值就是foo。

条件语句

Makefile也⽀持条件语句。这⾥先看⼀个⽰例。 下⾯的例⼦判断变量ROOT_PACKAGE是否为空,如果为空,则输出错误信息,不为空则打印变量值:

1
2
3
4
5
ifeq ($(ROOT_PACKAGE),)
$(error the variable ROOT_PACKAGE must be set prior to including golang.mk)
else
$(info the value of ROOT_PACKAGE is $(ROOT_PACKAGE))
endif

条件语句的语法为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# if ...
<conditional-directive>
<text-if-true>
endif
# if ... else ...
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif

例如,判断两个值是否相等:

1
2
3
4
5
ifeq 条件表达式
...
else
...
endif
  • ifeq表⽰条件语句的开始,并指定⼀个条件表达式。表达式包含两个参数,参数之间⽤逗号分隔,并且表 达式⽤圆括号括起来。
  • else表⽰条件表达式为假的情况。
  • endif表⽰⼀个条件语句的结束,任何⼀个条件表达式都应该以endif结束。
  • 表⽰条件关键字,有4个关键字:ifeq、ifneq、ifdef、ifndef。

为了加深你的理解,我们分别来看下这4个关键字的例⼦。

  1. ifeq:条件判断,判断是否相等。 例如:

    1
    2
    3
    4
    5
    
    ifeq (<arg1>, <arg2>)
    ifeq '<arg1>' '<arg2>'
    ifeq "<arg1>" "<arg2>"
    ifeq "<arg1>" '<arg2>'
    ifeq '<arg1>' "<arg2>"
    

    ⽐较arg1和arg2的值是否相同,如果相同则为真。也可以⽤make函数/变量替代arg1或arg2,例如 ifeq ($(origin ROOT_DIR),undefined) 或 ifeq ($(ROOT_PACKAGE),) 。origin函数会在之后专⻔讲 函数的⼀讲中介绍到。

  2. ifneq:条件判断,判断是否不相等。

1
2
3
4
5
ifneq (<arg1>, <arg2>)
ifneq '<arg1>' '<arg2>'
ifneq "<arg1>" "<arg2>"
ifneq "<arg1>" '<arg2>'
ifneq '<arg1>' "<arg2>"

⽐较arg1和arg2的值是否不同,如果不同则为真。

  1. ifdef:条件判断,判断变量是否已定义。

    1
    
    ifdef <variable-name>
    

    如果值⾮空,则表达式为真,否则为假。也可以是函数的返回值。

  2. ifndef:条件判断,判断变量是否未定义。

    1
    
    ifndef <variable-name>
    

    如果值为空,则表达式为真,否则为假。也可以是函数的返回值。

函数

Makefile同样也⽀持函数,函数语法包括定义语法调⽤语法

我们先来看下⾃定义函数。 make解释器提供了⼀系列的函数供Makefile调⽤,这些函数是Makefile的预定义函数。我们可以通过define关键字来⾃定义⼀个函数。⾃定义函数的语法为:

1
2
3
define 函数名
函数体
endef

例如,下⾯这个⾃定义函数:

1
2
3
4
define Foo
@echo "my name is $(0)"
@echo "param is $(1)"
endef

define本质上是定义⼀个多⾏变量,可以在call的作⽤下当作函数来使⽤,在其他位置使⽤只能作为多⾏变 量来使⽤,例如:

1
2
var := $(call Foo)
new := $(Foo)

⾃定义函数是⼀种过程调⽤,没有任何的返回值。可以使⽤⾃定义函数来定义命令的集合,并应⽤在规则 中。

再来看下预定义函数。 刚才提到,make编译器也定义了很多函数,这些函数叫作预定义函数,调⽤语法和 变量类似,语法为:

1
$(<function> <arguments>)

或者

1
${<function> <arguments>}

是函数名,是函数参数,参数间⽤逗号分割。函数的参数也可以是变量。 我们来看⼀个例⼦:

1
2
PLATFORM = linux_amd64
GOOS := $(word 1, $(subst _, ,$(PLATFORM)))

上⾯的例⼦⽤到了两个函数:word和subst。word函数有两个参数,1和subst函数的输出。subst函数将 PLATFORM变量值中的_替换成空格(替换后的PLATFORM值为linux amd64)。word函数取linux amd64 字符串中的第⼀个单词。所以最后GOOS的值为linux。

Makefile预定义函数能够帮助我们实现很多强⼤的功能,在编写Makefile的过程中,如果有功能需求,可以 优先使⽤这些函数。如果你想使⽤这些函数,那就需要知道有哪些函数,以及它们实现的功能。

常⽤的函数包括下⾯这些,你需要先有个印象,以后⽤到时再来查看。

image-20240102104652442

image-20240102104715542

image-20240102104725050

工作流

实际使⽤过程中,Makefile工作流分为如下两步:

  1. 编写Makefile⽂件,指定整个项⽬的编译规则
  2. 执行Linux make命令解析该Makefile⽂件,⾃动化编译管理项目。

默认情况下,make命令会在当前⽬录下,按照GNUmakefilemakefileMakefile(具体文件名取决于系统)⽂件的顺序查找 Makefile⽂件,⼀旦找到,就开始读取这个⽂件并执⾏。

⼤多数的make都⽀持makefileMakefile两种⽂件名,建议使⽤Makefile,容易辨别。

make也⽀持 -f --file 参数来指定其他⽂件名,⽐如 make -f golang.mk 或者 make –file golang.mk 。

第⼀步,先编写⼀个hello.c⽂件。

1
2
3
4
5
6
#include <stdio.h>
int main()
{
    printf("Hello World!\n");
    return 0;
}

第⼆步,在当前⽬录下,编写Makefile⽂件。

1
2
3
4
5
6
hello: hello.o
gcc -o hello hello.o
hello.o: hello.c
gcc -c hello.c
clean:
rm hello.o

第三步,执⾏make,产⽣可执⾏⽂件。

1
2
3
4
5
$ make
gcc -c hello.c
gcc -o hello hello.o
$ ls
hello hello.c hello.o Makefile

上⾯的⽰例Makefile⽂件有两个target,分别是hello和hello.o,每个target都指定了构建command。当执⾏make命令时,发现hello、hello.o⽂件不存在,就会执⾏command命令⽣成target。

第四步,不更新任何⽂件,再次执⾏make。

1
2
$ make
make: 'hello' is up to date.

当target存在,并且prerequisites都不⽐target新时,不会执⾏对应的command。

第五步,更新hello.c,并再次执⾏make。

1
2
3
4
$ touch hello.c
$ make
gcc -c hello.c
gcc -o hello hello.o

当target存在,但 prerequisites ⽐ target 新时,会重新执⾏对应的command。

第六步,清理编译中间⽂件。

Makefile⼀般都会有⼀个clean伪⽬标,⽤来清理编译中间产物,或者对源码⽬录做⼀些定制化的清理:

1
2
$ make clean
rm hello.o

我们可以在规则中使⽤通配符,make ⽀持三个通配符:*,?和~,例如:

1
2
3
objects = *.o
print: *.c
rm *.

引⼊其他Makefile

Makefile除了规则、语法之外,还有很多特性,⽐如可以引⼊其他Makefile、⾃动⽣成依赖关系、⽂件搜索等等。这里重点解释一下如何引入其他Makefile,以及这么做带来的收益。

Makefile要结构化层次化,这⼀点可以通过在项⽬根⽬录下的Makefile中引⼊其他Makefile来实现。

在Makefile中,我们可以通过关键字include,把别的makefile包含进来,类似于C语⾔的#include,被包含的⽂件会插⼊在当前的位置。例如:

1
2
include scripts/make-rules/common.mk
include scripts/make-rules/golang.mk

include也可以包含通配符include scripts/make-rules/*。make命令会按下⾯的顺序查找makefile ⽂件:

  1. 如果是绝对或相对路径,就直接根据路径include进来。
  2. 如果make执⾏时,有-I或–include-dir参数,那么make就会在这个参数所指定的⽬录下去找。
  3. 如果⽬录/include(⼀般是/usr/local/bin或/usr/include)存在的话,make也会去 找。

如果有⽂件没有找到,make会⽣成⼀条警告信息,但不会⻢上出现致命错误,⽽是继续载⼊其他的⽂件。 ⼀旦完成makefile的读取,make会再重试这些没有找到或是不能读取的⽂件。如果还是不⾏,make才会出现⼀条致命错误信息。如果你想让make忽略那些⽆法读取的⽂件继续执⾏,可以在include前加⼀个减 号-,如-include 。

References

https://opensource.com/article/18/8/what-how-makefile

《跟我⼀起写 Makefile》 (PDF 重制版)

我的玫瑰,种在繁星中的一颗~
Built with Hugo
主题 StackJimmy 设计