Go Makefile使用
Makefile 简介
Makefile 是一个工程文件的编译规则,描述了整个工程的编译和链接等规则,这些规则里包含了这些内容:
- 工程中的哪些源文件需要编译,以及如何编译;
- 需要创建哪些库文件,以及如何创建;
- 如何最终生成我们想要的可执行文件。
默认情况下,make 命令会在当前目录下按如下顺序查找 Makefile 文件:“GNUmakefile”、“makefile”、“Makefile”的文件,一旦找到,就开始读取这个文件并执行。
建议使用“Makefile”文件名,因为这个文件名第一个字符大写,这样有一种显目的感觉。还有一些 make 只对全小写的“makefile”文件名敏感。大多数的 make 都支持“makefile”和“Makefile”这两种默认文件名。make 也支持-f 和–file 参数来指定其它文件名,比如:make -f golang.mk 或者 make –file golang.mk。
如何学习 Makefile
上面,我们简单介绍了 Makefile。那么如何学习 Makefile 呢?要回答这个问题,我们先来看一下 Makefile 的组成部分,Makefile 脚本文件内容由以下三部分组成:
- 一系列规则来指定源文件编译的先后顺序。规则是 makefile 中的重要概念,它一般由目标、依赖和命令组成。
- 特定的语法规则,支持变量、函数和函数调用等。
- 操作系统中的各种命令。
学习 Makefile,其实也就是对这 3 个部分的学习,分别对应于:
- Makefile 规则
- Makefile 语法
- Shell 脚本、Linux 命令等
Makefile 的规则
|
|
-
target:可以是一个 object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。对于标签这种特性,在后续的“伪目标”章节中会有叙述。
-
prerequisites:生成该 target 所依赖的文件和/或 target
-
command:该 target 要执行的命令(任意的 shell 命令)
这是一个文件的依赖关系,也就是说,target 这一个或多个的目标文件依赖于 prerequisites 中的文件,其生成规则定义在 command 中。说白一点就是说:
prerequisites 中如果有一个以上的文件比 target 文件要新的话,command 所定义的命令就会被执行。
make 的工作方式
- 读入所有的 Makefile。
- 读入被 include 的其它 Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
伪目标
GO 项目下 Makefile 的管理能力都是通过伪目标来实现的,要执行的功能在 Makefile 中以伪目标的形式存在。
在上面的 Makefile 示例中,我们定义了一个 clean 目标,这个其实是一个伪目标,也就是说我们不会为该目标生成任何文件。因为伪目标不是文件,make 无法生成它的依赖关系和决定是否要执行它,通常我们需要显式地指明这个目标为伪目标。为了避免和文件重名,在 Makefile 中可以使用.PHONY 来标识一个目标为伪目标:
|
|
伪目标可以有依赖文件,也可以作为“默认目标”,例如:
|
|
因为伪目标总是会被执行,所以其依赖总是会被决议,通过这种方式,可以达到同时执行所有依赖项的目的。
Makefile 语法
书写命令
每条规则中的命令和操作系统 Shell 的命令行是一致的。make 会一按顺序一条一条的执行命令,每条命令的开头必须以 Tab
键开头,除非,命令是紧跟在依赖规则后面的分号后的。
#
是注释符
显示命令
|
|
如果 make 执行时,带入 make 参数 -n
或 --just-print
,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的 Makefile,看看我们书写的命令是执行起来是什么样子的或是什么顺序的。
而 make 参数 -s
或 --silent
或 --quiet
则是全面禁止命令的显示。
命令执行
如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。
|
|
命令出错
如果一个规则中的某个命令出错了(命令退出码非零),那么 make 就会终止执行当前规则,这将有可能终止所有规则的执行。
给 make 加上 -i
或是 --ignore-errors
参数,那么,Makefile 中所有命令都会忽略错误。
还有一个要提一下的 make 的参数的是 -k
或是 --keep-going
,这个参数的意思是,如果某规则中的命令出错了,那么就终止该规则的执行,但继续执行其它规则。
可以通过在命令行前加-符号,来让 make 忽略命令的出错,比如
|
|
引入其它的 Makefile
|
|
变量赋值
通过变量声明来声明一个变量,变量在声明时需要赋予一个初值,比如:GO=go
,引用变量时可以通过$()
或者${}
方式引用,建议用$()
方式引用变量:$(GO)
,也建议整个 makefile 的变量引用方式要保持一致。变量会像 bash 变量一样,在使用它的地方展开。
Makefile 中一共有 4 种变量赋值方法。
- = 最基本的赋值方法。
|
|
- := 直接赋值,赋予当前位置的值。
|
|
-
?= 表示如果该变量没有被赋值,则赋予等号后的值。
-
+= 表示将等号后面的值追加到前面的变量上。
多行变量
|
|
环境变量
Makefile 也像 Linux 一样支持环境变量,在 Makefile 中有 2 种环境变量:Makefile 预定义的环境变量和自定义的环境变量。
其中自定义的环境变量可以覆盖 Makefile 预定义的环境变量。默认情况下 Makefile 中定义的环境变量只在当前 Makefile 有效,如果想向下层传递(Makefile 中调用另一个 Makefile),需要使用 export 关键字来声明,如下声明了一个环境变量,并可以在下层 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 的路径,以空格隔开 |
条件语句
语法:
|
|
其中 <conditional-directive>
表示条件关键字,如 ifeq
。这个关键字有四个,如下
关键字 | 释义 |
---|---|
ifeq | 相等 |
ifneq | 不相等 |
ifdef | 不为空 |
ifndef | 为空 |
示例
|
|
函数
函数定义
|
|
例如,如下是一个自定义函数:
|
|
define 本质上是定义一个多行变量,可以在 call 的作用下当作函数来使用,在其它位置使用只能作为多行变量的使用,例如:
|
|
预定义函数
make 编译器也定义了很多函数,这些函数叫做预定义函数,调用语法和变量类似,语法为:
|
|
或者
|
|
<function>
是函数名,<arguments>
是函数参数,参数间以逗号(,)分割。函数的参数也可以为变量。
常用的函数,罗列如下:
函数名 | 功能描述 |
---|---|
$(origin |
告诉变量的“出生情况”,有如下返回值:
|
$(addsuffix |
把后缀 |
$(addprefix |
把前缀 |
$(wildcard |
扩展通配符,例如:$(wildcard ${ROOT_DIR}/build/docker/*) |
$(word |
取字符串 |
$(subst |
把字串 |
$(eval |
将 |
$(firstword |
取字符串 |
$(lastword |
取字符串 |
$(abspath |
将 |
$(shell cat foo) | 执行操作系统命令,并返回操作结果 |
$(info <text …>) | 输出一段信息 |
$(warning <text …>) | 出一段警告信息,而 make 继续执行 |
$(error <text …>) | 产生一个致命的错误,<text …> 是错误信息 |
$(filter <pattern…>, |
以 |
$(filter-out <pattern…>, |
以 |
$(dir <names…>) | 从文件名序列 |
$(notdir <names…>) | 从文件名序列 |
$(strip |
去掉 |
$(suffix <names…>) | 从文件名序列 |
$(foreach ,
|
把参数
|
Makefile 常见管理内容
- 静态代码检查(lint):推荐用 golangci-lint。
- 单元测试(test):运行 go test ./…。
- 编译(build):编译源码,支持不同的平台,不同的 CPU 架构。
- 镜像打包和发布(image/image.push):现在的系统比较推荐用 Docker/Kubernetes 进行部署,所以一般也要有镜像构建功能。
- 清理(clean):清理临时文件或者编译后的产物。
- 代码生成(gen):比如要编译生成 protobuf pb.go 文件。
- 部署(deploy,可选):一键部署功能,方便测试。
- 发布(release):发布功能,比如:发布到 Docker Hub、github 等。
- 帮助(help):告诉 Makefile 有哪些功能,如何执行这些功能。
- 版权声明(add-copyright):如果是开源项目,可能需要在每个文件中添加版权头,这可以通过 Makefile 来添加。
- API 文档(swagger):如果使用 swagger 来生成 API 文档,这可以通过 Makefile 来生成。
参考
https://github.com/marmotedu/geekbang-go/tree/master/makefile
https://seisman.github.io/how-to-write-makefile/Makefile.pdf