在golang的开发学习过程中,能经常需要执行go test
测试、go build
编译、go install
安装操作,简单的项目构建起来很方便。但遇到较复杂的项目,比如要跨平台交叉编译,选择性单元测试,性能测试,命令就要附加很多参数,敲起来麻烦,还容易忘记,时常要查看help。为了解决这个问题,我们可以编写shell,将常用操作封装在脚本中,虽然实现简单,但每个人编写、阅读能力不一,不利于规范化。另一个更好的选择,就是makefile了。
make命令是GNU的工程化编译工具,用以实现工程化的管理,提高开发效率。
Make解释Makefile 中的指令(应该说是规则)。在Makefile文件中描述了整个工程所有文件的编译顺序、编译规则。Makefile 有自己的书写格式、关键字、函数。像C 语言有自己的格式、关键字和函数一样。而且在Makefile 中可以使用系统shell所提供的任何命令来完成想要的工作。
构建规则都写在Makefile文件里面,要学会如何Make命令,就必须学会如何编写Makefile文件。
Makefile文件由一系列规则(rules)构成。
每条规则要说明构建的依赖条件,及怎么样去构建。那么格式如下,
# rule
<target> : <prerequisites>
[tab] <commands>
target: 目标
prerequisites: 先决条件,或者说依赖条件
tab: 使用tab来缩进
command: 要执行的命令
一个目标,可以是文件名,也可以是某个操作的名字(伪目标),这个名字由自己定义,用来指明要构建的对象。
create:
touch newfile
比如上面这条规则,伪目标为create,命令作用为创建一个文件。要想构建这个操作,调用make create
即可。
但是如果目录下,存在一个文件名为create,那么构建命令就不会去执行。为了解决这个问题,当使用伪目标时,可以明确声明create是“伪目标“,告诉make跳过文件检查。
.PHONY: clean
create:
touch newfile
如果Make命令运行时没有指定目标,默认会执行Makefile文件的第一个目标。
先决条件,通常是文件名,多个名字用空格分隔。
它定义了一个是否进行重新构建的判断标准: 如果有任何一个先决文件发生改变(时间戳更新),就要重新构建。
result.txt: source.txt
cp source.txt result.txt
上面代码中,构建 result.txt 的前置条件是 source.txt 。如果当前目录中,source.txt 已经存在,那么make result.txt
可以正常运行,否则必须再写一条规则,来生成 source.txt 。
source.txt:
echo "this is the source" > source.txt
上面代码中,source.txt后面没有前置条件,就意味着它跟其他文件都无关,只要这个文件还不存在,每次调用make source.txt
,它都会生成。
$ make result.txt
$ make result.txt
上面命令连续执行两次make result.txt
。第一次执行会先新建 source.txt,然后再新建 result.txt。第二次执行,make发现 source.txt 没有变动(时间戳晚于 result.txt),就不会执行任何操作,result.txt 也不会重新生成。
如果需要生成多个文件,往往采用下面的写法。
source: file1 file2 file3
上面代码中,source 是一个伪目标,只有三个前置文件,没有任何对应的命令。
$ make source
执行make source
命令后,就会一次性生成 file1,file2,file3 三个文件。这比下面的写法要方便很多。
$ make file1
$ make file2
$ make file3
命令是构建目标时具体执行的指令,由一行或多行shell组成。每行命令之前必须有一个tab键缩进。如果想用其他键缩进,可以用内置变量.RECIPEPREFIX声明。
.RECIPEPREFIX = >
hello:
> echo Hello, world
需要注意的是,每行shell在一个单独的bash进程中执行,多进程间没有继承关系。
var:
export name=wangpeng
echo "myname is $name"
运行上面的构建 ,发现变量name是取不到的,因为两行shell在两个独立的bash中运行。
最直接的方法就是将两行shell写到一行中,
var:
export name=wangpeng; echo "myname is $name"
第二种办法,在换行前加反斜杠\转义,
var:
export name=wangpeng \
echo "myname is $name"
还有第三种办法是使用。ONESHELL
内置命令。
.ONESHELL:
var:
export name=wangpeng
echo "myname is $name"
行首井号(#)表示注释。
回显是指,在执行到每行命令前,将命令本身打印出来。
test:
# 这是测试
执行上面构建会输出
$ make test
# 这是测试
在命令的前面加上@,就可以关闭回声。
test:
@# 这是测试
这下构建时就不会有任何输出。
Makefile 的通配符与 Bash 一致,主要有星号()、问号(?).比如 .text 表示所有后缀名为text的文件。
Make命令允许对文件名,进行类似正则运算的匹配,主要用到的匹配符是%。比如,假定当前目录下有 f1.c 和 f2.c 两个源码文件,需要将它们编译为对应的对象文件。
%.o: %.c
等同于下面的写法。
f1.o: f1.c
f2.o: f2.c
使用匹配符%,可以将大量同类型的文件,只用一条规则就完成构建。
Makefile 允许自定义变量。
txt = Hello World
test:
@echo $(txt)
调用shell中的变量,需要使用两个美元符号$$。
Makefile一共提供了四个赋值运算符 (=、:=、?=、+=),它们的区别请看StackOverflow。
VARIABLE = value
# 在执行时扩展,允许递归扩展。
VARIABLE := value
# 在定义时扩展。
VARIABLE ?= value
# 只有在该变量为空时才设置值。
VARIABLE += value
# 将值追加到变量的尾端。
Make命令提供一系列内置变量,比如,$(CC)指向当前使用的编译器,$(MAKE) 指向当前使用的Make工具。这主要是为了跨平台的兼容性,详细的内置变量清单见手册。
output:
$(CC) -o output input.c
Makefile使用 Bash 语法,完成判断和循环。
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
上面代码判断当前编译器是否 gcc ,然后指定不同的库文件。
LIST = one two three
all:
for i in $(LIST); do \
echo $$i; \
done
# 等同于
all:
for i in one two three; do \
echo $i; \
done
上面代码的运行结果。
one
two
three
.PHONY: cleanall cleanobj cleandiff
cleanall: cleanobj cleandiff
rm all
cleanobj:
rm *.o
cleandiff:
rm *.diff
上面代码可以调用不同目标,删除不同后缀名的文件,也可以调用一个目标(cleanall),删除所有指定类型的文件。
BUILD_NAME:=goappname
BUILD_VERSION:=1.0
SOURCE:=*.go
LDFLAGS:=-ldflags "-X main.Version=${BUILD_VERSION}"
all: deps build install
deps:
#安装依赖
[ -x glide ] && glide install || yum install glide
test:
go test
build: test
go build -o ${BUILD_NAME} ${SOURCE}
build_linux: test
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ${BUILD_NAME} ${SOURCE}
build_win: test
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ${BUILD_NAME} ${SOURCE}
install: deps build
go install
#生成配置文件等
#cp app.conf.example /etc/app.conf
clean:
go clean
.PHONY: all deps test build build_linux build_win install clean
这样就很方便地通过一个make命令完成对项目的构建。