β

Makefiles for Golang

奇虎360-addops 3 阅读

make是一个功能强大的build工具,但学习成本也比较高。幸运的是Go的工具链已经帮我们做了很多事情,大大降低了编写Makefile的难度,这对于我来说是个好消息。

Why need Makefile

在Go中,完整的构建也很快,go install在安装前检查二进制文件是否为最新版本,go test也会做很多测试。那我们为什么还需要Makefile?

因为它是可执行文档。 它能描述如何build项目,运行什么样的测试以及项目依赖的外部工具等等。你可以在外部工具中获得依赖关系跟踪,在需要时并行build,生成简单的CI脚本。这样我们就可以丢掉shell脚本了。

Quick primer on make

Introduction

make 是一个构建工具,构建的过程会通常会写到一个叫Makefile文件中,放到项目的根目录下。

Makefiles通常由一些rules组成,像这样:

target: prerequisites
    recipe

target是要构建的文件名称,prerequisites是构建target时依赖的文件,recipe就是构建的具体过程了,通常是一些shell命令。

makefile核心: make会跟踪prerequisites中文件的最新修改时间,只要prerequisites有一个以上的文件比target文件新,recipe中的内容就会被执行

有一点必须要牢记,Makefile中的缩进必须是tab。如果Makefile使用空格或tab、空格混合组合当做缩进,会出现一些诡异的错误。虽然可以修改默认的缩进字符,但仍然建议使用tab。

simple example:

abc: xyz
    echo "abc" > abc

xyz:
    echo "xyz" > xyz

这个Makefile包含了2个rule,abc依赖于xyz。同时它们都有各自的recipe,达到各自的目标。

第一次运行结果如下:

$ make
echo "xyz" > xyz
echo "abc" > abc

这里我没有指定构建哪个target,make会默认选择第一个target,也就是这个例子中的abc。可以看到xyz是在abc之前运行的。

再次运行结果

$ make
make: 'abc' is up to date.

noting to do,xyz、abc文件已经存在。所有的依赖都被满足,不需要再做什么事了。

variables

make中的变量很简单,声明 key := vaule,引用 $(key)。traget、prerequisites、recipe三者都可以包含变量

simple example:

FILE := abc

$(FILE): xyz
    echo $(FILE) > "something"

xyz:
    echo "xyz" > xyz

Let's write Makefile

Running test

PKGS := $(shell go list ./ ... | grep -v /vendor)

.PHONY: test
test:
    go test $(PKGS)

这里有个叫test的rule,而.PHONY: test 显式声明test是个伪目标。意味着test每次都会运行,但不会生成文件。只要有这个声明,要运行test这个target,只能make test。

$(shell ...)是shell code,这个应该很熟悉了。

伪目标并不是一个文件,只是一个标签,由于伪目标不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显式地声明这个目标才能让其生效。当然,伪目标的取名不能和文件名重名,不然其就失去了伪目标的意义了。为了避免和文件重名的这种情况,我们可以使用一个特殊的标记.PHONY来显式地指 明一个目标是伪目标,向make说明,不管是否有这个文件,这个目标就是伪目标。

linting

PKGS := $(shell go list ./ ... | grep -v /vendor)

.PHONY: test
test:
    go test $(PKGS)

.PHONY: lint
lint:
    gometalinter ./ ... --vendor

lint这个rule执行了gometalinter命令

linting before testing

.PHONY: test
test: lint
    go test $(PKGS)

每次执行test前先运行lint,如果lint运行失败,test也同样失败。我们不需要记住细节,只要执行make即可。

lint Dependency management for external tools

gometalinter是一个外部工具,我们还没有在Makefile中描述如何获取到它,在gometalinter没有安装的情况下运行make会报错。我们会这么做

BIN_DIR := $(GOPATH)/bin
GOMETALINTER := $(BIN_DIR)/gometalinter

$(GOMETALINTER):
    go get -u github.com/alecthomas/gometalinter
    gometalinter --install &> /dev/null

上面的rule描述了gometalinter的安装过程

Ok,让我们再进一步

.PHONY: lint
lint: $(GOMETALINTER)
  gometalinter ./... --vendor

在lint这个rule在执行前,先进行gometalinter的安装。如果gometalinter的二进制文件在$GOPATH/bin目录下不存在,会进行重建。否则提示up-to-date,同时make会跳过$(GOMETALINTER)这个rule

看下现在的Makefile

PKGS := $(shell go list ./... | grep -v /vendor)

.PHONY: test
test: lint
    go test $(PKGS)

BIN_DIR := $(GOPATH)/bin
GOMETALINTER := $(BIN_DIR)/gometalinter

