make

make 是构建大型项目的首选方案。

Makefile

Makefile 基本格式:

1
2
3
target ... : prerequisites ...
command
...

  • target - 目标,目标通常是文件名,Make 命令所要构建的对象。
  • prerequisites - 前置条件,通常是一组文件名,之间用空格分隔。
  • command - 命令,生成目标所需要执行的命令,由一行或多行的 shell 命令组成,是构建 target 的具体指令。

Makefile 中的命令必须以 [tab] 开头

target

一个 target 构成一条规则。target 可以是一个文件名,也可以是多个文件名,之间用空格分隔。

运行 make 时没有指定目标,默认会执行 Makefile 文件的第一个目标。

伪目标

当 target 是某个操作的名字时,被称为伪目标(phony target)。

1
2
clean:
rm -f apiserver

clean 就是伪目标,做一些清理操作。

1
make clean

如果当前目录中,存在一个文件叫做 clean,那么 clean 操作不会执行。因为 Make 任务 clean 已经存在,不需要重新构建。

这个时候可以声明 clean 是”伪目标”:

1
2
3
.PHONY: clean
clean:
rm -f apiserver

声明”伪目标”之后, make 就不会检查 clean 文件是否存在。

.PHONY: 后面可以声明一个或多个伪目标,之间用空格分隔。

prerequisites

prerequisites 指定了 target 是否重新构建的判断标准:只要有一个前置文件不存在,或者有过更新,target 就需要重新构建。

1
2
3
4
5
6
.PHONY: buildimg
buildimg: apisevergo
cp apisevergo /apisevergo
apisevergo:
./build_binary.sh

上面的示例,buildimg 的前置条件是 apisevergo。如果 apisevergo 已经存在,那么 make buildimg 可以正常运行,
否则必须再写一条规则,来生成 apisevergo

1
2
make buildimg
make buildimg

上面连续执行两次 make buildimg。第一次执行会先生成 apisevergo,然后再执行 cp apisevergo /apisevergo
第二次执行,make 发现 apisevergo 存在而且没有变动,就不会执行任何操作。

command

command 是构建 target 的具体指令,它的运行结果通常就是生成目标文件。

命令必须以 [tab] 开头,可以用内置变量 .RECIPEPREFIX 声明,来使用其他键。

1
2
3
.RECIPEPREFIX = >
all:
> echo Hello, world

.RECIPEPREFIX指定了 > 替代 tab 键。

每行命令在一个单独的 shell 中执行。也就是说 shell 之间没有继承关系

1
2
3
var-lost:
export foo=bar
echo "foo=[$$foo]"

执行 var-lost 取不到 foo 的值。因为两行命令在两个不同的进程执行。有三种解决办法:

  1. 将两行命令写在一行,中间用分号分隔。
  2. 在换行符前加反斜杠转义。
  3. 使用 .ONESHELL
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 1
    var-kept:
    export foo=bar; echo "foo=[$$foo]"
    # 2
    var-kept2:
    export foo=bar; \
    echo "foo=[$$foo]"
    # 3
    .ONESHELL:
    var-kept3:
    export foo=bar;
    echo "foo=[$$foo]"

语法

# 表示注释。
@ 放在行首,表示不打印此行。默认情况下会打印每条命令。

通配符

通配符(wildcard)用来指定一组符合条件的文件名。与 Bash 一致,主要有星号 *?[...]

1
2
3
# *.log 表示所有后缀名为 log 的文件。
clean:
rm -f *.log

匹配符

可以对文件名,进行类似正则运算的匹配,主要用到的匹配符是 %。可以将大量同类型的文件,只用一条规则就完成构建。

1
2
3
4
5
%.o: %.c
# 等同于
f1.o: f1.c
f2.o: f2.c

变量和赋值符

1
2
3
4
5
6
7
# 使用等号自定义变量
txt = Hello World
test:
@echo $(txt) # 调用自定义变量需要放在 `$( )` 之中
test:
@echo $$HOME # 调用 shell 变量时,变量前需要多加一个 `$`,这样才会对对 `$` 符号转义

赋值

1
2
3
4
5
6
7
8
9
10
11
VARIABLE = value
# 在执行时扩展,允许递归扩展。
VARIABLE := value
# 在定义时扩展。
VARIABLE ?= value
# 只有在该变量为空时才设置值。
VARIABLE += value
# 将值追加到变量的尾端。

