Makefile用于工程管理
规则
每条规则中的命令和操作系统Shell的命令行是一致的。
make会按顺序一条一条的执行命令,命令如果单起一行,必须以Tab
开头,如果不单起一行,用分号;
规则分开
内置变量
$@
目标文件集$(@D)
目标文件路径的目录部分$%
仅当目标文件是一个静态库文件时,代表一个静态库的一个成员名$<
第一个依赖文件名$?
所有比目标文件新的依赖文件列表。 如果目标是静态库文件,代表库成员。$^
所有依赖文件列表$+
所有依赖文件列表,保留了依赖文件中重复出现的文件$*
目标模式中%代表的部分(“茎”),文件名中存在目录时,也包含目录部分$(*F)
目标“茎”中的文件名部分$(*D)
目标”茎”中的目录部分-
用在command前面,忽略命令执行时的错误。如果发生错误,继续执行makefile$$$$
随机编号vpath
选择性搜索,为符合pattern的文件指定搜索路径dir,vpath pattern dirCURDIR
make的工作目录-
语法
``` 空格 在makefile中不要为了美观而进行缩进,或者使用空格间隔变量,空格是makefile语法的一部分 := 定义直接展开式变量 ?= 如果变量未定义则赋予后面的值,否则什么都不作 = 定义递归展开式变量 @ 规则的命令行以@开头,表示不打印出该条命令 unexport 不将变量传递给子makefile export: 当一个变量使用export进行声明后,变量和它的值将被加入到当前工作的环境变量中,以后make执行的所有规则的命令都可以使用这个变量。 SHELL 特殊变量,默认传递给子makefile,注意该变量没有使用系统环境变量中的定义,GNUmake默认值为/bin/sh MAKEFLAGS 最上层make的命令行选项会被自动的通过环境变量MAKEFLAGS传递给子make进程。 MAKEFILES make执行时首先将此变量的值作为需要读入的Makefile文件 MAKECMDGOALS 记录命令行参数指定的终极目标列表 VPATH 依赖文件搜索路径,用”:”分隔 多目标规则 一个文件可以作为作为多个规则的目标(多个规则中只能有一个规则定义命令)。以这个文件为目标的规则的所有依赖文件将会被合并成此目标的一个依赖文件列表。 .PHONY:name 将name声明为伪目标,伪目标指的是不真正生成目标文件,只是为了执行后面的指令 .SILENT 创建.SILENT依赖列表中的文件时,不打印重建这些文件时使用的命令,例如 .SILENT : all $()和${} 取出变量值 $$ 表示一个$符号,因为$在makefile中具有特殊含义,所以要使用$字符时,需要使用$$,类似于C语言中的转义字符 override 不使用命令行中定义的同名变量替代该变量,对使用override定义的变量追加值时,也需要使用override define 多行定义,如下varname是变量名,value1 value2是分行写的value的组成部分,以endef结束
define varname
value1
value2
endef
ifeq 判断关键字是否相等,四种格式:
ifeq (ARG1, ARG2)
ifeq 'ARG1' 'ARG2
ifeq "ARG1" 'ARG2'
ifeq 'ARG1' "ARG2"
shell command 返回command命令在shell中执行的结果(类似于shell脚本中的
command
),注意大小写的含义不同,大写的SHELL是一个特殊变量 export 将变量添加到当前工作环境,传递给子makefile,不覆盖子makefile中的同名变量(除非是-e选项)。没有使用export的变量(除了一些特殊变量)不传递给子makefile 单行命令与多行命令的区别makefile的规则的命令行中,每一行命令在一个独立的子shell进程中被执行。
上一行中使用的cd命令不会改变下一行命令的工作目录。可以使用\将一行命令多行排版(与C语言中的\符号作用类似)
.d文件 GNU组织建议为每一个“name.c”的文件都生成一个“name.d’的文件,存放.c文件的依赖关系 %.d: %.c @set -e; rm -f $@; \ gcc -MM $< > $@.; \ sed ‘s/($).o[ :]\1.o $@ :/g’ < $@. > $@; \ rm -f $@.
FORCE 没有依靠关系也没有命令的规则被认为总是新的,如下面的FORCE clean:FORCE rm *.o FORCE:
include make暂停读取当前的Makefile,转去读取include指定的一个或者多个文件,完成以后继续当前Makefile的读取
<a name="78162069"></a>
## 内置函数
origin $(origin VARIABLE) 查询变量VARIABLE的出处 返回值: 1 undefined 2 default 3 environment 4 environment override 5 file 6 command line 7 override 8 automatic if $(if CONDITION,THEN-PART[,ELSE-PART]) filter-out $(filter-out PATTERN…,TEXT) 保留TEXT中不符合PATTERN的内容 filter $(filter PATTERN…,TEXT) 取出TEXT中符合PATTERN的内容 dir 取目录函数 notdir 取文件名函数 subst $(subst from,to,text) 把text中的from替换成to call $(call VARIABLE,PARAM,PARAM), 参数将会依次赋值给临时变量$(1) $(2)等 firstword firstword NAMES 取首单词函数 wildcard $(wildcard PATTERN) 获取当前目录下符合模式PATTERN的文件名 patsubst $(patsubst PATTERN,REPLACEMENT, TEXT) 把TEXT中以空格分隔的、符合模式PATTERN单词替换为REPLACEMENT error 产生致命错误, $(error Text) $(VAR:x=y) 把var中符合%x的替换成%y strip 去掉行首和行尾的空字符和行中重复的空字符 findstring $(findstring FIND,IN) 在IN中存在FIND返回FIND,否则返回空 foreach $(foreach var,list.text) 展开var和list,不展开text,依次把list中空格分割的值赋给var,然后执行text。类似于for循环。 eval $(eval XX) eval将XX展开一次,Makefile会将XX再次展开一次。 sort $(sort XX XX XX) 排序和去掉重复的内容 words $(words, XX XX) 统计单词个数
<a name="8a8dece5"></a>
## 控制结构
- ifeq
ifeq(XXX,XXX) command else command endif
> 注意command前面不要有TAB键!
- ifneq: 同ifeq
- ifdef:
- ifndef:
<a name="d52a120d"></a>
## 不依赖任何文件
$(CRUDIR)/Makefile Makefile: ;
<a name="BV9LP"></a>
# Go与Makefile
精简的 Makefile,用于简化构建和管理用 Go 编写的 Web 服务器。
- 高级,简单的命令。比如
- compile
- start
- stop
- watch
- 管理具体项目环境的变量,它应该包含 `.env` 文件
- 开发模式,修改时自动编译
- 开发模式,修改时自动重启服务
- 开发模式,简洁地显示编译的错误信息
- 具体项目的 GOPATH,以使我可以在 `vendor` 目录维护依赖包
- 简化文件查看,比如 `make watch run="go test ./..."`
在此文件结构中键入 make 命令将提供以下输出:
```bash
$ make
Choose a command run in my-web-server:
install Install missing dependencies. Runs `go get` internally.
start Start in development mode. Auto-starts when code changes.
stop Stop development mode.
compile Compile the binary.
watch Run given command when code changes. e.g; make watch run="go test ./..."
exec Run given command, wrapped with custom GOPATH. e.g; make exec run="go test ./..."
clean Clean build files. Runs `go clean` internally.
环境变量导入
首先,我们希望在 Makefile
中 include 我们为项目定义的环境变量,所以,第一行如下:
include .env
在具体项目的环境变量文件的头部,我们将定义这些:项目名,Go 目录/文件,进程 id 的路径…
PROJECTNAME=$(shell basename "$(PWD)")
# Go related variables.
GOBASE=$(shell pwd)
GOPATH=$(GOBASE)/vendor:$(GOBASE)
GOBIN=$(GOBASE)/bin
GOFILES=$(wildcard *.go)
GOPROXY="https://goproxy.cn"
# Redirect error output to a file, so we can show it in development mode.
STDERR=/tmp/.$(PROJECTNAME)-stderr.txt
# PID file will store the server process id when it's running on development mode
PID=/tmp/.$(PROJECTNAME)-api-server.pid
# Make is verbose in Linux. Make it silent.
MAKEFLAGS += --silent
默认情况下make打印构建目标所执行的命令。如果要禁止打印,可以使用make的选项—silent
在 Makefile 文件的其余部分,我们将使用特别的 GOPATH 变量。我们所有的命令都应该包含项目特定的 GOPATH,否则它们将无法工作。这为我们的 Go 项目提供了明确的隔离,并带来了一些复杂性。为了简化操作,我们可以添加一个 exec
命令,该命令执行任何给定的命令,并使用上面定义的自定义 GOPATH。
## exec: Run given command, wrapped with custom GOPATH. e.g; make exec run="go test ./..."
exec:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) $(run)
但这并不是很高级的做法。我们应该用简单的命令来介绍一些常见的情况,如果我们正在做 Makefile 未涵盖的事情,那么只能使用 exec
。
开发模式
开发模式应该是这样:
- 清空构建缓存
- 编译代码
- 后台执行服务
- 当代码被修改时,重复上面步骤
我们将同时运行服务器和文件观察程序。我们需要确保在开始新流程之前正确停止服务,并且也不会破坏常见的命令行行为,例如在按下 Control-C 或 Control-D 时停止。
# start 解决的问题
# 后台编译与执行服务
# 主进程不在后台执行,我们可以随时使用 Control-C 中断它;
# 当主进程中断时,停止后台进程,为此,我们需要 trap
# 当代码修改时,重新编译与重启服务
start:
bash -c "trap 'make stop' EXIT; $(MAKE) compile start-server watch run='make compile start-server'"
stop: stop-server
编译
compile
命令不仅仅是在后台调用 go compile
; 它还清理错误输出并打印简化版本。
以下是在命令行中进行重大更改的方法:
compile:
@-touch $(STDERR)
@-rm $(STDERR)
@-$(MAKE) -s go-compile 2> $(STDERR)
@cat $(STDERR) | sed -e '1s/.*/\nError:\n/' | sed 's/make\[.*/ /' | sed "/^/s/^/ /" 1>&2
开启/停止服务
start-server
基本上运行它在后台编译的二进制文件,将其 PID 保存到临时文件中。stop-server
读取 PID 并在需要时终止进程。
start-server:
@echo " > $(PROJECTNAME) is available at $(ADDR)"
@-$(GOBIN)/$(PROJECTNAME) 2>&1 & echo $$! > $(PID)
@cat $(PID) | sed "/^/s/^/ \> PID: /"
stop-server:
@-touch $(PID)
@-kill `cat $(PID)` 2> /dev/null || true
@-rm $(PID)
restart-server: stop-server start-server
观察变化
我们需要一个文件观察器来观察变化。我尝试了很多并且感到不满意,所以最终创建了我自己的文件观察工具 yolo 。通过下面命令安装在您的系统中
$ go get github.com/azer/yolo
一旦安装完毕,我们基本上可以开始观察项目目录中的更改,排除像 vendor
或者 bin
这样的目录,如下:
## watch: Run given command when code changes. e.g; make watch run="echo 'hey'"
watch:
@yolo -i . -e vendor -e bin -c $(run)
现在我们得到一个 watch
命令,它在项目目录中以递归方式监视更改,不包括 vendor
目录。我们可以直接传递我们想要的任何运行命令。例如,start
命令基本上在代码更改时调用 make compile start-server
:
make watch run="make compile start-server"
我们可以用它来运行测试,或自动检查是否有任何竞争条件。将为执行设置环境变量,因此您根本不必担心 GOPATH:
make watch run="go test ./..."
关于 Yolo 的一个好处是它的网络界面。如果启用它,您可以立即在 Web 界面中看到命令的输出。您只需要传递 -a
选项来启用它:
yolo -i . -e vendor -e bin -c "go run foobar.go" -a localhost:9001
然后,您可以在浏览器中打开 localhost:9001
并立即开始在浏览器中查看结果
安装依赖
当我们在代码中进行更改时,我们希望在编译之前下载缺少的依赖项。install
命令将为我们完成这项工作;
install: go-get
我们在文件改动时,编译之前自动调用 install
, 让依赖包可以自动安装,如果您想手动安装依赖项,你可以执行:
make install get="github.com/foo/bar"
在内部,这个命令会转换成:
$ GOPATH=~/my-web-server GOBIN=~/my-web-server/bin go get github.com/foo/bar
Go命令
如果我们想设置 GOPATH 到项目的目录,简化依赖管理(在 Go 生态中还没正式解决的问题),就需要在 Makefile 中封装 Go 命令。
go-compile: go-clean go-get go-build
go-build:
@echo " > Building binary..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build -o $(GOBIN)/$(PROJECTNAME) $(GOFILES)
go-generate:
@echo " > Generating dependency files..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go generate $(generate)
go-get:
@echo " > Checking if there is any missing dependencies..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go get $(get)
go-install:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go install $(GOFILES)
go-clean:
@echo " > Cleaning build cache"
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go clean
帮助
最后,我们需要一个 help 命令来查看可用命令的概述。我们可以使用自动生成优雅的格式的命令 sed
与 column
, 如下:
help: Makefile
@echo " Choose a command run in "$(PROJECTNAME)":"
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
这个命令会基本地扫描 Makefile 中以 ##
开头的文本行并输出它们。所以,你可以简单的注释你所定义的命令,这些命令会被 help
命令打印出来。
比如我们添加如下的注释:
## install: Install missing dependencies. Runs `go get` internally.
install: go-get
## start: Start in development mode. Auto-starts when code changes.
start:
## stop: Stop development mode.
stop: stop-server
我们可以执行:
$ make help
Choose a command run in my-web-server:
install Install missing dependencies. Runs `go get` internally.
start Start in development mode. Auto-starts when code changes.
stop Stop development mode.
例子
include .env
PROJECTNAME=$(shell basename "$(PWD)")
# Go related variables.
GOBASE=$(shell pwd)
GOPATH="$(GOBASE)/vendor:$(GOBASE)
GOBIN=$(GOBASE)/bin
GOFILES=$(wildcard *.go)
# Redirect error output to a file, so we can show it in development mode.
STDERR=/tmp/.$(PROJECTNAME)-stderr.txt
# PID file will keep the process id of the server
PID=/tmp/.$(PROJECTNAME).pid
# Make is verbose in Linux. Make it silent.
MAKEFLAGS += --silent
## install: Install missing dependencies. Runs `go get` internally. e.g; make install get=github.com/foo/bar
install: go-get
## start: Start in development mode. Auto-starts when code changes.
start:
bash -c "trap 'make stop' EXIT; $(MAKE) compile start-server watch run='make compile start-server'"
## stop: Stop development mode.
stop: stop-server
start-server: stop-server
@echo " > $(PROJECTNAME) is available at $(ADDR)"
@-$(GOBIN)/$(PROJECTNAME) 2>&1 & echo $$! > $(PID)
@cat $(PID) | sed "/^/s/^/ \> PID: /"
stop-server:
@-touch $(PID)
@-kill `cat $(PID)` 2> /dev/null || true
@-rm $(PID)
## watch: Run given command when code changes. e.g; make watch run="echo 'hey'"
watch:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) yolo -i . -e vendor -e bin -c "$(run)"
restart-server: stop-server start-server
## compile: Compile the binary.
compile:
@-touch $(STDERR)
@-rm $(STDERR)
@-$(MAKE) -s go-compile 2> $(STDERR)
@cat $(STDERR) | sed -e '1s/.*/\nError:\n/' | sed 's/make\[.*/ /' | sed "/^/s/^/ /" 1>&2
## exec: Run given command, wrapped with custom GOPATH. e.g; make exec run="go test ./..."
exec:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) $(run)
## clean: Clean build files. Runs `go clean` internally.
clean:
@(MAKEFILE) go-clean
go-compile: go-clean go-get go-build
go-build:
@echo " > Building binary..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build -o $(GOBIN)/$(PROJECTNAME) $(GOFILES)
go-generate:
@echo " > Generating dependency files..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go generate $(generate)
go-get:
@echo " > Checking if there is any missing dependencies..."
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go get $(get)
go-install:
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go install $(GOFILES)
go-clean:
@echo " > Cleaning build cache"
@GOPATH=$(GOPATH) GOBIN=$(GOBIN) go clean
.PHONY: help
all: help
help: Makefile
@echo
@echo " Choose a command run in "$(PROJECTNAME)":"
@echo
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
@echo