$(GOMETALINTER):
    go get -u github.com/alecthomas/gometalinter
    gometalinter --install &> /dev/null

.PHONY: lint
lint: $(GOMETALINTER)
    gometalinter ./... --vendor

依赖顺序是test -> lint -> $(GOMETALINTER)

看下第一次运行make的结果

$ make
go get -u github.com/alecthomas/gometalinter
gometalinter --install &> /dev/null
gometalinter ./... --vendor
go test github.com/sahilm/yamldiff
ok      github.com/sahilm/yamldiff  0.351s

可以看到,执行过程跟我们计划是一致的

再次运行

$ make
gometalinter ./... --vendor
go test github.com/sahilm/yamldiff
ok      github.com/sahilm/yamldiff  0.120s

这次gometalinter已经安装过,所以不需要再次安装。

Building in parallel

BINARY := mytool

.PHONY: windows
windows:
    mkdir -p release
    GOOS=windows GOARCH=amd64 go build -o release/$(BINARY)-v1.0.0-windows-amd64

.PHONY: linux
linux:
    mkdir -p release
    GOOS=linux GOARCH=amd64 go build -o release/$(BINARY)-v1.0.0-linux-amd64

.PHONY: darwin
darwin:
    mkdir -p release
    GOOS=darwin GOARCH=amd64 go build -o release/$(BINARY)-v1.0.0-darwin-amd64

.PHONY: release
release: windows linux darwin

内容不少,但到这我们应该都能看懂了。定义了3个rule来build不通平台的二进制文件,想让它们并行很简单,执行make命令时带-j参数,就像这样

看下运行结果

$ make release -j3
mkdir -p release
mkdir -p release
mkdir -p release
GOOS=linux GOARCH=amd64 go build -o release/mytool-v1.0.0-linux-amd64
GOOS=windows GOARCH=amd64 go build -o release/mytool-v1.0.0-windows-amd64
GOOS=darwin GOARCH=amd64 go build -o release/mytool-v1.0.0-darwin-amd64

Reducing duplication

我们利用 automatic variables 可以减少一些重复的编写。

BINARY := mytool

PLATFORMS := windows linux darwin

.PHONY: $(PLATFORMS)
$(PLATFORMS):
    mkdir -p release
    GOOS=$@ GOARCH=amd64 go build -o release/$(BINARY)-v1.0.0-$@-amd64

.PHONY: release
release: windows linux darwin

$@是指target的名称,如果是多个target,$@就是正在执行recipe的target名称。

Injecting values at build time

之前的版本号我们都是预先设置好的,如果我们想接受外部的版本号,应该怎么做

BINARY := mytool
VERSION ?= v1.0.0
PLATFORMS := windows linux darwin

.PHONY: $(PLATFORMS)
$(PLATFORMS):
    mkdir -p release
    GOOS=$@ GOARCH=amd64 go build -o release/$(BINARY)-$(VERSION)-$@-amd64

.PHONY: release
release: windows linux darwin

现在版本会有个默认值,但我们也可以用命令参数的形式替换这个值,就像这样

$ make VERSION=v2.0.0 release -j3
mkdir -p release
mkdir -p release
mkdir -p release
GOOS=linux GOARCH=amd64 go build -o release/mytool-v2.0.0-linux-amd64
GOOS=windows GOARCH=amd64 go build -o release/mytool-v2.0.0-windows-amd64
GOOS=darwin GOARCH=amd64 go build -o release/mytool-v2.0.0-darwin-amd64

最后完整的例子

PKGS := $(shell go list ./... | grep -v /vendor)

.PHONY: test
test: lint
    go test $(PKGS)

BIN_DIR := $(GOPATH)/bin
GOMETALINTER := $(BIN_DIR)/gometalinter

$(GOMETALINTER):
    go get -u github.com/alecthomas/gometalinter
    gometalinter --install &> /dev/null

.PHONY: lint
lint: $(GOMETALINTER)
    gometalinter ./... --vendor

BINARY := mytool
VERSION ?= v1.0.0
PLATFORMS := windows linux darwin

.PHONY: $(PLATFORMS)
$(PLATFORMS):
    mkdir -p release
    GOOS=$@ GOARCH=amd64 go build -o release/$(BINARY)-$(VERSION)-$@-amd64

.PHONY: release
release: windows linux darwin

Final

希望看完文章的人能有个好的开始。作为一个gopher,至少我们得看懂别人写的makefile,捂脸~

作者:奇虎360-addops
应用运维|运维开发|opsdev|addops|虚拟化|openstack|docker|容器化|k8s|智能运维
原文地址:Makefiles for Golang, 感谢原作者分享。

发表评论