目录:网上冲浪指南

如何编写 Makefile

2022/03/13

经过一段时间的使用,我决定将 Makefile 加入到我的必备技术列表。这篇文章将向你介绍 Makefile 的基本编写方法,以及一个在现代前端项目中的使用样例。相信你在看完之后,就会像我一样爱上 Makefile,虽然某些时候它不是最好的方案,但在你没有更好的方案时,它一定不会让你失望。

make 有很多种实现,在撰写本文时,参考的是 GNU make 的相关文档。

配方

Makefile 中最基本的构成元素就是配方(Recipe),配方的作用就是用来说明如何构建软件某个产物(Target),每个配方都由三个元素组成:target、prerequisites、commands,一个简单的配方如下:

dist: node_modules # target: prerequisites
	npm run build    # commands
	npm run tsc

这个配方描述了构建 dist 目录(target)的方式:在构建之前,make 需要确保它的依赖 node_modules (prerequisites)已经被构建且更新了,然后再执行两个 npm 命令(commands)来构建产物。

配方的 target 也可以不是事实存在于硬盘上的文件,例如:

test: node_modules
	npm run test

这个配方描述了执行测试的方式:先更新 node_moidules ,然后执行测试脚本,整过程不会产生并不会产生名为 test 的文件。

配方的 prerequisites 也跟 target 类似,除了可以是实际存在的文件之外,还可以是其他的 target,例如:

dist: node_modules test
	npm run test

新的用来构建 dist 的配方将会同时依赖 node_modulestest ,这样在我们执行 make dist 之前,测试脚本也会被执行了。

配方之间的依赖关系

有了 target 跟 prerequisites,make 就可以分析各个配方之间的依赖关系,进而可以确定不同配方的执行先后顺序,更进一步,如果你的 target、prerequisites 是文件的话,make 可以通过文件的最后修改时间来找出已经过时的产物,从而实现比较精准的增量构建。

通常,我会这么来编写一个现代的前端项目 Makefile:

node_modules: package.json yarn.lock
	yarn install --fronzen-lockfile

test: node_modules tests
	yarn test

types: src node_modules
	yarn tsc

dist: src node_modules test types
	yarn build

同样是执行 make dist ,对于初次上手的开发者来说,make 会为他还原好 node_modules,并且自动执行测试,测试通过后再构建 dist 目录。而对于经常在这个项目中摸爬滚打的熟手,只要没有更新项目的依赖,make 就会为他自动的跳过还原 node_modules 的步骤,而要在 npm scripts 中实现类似的效果是非常困难的。

变量

Makefile 中可以声明变量,变量可以使用在配方中的任何地方:

foo := 123 (1)

$(foo): (2)
	echo $(foo) (3)
1 声明变量 foo
2 使用变量 foo 的值作为 target
3 在命令中使用 foo 变量的值

make 在解析配方之前,会将 Makefile 中形如 $(var) 的文本进行变量展开,所以与上面命令等价的是 shell 脚本是 echo 123 而不是 echo $foo 。因为 make 的这个特点,当我们想要在配方中使用 $ 时,需要用 $$ 来替换。

环境变量

make 默认会继承当前系统的全部环境变量,并将这些变量载入到 Makefile 中,例如:

build:
	echo $(PATH) (1)
1 $(PATH) 将会被替换为当前的系统环境变量 PATH 的值
Output
$ make
echo /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl

而在 Makefile 中声明的自定义变量则不会在命令中以环境变量的形式存在:

foo := hello

build:
	echo $$foo
Output
$ make
echo $foo

如果想要在命令中通过环境变量的方式来访问自定义变量,可以在变量声明前加上 `export `。

export foo := hello

build:
	echo $$foo
Output
$ make
echo $foo
hello

变量覆盖

在执行 make 命令时,我们可以使用下面的语法通过命令行来覆盖自定义变量的值,一个常见的使用场景是这样的:

ENV := prod

build:
	yarn build --$(ENV)
Output
$ make
yarn build --prod

$make ENV=dev (1)
yarn build --dev
1 通过命令行参数来覆盖 Makefile 中定义的 ENV 变量。
通过命令行设置的变量同样也可以作为环境变量在配方的命令中使用。

书写命令

我们已经大致了解了配方的编写方法,现在让我们更进一步地来看看 make 执行命令的方式,make 会根据以下的变量来解析执行配方中的命令:

变量名 说明 默认值

.RECIPEPREFIX

单个字符,使用这个字符开头的行将被识别为配方的命令

Tab

SHELL

命令解释器的路径,用来执行配方中的命令

/bin/sh

.SHELLFLAGS

命令解释器的参数

-c

make 将所有以 .RECIPEPREFIX 开头的行识别为命令,接着逐行调用 SHELL 来执行,当执行出错的时候,make 就会中断配方的执行。

make 在执行命令之前会输出将要执行的语句,如果你不希望 make 打印命令语句,可以在命令之前加上 @

build:
	echo hello
	@echo world
Output
$ make
echo hello
hello
world
build:
	foo=123
	echo $$foo

@ 类似的记号还有 - ,表示允许这行命令出错。

build:
	echo hello
	-exit 23
	echo world
Output
$ make
echo hello
hello
exit 23
make: [Makefile:3: build] Error 23 (ignored)
echo world
world

执行多行命令的脚本

make 逐行解释并执行的命令的方式在大多数情况下还是很方便的,但如果你准备使用 sh 以外的命令解释器,例如,Python、nodejs,你就需要让 make 能够一次性解释并执行多行命令,这时你只需要声明一个名为 .ONESHELL 的 target 即可。

.ONESHELL:
SHELL = /bin/sh
.SHELLFLAGS = -ec (1)

build:
	foo=32
	echo $$foo (2)
1 配方中的脚本不再被逐行解释执行,所以需要设置 sh 在遇到命令错误的时候自动退出
2 现在可以在命令块中声明并使用 shell 变量了,因为这些语句将在同一个 sh 调用中执行
Output
foo=32
echo $foo
32
设置 .ONESHELL 之后,@ 修饰符只能出现在整个命令块的开头

一些特殊的内建配方

到目前为止,关于 Makefile 的基本知识已经展现我们的眼前,除了这些基本通用的语法规则之外,GNU make 还有一些内置的配方,可以很方便的帮助我们实现常用的功能:

名称 功能

PHONY

将其 prerequisites 标记为伪目标,make 将始终重新构建这些目标,无论其是否需要更新

IGNORE

当 make 在构建其 prerequisites 的时候,忽略构建错误

内容导航

本网站所展示的文章由 Zeeko Zhu 采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可

Zeeko's blog, Powered by ASP.NET Core 🐳