前言

在 Go 语言开发中,我们希望能够规范代码风格,每个成员在提交时可以一键格式化,同时检查是否有语法错误;我们希望能够一键运行单测,生成单测报告;我们希望能够一键编译、打包、发布项目,这就需要使用到 Make。Make 有很多种,我们常用的就是 GUN Make,有了 Make,我们将极大的提高项目开发、测试、发布的效率。

Make 最初是为 C、C++项目的编译、构建服务的,因此有很多为C、C++的特性,但是这些特性在 Go 开发中使用不到。毕竟每个人的时间有限,我们就只学习Go 开发中 Make 使用所必需的知识。

Make 的规则都在 Makefile 文件编写上,本篇文章,我们来学习 Makefile 命令和变量相关知识。

make 命令在 Windows 下不支持运行,需要在 Linux 或 Mac 环境下运行

命令

命令显示

运行 target 时,执行命令的同时,默认会输出命令到控制台

1
2
3
4
.PHONY: echo_test
echo_test:
	echo "hello"
	echo "world"
1
2
3
4
5
➜   make echo_test   
echo "hello"
hello
echo "world"
world

如果在命令前加上一个 @ 符号,则只执行命令,不显示命令

1
2
3
4
.PHONY: echo_test
echo_test:
	@echo "hello" # 这个命令不会显示
	echo "world"
1
2
3
4
➜   make echo_test
hello
echo "world"
world

@符号只对单个命令生效,如果想要对所有命令都生效,可以使用 -s 或者 --silent 参数,表示沉默:

1
2
3
➜   make -s echo_test 
hello
world

如果想要只显示命令,不执行命令,使用如下参数:make -n 或者 make –just-print

1
2
3
4
5
# .PHONY 表示这是一个伪目标
.PHONY: echo_test
echo_test:
	echo "hello"
	@echo "world"
1
2
3
4
➜ make -n echo_test
echo "hello"
echo "world"

命令执行

如果想要后一个命令基于前一个命令,需要在命令之间加上分号;而命令写在两行是单独执行的:

1
2
3
exec:
	cd /tmp
	pwd
1
2
➜   make -s exec
/Users/admin/makefile_study

通过上面的例子,可以看到第二个命令执行时,并没有在 /tmp 目录,说明前一个命令并没有影响到后一个命令。

我们把两个命令使用分号分隔

1
2
exec:
	cd /tmp; pwd
1
2
➜   make -s exec
/tmp

命令出错

在我们执行命令时,有可能命令会出错,影响后续命令的执行。对于有些错误,我们可以忽略,让命令继续执行下去。比如我们想新建个文件夹,如果文件夹本来就存在,新建就会出错,但我们需要的就是文件夹存在,这种错误可以忽略。

当前目录 test 文件夹不存在,新建文件夹和文件,并显示文件列表

1
2
exec:
	@mkdir test; cd test; touch a.txt; ls

第一次运行正常

1
2
3
➜ make exec
a.txt

第二次运行就会提示错误,但是同一行的命令会继续执行下去

1
2
3
4
➜ make exec
mkdir: test: File exists
a.txt

如果存在多行命令的话,前面一行的命令报错,后面的命令就不会继续执行了

1
2
3
exec:
	@mkdir test
	@echo "hello world"
1
2
3
➜ make exec
mkdir: test: File exists
make: *** [exec] Error 1

如果我们想忽略这个报错的话,可以有多种方式:

  1. 在命令前面加上一个减号 ‘-’ ,表示忽略这个错误,继续执行下面的命令
1
2
3
exec:
	-@mkdir test
	@echo "hello world"
1
2
3
4
➜ make exec
mkdir: test: File exists
make: [exec] Error 1 (ignored)
hello world
  1. 对规则 加上 .IGNORE 标记,会忽略该规则中所有命令的错误
1
2
3
4
.IGNORE: exec
exec:
	@mkdir test
	@echo "hello world"
1
2
3
4
➜ make exec
mkdir: test: File exists
make: [exec] Error 1 (ignored)
hello world
  1. make 中加上 -i 或者 --ignore-errors 参数,那么此次运行的所有命令都会忽略错误
1
2
3
exec:
	@mkdir test
	@echo "hello world"
1
2
3
4
5
➜ make -i exec
mkdir: test: File exists
make: [exec] Error 1 (ignored)
hello world

定义命令模板

有时候可能有一些常用的功能,我们可以将其抽出来做成一个模板,供其他command 调用。定义模板的语法为:define ,接模板的名字;中间是命令,最后以 endef 结尾。

1
2
3
define template-name
...commands...
endef

使用命令模板,就像使用普通变量一样:

1
2
3
4
5
6
7
define my_template
@echo "this is a template"
endef

.PHONY: test
test: 
   $(my_template)
1
2
➜  make test 
this is a template

自动变量

使用命令模板,经常需要使用自动常量,这种变量和 shell 中的 位置参数变量 类似,常用的有这几个:

1
2
3
4
$@  表示目标文件
$^  表示所有的依赖文件
$<  表示第一个依赖文件
$?  表示比目标还要新的依赖文件列表
1
2
3
4
5
6
.PHONY: test
test: hello.go world.go
	@echo $@ # test
	@echo $^ # hello.go world.go
	@echo $< # hello.go
	@echo $? # hello.go world.go