内置变量
内置变量,如 $(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的 make 工具。
内置变量

自动变量
make 的自动变量与当前规则有关。

  • $@ - 指代当前构建的那个目标。比如,make foo$@ 就指代 foo
  • $< - 指代第一个前置条件。比如,t: p1 p2,那么 $< 就指代 p1
  • $? - 指代比目标更新的所有前置条件,之间以空格分隔。比如,t: p1 p2p2 的时间戳比 t 新,$? 就指代 p2
  • $^ - 指代所有前置条件,之间以空格分隔。比如,t: p1 p2$? 就指代 p1 p2
  • $* - 指代匹配符 % 匹配的部分, 比如 % 匹配 f1.txt 中的 f1$* 就表示 f1
  • $(@D)$(@F) - 分别指向 $@ 的目录名和文件名。比如,$@src/input.c,那么 $(@D)src$(@F)
    值为 input.c
  • $(<D)$(<F) - 分别指向 $< 的目录名和文件名。

控制语句
控制语句使用 Bash 语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
LIST = one two three
all:
for i in $(LIST); do \
echo $$i; \
done
# 等同于
all:
for i in one two three; do \
echo $i; \
done

函数
函数格式:$(function arguments) 或者 ${function arguments}

常用内置函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# shell 函数用来执行 shell 命令
srcfiles := $(shell echo src/{00..99}.txt)
# wildcard 函数替换 Bash 的通配符
srcfiles := $(wildcard src/*.txt)
# subst 函数用来文本替换
$(subst from,to,text)
$(subst ee,EE,feet on the street) # "feet on the street"替换成"fEEt on the strEEt"
# patsubst 函数用于模式匹配的替换
$(patsubst pattern,replacement,text)
$(patsubst %.c,%.o,x.c.c bar.c) # 将文件名"x.c.c bar.c",替换成"x.c.o bar.o"
# 替换后缀名
# 替换后缀名函数的写法是:变量名 + 冒号 + 后缀名替换规则
min: $(OUTPUT:.js=.min.js) # 将变量 OUTPUT 中的后缀名 .js 全部替换成 .min.js 。

include

include 可以引用其他 makefile。include 不能以 [tab] 开头。

makefile 入口文件,引用 build 目录下的 makefile。

1
include build/Makefile

构建 Go 项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
SHELL := /bin/bash
BASEDIR = $(shell pwd)
PROXY = http://web-proxy:8080
NAME = apiservergo
IMAGE_NAME=installer-backend
VERSION=0.1.2
.PHONY: all
all: proxy build
.PHONY: build
build: module lint govet
@echo "build binary: $(NAME) .."
@go build -o $(NAME) ./app
.PHONY: image
image: docker-build
@echo "build image: $(IMAGE_NAME):$(VERSION) .."
@docker build --rm --no-cache \
--label com.example.image-version=$(VERSION) \
--label com.example.image-name=$(IMAGE_NAME) \
--label com.example.itom.installer-backend=$(VERSION) \
--label vendor="Example International plc" \
-t localhost:5000/installer-backend:$(VERSION) .
.PHONY: docker-build
docker-build:
@echo "make sure the docker is installed"
@echo "docker build binary: $(NAME) .."
@docker run --rm -v $(BASEDIR)/app:/suite-installer-go/app \
-v $(BASEDIR)/library:/suite-installer-go/library \
-v $(BASEDIR)/build.sh:/suite-installer-go/build.sh \
-w /suite-installer-go \
docker.example.net/example/golang-fips:1.0.0 \
sh build.sh
.PHONY: proxy
proxy:
@echo "set http proxy .."
@export http_proxy=$(PROXY)
@export https_proxy=$(PROXY)
.PHONY: module
module: go.mod
@echo "download dependencies .."
@go mod tidy
.PHONY: lint
lint: golint
@echo "run go lint .."
@golint
.PHONY: golint
golint:
@echo "install golint .."
@go get -u golang.org/x/lint/golint
@go install golang.org/x/lint/golint
.PHONY: govet
govet:
@echo "run go vet .."
@go vet ./app
.PHONY: test
test:
@echo "run go test .."
.PHONY: benchmark
benchmark:
@echo "run go test benchmark .."
go.mod:
@echo "init module .."
@go mod init suite-installer-go
clean:
@echo "clean .."
@rm -rf apiservergo
docker system prune
.PHONY: help
help:
@echo "make - compile the source code"
@echo "make image - build image"
@echo "make docker-build - compile the source code with docker"
@echo "make lint - run go lint"
@echo "make vet - run go vet"
@echo "make clean - remove binary file and prune image"

build.sh 的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/bin/sh
# Proxy
export http_proxy=http://web-proxy.net:8080
export https_proxy=http://web-proxy.net:8080
# Enable go module
export GO111MODULE=on
# Install tools
apk --update add gcc git musl-dev
# Download dependencies, remove unused dependencies
go mod tidy
# Fix clinet-go unsupport go.mod
go get k8s.io/client-go@kubernetes-1.15.0
# Fix github.com/ugorji/go import issue
go get github.com/ugorji/go@v1.1.2-0.20180831062425-e253f1f20942
# Build the renewCert
echo "Build apiservergo ..."
CGO_ENABLED=0 go build -a -o apiservergo ./app/main.go
echo "Build apiservergo completed."

参考链接