在团队多人合作开发golang工程时,我们经常会遇到下面的问题:
- 线上运行的应用程序版本号对应工程代码的哪个分支,哪个commit
- 线上运行的二进制文件?上线的服务是使用golang的哪个版本编译的?
- A引入了bouk/staticfiles等工具将工程目录下的文件嵌入到二进制程序中,B如何方便的在修改文件后同步更新asset文件?
- 如何不口口相传的告知团队成员如何编译工程中众多的应用?
要解决上述的问题,我们需要一个构建脚本/工具来自动化的在开发、持续集成、预发布阶段提供下列功能:
- 提供无学习成本的简单命令完成编译(make build)、嵌入文件(make asset)、代码生成(make gen)、本地执行(make run)、单元测试(make test)、清理(make clean)、制作镜像(make image)等诸多动作;
- 在构建开始前能检查各种依赖的工具/环境是否符合条件,例如:golang的版本,是否安装了revive代码静态扫描工具,是否安装了符合条件的docker版本等等;
- 跨平台支持以符合团队成员的各种开发环境;
- 编译过程中自动的将git的commit/branch/tag、编译的时间、golang的版本、os等信息嵌入程序中;
不幸的是,golang官方以及社区目前并没有一个类似java世界中的maven/gradle
,rust世界中的cargo
,c/c 世界中的cmake
等工具来支持上述的诉求:
- bash脚本跨平台不友好;
- bazel不好用我也不无脑粉google神教;
- maven/gradle的golang插件对没有java经验的团队成员学习成本高;
跟随社区大流,使用gnu/make以及Makefile来做为当下golang工程的构建工具似乎是一个最佳选择。但是Makefile的编写同样有不小的学习成本,因此,在这里我将经过多个大小工程的全套Makefile分享给大家。
工程文件结构
工程整体结构如下:
首先,使用go mod init example.com/group/repo初始化golang工程后,添加.gitignore文件,设置忽略output目录下的所有文件,该目录在工程编译后输出不同平台可执行文件以及单元测试后输出单元测试报告,这些内容无需添加到git中。
主Makefile文件
根目录下Makefile内容如下:
代码语言:txt复制.PHONY: all
all: lint test build
# ==============================================================================
# Build Options
ROOT_PACKAGE=github.com/choujimmy/gomakefile
VERSION_PACKAGE=$(ROOT_PACKAGE)/pkg/app/version
# ==============================================================================
# Includes
include build/lib/common.mk
include build/lib/golang.mk
include build/lib/image.mk
# ==============================================================================
# Targets
## build: Build source code for host platform.
.PHONY: build
build:
@$(MAKE) go.build
## build.all: Build source code for all platforms.
.PHONY: build.all
build.all:
@$(MAKE) go.build.all
## image: Build docker images and push to registry.
.PHONY: image
image:
@$(MAKE) image.push
## clean: Remove all files that are created by building.
.PHONY: clean
clean:
@$(MAKE) go.clean
## lint: Check syntax and styling of go sources.
.PHONY: lint
lint:
@$(MAKE) go.lint
## test: Run unit test.
.PHONY: test
test:
@$(MAKE) go.test
## help: Show this help info.
.PHONY: help
help: Makefile
@echo -e "nUsage: make <OPTIONS> ... <TARGETS>nnTargets:"
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
其中,ROOT_PACKAGE变量修改为实际的工程模块导入包名,每个make的target上使用两个##号开头的注释内容供help的target解析生成make的帮助说明,实际效果如下:
代码语言:txt复制$ make help
Usage: make <OPTIONS> ... <TARGETS>
Targets:
build Build source code for host platform.
build.all Build source code for all platforms.
image Build docker images and push to registry.
clean Remove all files that are created by building.
lint Check syntax and styling of go sources.
test Run unit test.
help Show this help info.
common.mk
build/lib/common.mk
文件内容如下:
# ==============================================================================
# Makefile helper functions for common
#
SHELL := /bin/bash
COMMON_SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
ifeq ($(origin ROOT_DIR),undefined)
ROOT_DIR := $(abspath $(shell cd $(COMMON_SELF_DIR)/../.. && pwd -P))
endif
ifeq ($(origin OUTPUT_DIR),undefined)
OUTPUT_DIR := $(ROOT_DIR)/output
endif
ifeq ($(origin TOOLS_DIR),undefined)
TOOLS_DIR := $(OUTPUT_DIR)/tools
endif
ifeq ($(origin TMP_DIR),undefined)
TMP_DIR := $(OUTPUT_DIR)/tmp
endif
# set the version number. you should not need to do this
# for the majority of scenarios.
ifeq ($(origin VERSION), undefined)
VERSION := $(shell git describe --dirty --always --tags | sed 's/-/./2' | sed 's/-/./2' )
endif
export VERSION
COMMA := ,
SPACE :=
SPACE =
该文件作为依赖包含在根目录下的Makefile
文件中,定义了工程通用的路径变量以及根据git describe --dirty --always --tags | sed 's/-/./2' | sed 's/-/./2'
命名的结果获取工程的git代码库状态作为工程的版本信息。
golang.mk
build/lib/golang.mk
文件内容如下:
# ==============================================================================
# Makefile helper functions for golang
#
GO := go
GO_SUPPORTED_VERSIONS ?= 1.11|1.12
GO_LDFLAGS = -X $(VERSION_PACKAGE).GitVersion=$(VERSION) -X $(VERSION_PACKAGE).BuildDate=$(shell date -u '%Y-%m-%dT%H:%M:%SZ')
ifeq ($(GOOS),windows)
GO_OUT_EXT := .exe
endif
ifeq ($(ROOT_PACKAGE),)
$(error the variable ROOT_PACKAGE must be set prior to including golang.mk)
endif
ifeq ($(origin PLATFORM), undefined)
ifeq ($(origin GOOS), undefined)
GOOS := $(shell go env GOOS)
endif
ifeq ($(origin GOARCH), undefined)
GOARCH := $(shell go env GOARCH)
endif
PLATFORM := $(GOOS)_$(GOARCH)
else
GOOS := $(word 1, $(subst _, ,$(PLATFORM)))
GOARCH := $(word 2, $(subst _, ,$(PLATFORM)))
endif
GOPATH := $(shell go env GOPATH)
ifeq ($(origin GOBIN), undefined)
GOBIN := $(GOPATH)/bin
endif
PLATFORMS ?= darwin_amd64 windows_amd64 linux_amd64
COMMANDS ?= $(wildcard ${ROOT_DIR}/cmd/*)
BINS ?= $(foreach cmd,${COMMANDS},$(notdir ${cmd}))
ifeq (${COMMANDS},)
$(error Could not determine COMMANDS, set ROOT_DIR or run in source dir)
endif
ifeq (${BINS},)
$(error Could not determine BINS, set ROOT_DIR or run in source dir)
endif
.PHONY: go.build.verify
go.build.verify:
ifneq ($(shell $(GO) version | grep -q -E 'bgo($(GO_SUPPORTED_VERSIONS))b' && echo 0 || echo 1), 0)
$(error unsupported go version. Please make install one of the following supported version: '$(GO_SUPPORTED_VERSIONS)')
endif
.PHONY: go.build.%
go.build.%:
$(eval COMMAND := $(word 2,$(subst ., ,$*)))
$(eval PLATFORM := $(word 1,$(subst ., ,$*)))
$(eval OS := $(word 1,$(subst _, ,$(PLATFORM))))
$(eval ARCH := $(word 2,$(subst _, ,$(PLATFORM))))
@echo "===========> Building binary $(COMMAND) $(VERSION) for $(OS) $(ARCH)"
@mkdir -p $(OUTPUT_DIR)/$(OS)/$(ARCH)
@CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) $(GO) build -o $(OUTPUT_DIR)/$(OS)/$(ARCH)/$(COMMAND)$(GO_OUT_EXT) -ldflags "$(GO_LDFLAGS)" $(ROOT_PACKAGE)/cmd/$(COMMAND)
.PHONY: go.build
go.build: go.build.verify $(addprefix go.build., $(addprefix $(PLATFORM)., $(BINS)))
.PHONY: go.build.all
go.build.all: go.build.verify $(foreach p,$(PLATFORMS),$(addprefix go.build., $(addprefix $(p)., $(BINS))))
.PHONY: go.clean
go.clean:
@echo "===========> Cleaning all build output"
@rm -rf $(OUTPUT_DIR)
.PHONY: go.lint.verify
go.lint.verify: go.build.verify
ifeq (,$(wildcard $(GOBIN)/revive))
@echo "===========> Installing revive"
@GO111MODULE=off $(GO) get -u github.com/mgechev/revive
endif
.PHONY: go.lint
go.lint: go.lint.verify
@echo "===========> Run revive to lint source codes"
@$(GOBIN)/revive -config $(ROOT_DIR)/build/linter/revive.toml
-exclude vendor/...
./...
.PHONY: go.test.verify
go.test.verify: go.build.verify
ifeq (,$(wildcard $(GOBIN)/go-junit-report))
@echo "===========> Installing go-junit-report"
@GO111MODULE=off $(GO) get -u github.com/jstemmer/go-junit-report
endif
.PHONY: go.test
go.test: go.test.verify
@echo "===========> Run unit test"
@mkdir -p $(OUTPUT_DIR)
@$(GO) test -count=1 -timeout=10m -short -v ./... 2>&1 | tee >($(GOBIN)/go-junit-report --set-exit-code >$(OUTPUT_DIR)/report.xml)
在这里我们定义了golang工程常用的编译、单元测试、代码检查等目标,其中编译包含:
make build
: 编译当目前操作系统系统目标的可执行文件make build.all
: 同时编译macos/windows/linux的64位可执行程序
在使用上,还支持指定options
,例如仅编译linux/amd64的可执行程序:
$ make build.all PLATFORMS="linux_amd64"
===========> Building binary app1 a6ac381 for linux amd64
===========> Building binary app2 a6ac381 for linux amd64
仅编译app1
的windows和linux的64位可执行程序:
$ make build.all PLATFORMS="linux_amd64 windows_amd64" BINS="app1"
===========> Building binary app1 a6ac381 for linux amd64
===========> Building binary app1 a6ac381 for windows amd64
使用注意:
- 必须按照golang工程建议的规范在根目录下的
cmd
目录下为每一个可执行程序建立单独包 - 使用
go module
作为依赖管理工具,仅支持golang的1.11
,1.12
版本 - 代码检查工具使用的是
revive
,且示例工程中的规则文件build/linter/revive.toml
中的规则非常严谨,各位看官自行修改
image.mk
build/lib/image.mk
文件内容如下:
# ==============================================================================
# Makefile helper functions for docker image
#
DOCKER := docker
DOCKER_SUPPORTED_VERSIONS ?= 17|18
REGISTRY_PREFIX ?= jimmychou
# Determine image files by looking into hack/docker/*.Dockerfile
IMAGE_FILES=$(wildcard ${ROOT_DIR}/build/docker/*.Dockerfile)
# Determine images names by stripping out the dir names
IMAGES=$(foreach image,${IMAGE_FILES},$(subst .Dockerfile,,$(notdir ${image})))
ifeq (${IMAGES},)
$(error Could not determine IMAGES, set ROOT_DIR or run in source dir)
endif
.PHONY: image.build.verify
image.build.verify:
ifneq ($(shell $(DOCKER) -v | grep -q -E 'bversion ($(DOCKER_SUPPORTED_VERSIONS))b' && echo 0 || echo 1), 0)
$(error unsupported docker version. Please make install one of the following supported version: '$(DOCKER_SUPPORTED_VERSIONS)')
endif
@echo "===========> Docker version verification passed"
.PHONY: image.build
image.build: image.build.verify go.build.verify $(addprefix image.build., $(IMAGES))
.PHONY: image.push
image.push: image.build.verify go.build.verify $(addprefix image.push., $(IMAGES))
.PHONY: image.build.%
image.build.%: go.build.linux_amd64.%
@echo "===========> Building $* $(VERSION) docker image"
@cat $(ROOT_DIR)/build/docker/$*.Dockerfile
| sed "s#{{REGISTRY_PREFIX}}#$(REGISTRY_PREFIX)#g" >tmp_$*.Dockerfile
@$(DOCKER) build --pull -t $(REGISTRY_PREFIX)/$*:$(VERSION) -f tmp_$*.Dockerfile .
@rm tmp_$*.Dockerfile
.PHONY: image.push.%
image.push.%: image.build.%
@echo "===========> Pushing $* $(VERSION) image to $(REGISTRY_PREFIX)"
@$(DOCKER) push $(REGISTRY_PREFIX)/$*:$(VERSION)
该文件提供对本地构建容器镜像以及推送容器镜像等的支持,在使用前请先修改REGISTRY_PREFIX
变量的值。
示例
该makefile
的全部文件以及工程示例已在github
上建立示范工程,地址: gomakefile