前言

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

Make 最初是为 C、C++项目的编译、构建服务的,因此有很多为C、C++的特性,但是这些特性在 Go 开发中使用不到。毕竟每个人的时间有限,我们就只学习Go 开发中 Make 使用所必需的知识。Make 的规则都在 Makefile 文件编写上,我们后面就一起来学习 Makefile 的编写吧!

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

规则

Makefile 的规则很简单,分为三个部分:

1
2
3
target : prerequisites
    command
    ...

target:目标,可以是一个文件,或者是一个标签 label

prerequisites:targets 依赖的东西, 可以是文件,也可以依赖其他 target

command:shell 命令(开头是 Tab 键)

  1. 如果 prerequisites 比 target 要新,就会 执行 command 命令 (已存在的文件比不存在的文件新)

  2. 如果 prerequisites 中存在其他 target,对该 target 重复第一条

举个例子:

1
2
hello: hello.go
	go build hello.go

如果 hello 文件不存在,hello.go 肯定比不存在的 hello 新,执行命令 go build hello.go

如果 hello 文件存在,但是 hello.go 又有了新的修改,hello.go 也比 hello 新,执行 go build hello.go

如果 hello 文件存在,且 hello.go 没有改动,不执行任何操作

再举个例子:

1
2
3
4
5
hello: hello.go
	go build hello.go

world: hello world.go
	go build world.go

hello 是个 target ,world 也是个 target,且 world 依赖 hello。

如果执行 world 这个 target,会有如下两种情形:

world 不存在,遍历 prerequisites

  1. hello 是个 target,走一遍例一描述的流程

    如果 hello 文件不存在,执行命令 go build hello.go,此时生成了 hello,比world新

    如果 hello 文件存在,但是 hello.go 又有了新的修改,执行 go build hello.go,,此时生成了 hello,比 world 新

    如果 hello 文件存在,且 hello.go 没有改动,继续判断 world.go

  2. world.go 是个普通文件,比不存在的 world 新

  3. world 的依赖中,存在有比 world 新的,因此执行 command

world 存在,遍历 prerequisites

  1. hello 是个 target,走一遍例一描述的流程

    如果 hello 文件不存在,那么就会执行命令 go build hello.go此时 hello 比 world 新了

    如果 hello 文件存在,但是 hello.go 又有了新的修改,也会执行 go build hello.go此时 hello 比 world 新

    如果 hello 文件存在,且 hello.go 没有改动,不执行任何操作

  2. world.go 是个普通文件,如果修改过,那么比 world 新,否则不如 world 新

  3. 判断 prerequisites 中的 hello 或者 world.go 是否存在比 world 新的,如果有,执行command;没有的话不执行任何操作

多目标

有可能我们会有多个 target 规则类似,我们可以将它们合并起来:

1
2
3
.PHONY: target1 target2
target1 target2: prerequisites
	command

等同于

1
2
3
4
5
6
.PHONY: target1 target2
target1: prerequisites
	command

target2: prerequisites
	command

伪目标

上面说过,target 不一定是一个文件,也可能是一个标签,因为有些操作,我们不一定会生成文件,比如清理一些中间文件,运行单测等,这种 target 我们把它叫做伪目标。

1
2
3
4
5
6
7
8
hello: hello.go
	go build hello.go

world: hello world.go
	go build world.go

clean:
	rm hello world

clean 就是一个伪目标,只是一个标签,运行这个伪目标,需要显式地指定这个目标,即 make clean

1
2
➜   make clean 
rm hello world

但是有可能会有文件和我们的伪目标名称重名,这样我们运行 make clean 的时候就达不到我们的目的了。

1
2
3
4
# 如果有重名文件存在,make clean 就不会运行
➜  touch clean
➜  make clean 
make: `clean' is up to date.

为了防止这样的情况,我们可以使用 “.PHONY” 显式地声明我们的 target 是个 伪目标,这样即使有重名文件在,也不会影响 target 的运行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
hello: hello.go
	go build hello.go

world: hello world.go
	go build world.go

# 显式声明 clean 是个伪目标
.PHONY: clean
clean:
	rm hello world
1
2
3
4
5
6
➜   make world
go build hello.go
go build world.go
➜   touch clean
➜   make clean 
rm hello world

运行

上面我们其实已经运行过 makefile 了,这里我们再来简单看下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
hello: hello.go
	go build hello.go

world: hello world.go
	go build world.go

# 显式声明 clean 是个伪目标
.PHONY: clean
clean:
	rm hello world

在默认的方式下,也就是我们只输入 make 命令。那么,

  1. make 会在当前目录下找名字叫 “Makefile” 或 “makefile” 的文件。
  2. 如果找到,它会找文件中的第一个 target,运行该 target,并打印相应的日志。

或者我们可以输入 make + target ,强制运行某个 target,比如输入 make world

1
2
3
4
5
6
➜   make
go build hello.go
➜  makefile_study make world
go build world.go
➜   make clean 
rm hello world

引用 Makefile

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

1
include filenames…
  • filename 可以是绝对路径或者当前目录下的文件;如果 filename 是空,不会进行引用也不会报错。
  • include 前面可以有多个空格,但是一定不能Tab
  • include 和 filename 直接使用空格分隔;多个 filename 之间也使用空格分隔
  • filename 中可以使用变量

举个例子,如果你当前文件夹下有三个 makefile 文件,分别叫做 a.mk、b.mk、c.mk,那么如下引用

1
include foo *.mk 

相当于

1
include foo a.mk b.mk c.mk

Makefile 在处理 include 时,遇到一个 include 文件就读取该文件内容,读取完后再处理下一个 include 文件。

如果一个文件没有指定路径,会在当前路径下寻找,如果没找到,还会按照如下顺序寻找:

  1. 运行 Make 命令时使用 ‘-I’ 或 ‘–include-dir’ 参数指定的路径
  2. 如果如下目录存在的话,会挨个寻找。prefix/include(一般是/usr/local/include) 、/usr/gnu/include、/usr/local/include、 /usr/include

在处理 include 时,如果一个 include 的文件没有找到,程序不会终止,只会打印一条警告信息,然后继续处理。当读取完整个 Makefile 文件后,程序再去重试处理这些没有找到的文件,如果还是处理失败,会产生一个致命错误。如果想忽略文件不存在带来的报错,可以在 include 前面加上一个减号 ‘-',表示忽略引用错误。

1
-include filenames…

我们一般都是有一个主文件 Makefile 对外暴露,其他的引用文件被主文件 Makefile 调用,类似函数调用。一个 target 调用 另一个 target,可以直接使用 $(MAKE)+target

a.mk

1
2
3
.PHONY: a_func
a_func:
	echo "this is a_func"

Makefile

1
2
3
4
5
include a.mk

.PHONY: func
func:
	$(MKAKE) a_func  # 调用 a.mk 中的 a_func target

总结

本文主要介绍了 Makefile 的运行机制,分为以下几个方面:

  • 规则:依据 prerequisites 是否比 target新,决定是否执行 command
  • 多目标:多个 target 的 prerequisites 和 command类似时,使用多目标
  • 伪目标:target 不是一个目标文件
  • 运行:如何运行 Make
  • 引用:引用其他 Makefile 文件

更多

微信公众号:CodePlayer