我们就定义一个模板,输出 target 和 所有的 prerequisites,然后再使用这个模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
define my_template
@echo $@ $^
endef

.PHONY: hello
hello: hello.go
	$(my_template)

.PHONY: world
world: world.go
	$(my_template)
1
2
3
4
5
➜  make hello
hello hello.go
➜  make world
world world.go

变量

变量的声明和使用

和其他编程语言一样,Makefile 也可以定义变量,在 target、prerequisites、command中都可以使用。变量名由字母、数字和下划线组成(可以是数字开头),大小写敏感。

定义变量时,变量名 和 值 之间使用 “=” 连接,"=" 两侧空格不敏感;使用变量时,可以使用 $+变量名的形式,更推荐的方式还是使用 $(变量名) 将变量包裹起来。

1
2
3
4
5
name= tom
age = 18
info:
	echo $name
	echo $(age)
1
2
3
4
5
➜   make info
echo tom
tom
echo 18
18

可以将变量使用在 target 中:

1
2
3
4
MyTarget = info
$(MyTarget):
	go build hello.go
	go build world.go
1
2
3
➜  make info
go build hello.go
go build world.go

也可以将变量使用在 prerequisite 中:

1
2
3
files = hello.go
info: $(files)
	go build $(files)
1
2
➜ make info
go build hello.go

Makefile 中的变量,会在使用的时候 展开,直接使用你定义的值来替代,你完全可以这样使用这样的骚操作,但是并不推荐:

1
2
3
4
5
6
7
8
9
type = go
build:clean
	go build hello.$(type)
	go build world.$(type)


.PHONY: clean
clean:
	-@rm -f hello world
1
2
3
➜  make build
go build hello.go
go build world.go

使用其他变量

我们可以一个变量中使用其他变量

1
2
3
4
5
6
name = tom
age = 18
detail = $(name) $(age)

info:
	@echo $(detail)
1
2
➜  make info
tom 18

在我们平常编程过程中,被引用的变量,一定要定义在前面,但是在 Makefile 中没有这个限制,你可以引用后面定义的变量(也可以理解为懒加载,只有用到时才会展开,此时整个文件已经都加载完了,因此不在乎使用的变量定义在前或者在后):

1
2
3
4
5
6
detail = $(name) $(age)
name = tom
age = 18

info:
	@echo $(detail)
1
2
➜  make info
tom 18

如果可以引用之后的变量,就可能出现递归的情况,比如下面的示例,a 引用 b,b 引用 a,当然 makefile 是会检测到这种递归的。

1
2
3
4
5
a = $(b)
b = $(a)

info:
	@echo $(b)
1
2
3
➜  make info
Makefile:4: *** Recursive variable `b' references itself (eventually).  Stop.

懒加载

上面也提到过,Makefile 中的变量是懒加载的,只有在使用的时候,才会将变量的值展开,也就是说后面定义的变量值会将之前的覆盖:

1
2
3
4
5
6
7
name = tom
age = 18
detail = $(name) $(age)

info:
	@echo $(detail)
age = 22
1
2
➜  make info
tom 22  # 被后面的值覆盖了

当然,我们可以强制指定当前的变量值,即使用 " := " 来定义变量:

1
2
3
4
5
6
7
8
9
name = tom
age = 18
detail := $(name) $(age)

.PHONY: info
info:
	@echo $(detail)

age = 22
1
2
➜ make info
tom 18 # 使用的是定义detail 时 age 的值

如果当前没有变量值,展开就是空值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
name = tom

# 此时还没有定义 age 变量
detail := $(name) $(age)  

.PHONY: info
info:
	@echo $(detail)

age = 22
1
2
3
➜  make info
tom # age 是空值

追加变量值

我们可以对已有的变量进行修改,重新赋值或者追加,我们可以使用 += 来追加:

1
2
3
4
5
6
name = tom
name += john

.PHONY: info
info:
	@echo $(name)
1
2
3
➜  make info
tom john

需要注意的是,追加时,变量的赋值方式会延续初次定义的符号,使用 = 或者 := 来保持是否需要懒加载

1
2
name = tom
name += john  # 等同于 name = tom john
1
2
name := tom
name += john  # 等同于 name := tom john

因此在引用其他变量时就需要注意:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
name = tom

# 等同于 name = tom $(a_name)
name += $(a_name)

.PHONY: info
info:
	@echo $(name)  # tom john

a_name = john

上述例子保持了懒加载,而下面这个则不会使用懒加载:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
name := tom

# 等同于 name := tom $(a_name)
name += $(a_name)

.PHONY: info
info:
	@echo $(name) # tom

a_name = john

总结

本篇文章我们一起学习了Makefile 命令和变量相关的知识点,主要有:

  • 命令显示:使用 @ 符号控制命令是否打印
  • 命令执行:使用 符号使得后一个命令基于前一个命令执行
  • 命令出错:使用 - 符号来忽略命令执行中产生的错误
  • 位置变量:可以获取命令中的参数,结合命令模板使用会更加易用
  • 变量使用:包括变量的声明、使用以及懒加载机制

更多

微信公众号:CodePlayer