2020 年正确使用现代 makefile
如果你是一位经验丰富的开发人员,你可能对 Makefile 很了解。Makefile 是一种纯文本文件,用于定义软件编译规则,这在以前很常见。对吧?
今天我们将:
-
看看我的经历中遇到的三大误区,并证明它们是错误的
-
我们将看到当按预期使用时 make 会如何大放异彩。
误区一:
仅适用于 C、C++ 和本机软件
尽管 C/C++ 生态系统确实受到 make 的严重影响,但你可以用它做更多的事情。make 可以处理任何类型的文件,只要它有路径和时间戳。
典型例子:
edit : main.o kbd.o command.o | |
cc -o edit main.o kbd.o command.o | |
main.o : main.c defs.h | |
cc -c main.c | |
kbd.o : kbd.c defs.h command.h | |
cc -c kbd.c | |
command.o : command.c defs.h command.h | |
cc -c command.c | |
clean : | |
rm edit main.o kbd.o command.o display.o \ | |
insert.o search.o files.o utils.o |
edit : main.o kbd.o command.o | |
cc -o edit main.o kbd.o command.o | |
main.o : main.c defs.h | |
cc -c main.c | |
kbd.o : kbd.c defs.h command.h | |
cc -c kbd.c | |
command.o : command.c defs.h command.h | |
cc -c command.c | |
clean : | |
rm edit main.o kbd.o command.o display.o \ | |
insert.o search.o files.o utils.o |
edit : main.o kbd.o command.o | |
cc -o edit main.o kbd.o command.o | |
main.o : main.c defs.h | |
cc -c main.c | |
kbd.o : kbd.c defs.h command.h | |
cc -c kbd.c | |
command.o : command.c defs.h command.h | |
cc -c command.c | |
clean : | |
rm edit main.o kbd.o command.o display.o \ | |
insert.o search.o files.o utils.o |
-
创建每次执行时需要运行的命令的依赖树
-
如果运行
make edit
,那么main.o
,kbd.o
和command.o
将首先被编译,然后edit
在它们的基础上构建。
但是,您也可以使用它来转换像纯文本文件这样简单的东西:
.DEFAULT_GOAL := my-content.txt | |
my-content.txt: dependency-1.txt dependency-2.txt | |
cat dependency-1.txt dependency-2.txt > my-content.txt | |
dependency-1.txt: | |
echo -n "hello " > dependency-1.txt | |
dependency-2.txt: | |
echo "world" > dependency-2.txt | |
.PHONY: clean | |
clean: | |
rm dependency-1.txt dependency-2.txt my-content.txt |
.DEFAULT_GOAL := my-content.txt | |
my-content.txt: dependency-1.txt dependency-2.txt | |
cat dependency-1.txt dependency-2.txt > my-content.txt | |
dependency-1.txt: | |
echo -n "hello " > dependency-1.txt | |
dependency-2.txt: | |
echo "world" > dependency-2.txt | |
.PHONY: clean | |
clean: | |
rm dependency-1.txt dependency-2.txt my-content.txt |
.DEFAULT_GOAL := my-content.txt | |
my-content.txt: dependency-1.txt dependency-2.txt | |
cat dependency-1.txt dependency-2.txt > my-content.txt | |
dependency-1.txt: | |
echo -n "hello " > dependency-1.txt | |
dependency-2.txt: | |
echo "world" > dependency-2.txt | |
.PHONY: clean | |
clean: | |
rm dependency-1.txt dependency-2.txt my-content.txt |
在这种情况下,我们的(默认)目标是my-content.txt
,它是通过简单地连接两个依赖文件(动态创建)的输出来构建的。
我已经成功地将它应用于其他场景,例如Web 开发和移动应用开发。但它的使用方式没有任何限制。
误区二
它只是另一个任务运行器,NPM 脚本做同样的工作
这确实不对。是的,它确实运行任务(规则的命令),但不一定。我们用文本文件来举上面的例子。
第一次运行时make
,它会触发依赖项,然后触发主目标。所以,我们运行了一堆任务。但是,如果我们make
再次运行会发生什么?
什么也没发生,但是为什么呢?
事实证明, 的make
设计初衷是跟踪文件的修改日期dependency-1.txt
。在本例中,它检测到和 的修改时间自上次构建dependency-2.txt
以来没有变化。因此,无需重建。my-content.txt
my-content.txt
如果我们改变依赖项的内容会发生什么?
然后,make 足够智能,可以确定此时只需要执行第一条规则。
-
npm
这与脚本所做的事情不一样 -
使用 shell 脚本实现同样的功能需要比简单的
makefile
-
如果这 3 条规则每条都需要 30 秒运行,那么每次执行你都可以为自己节省一分钟
误区3
对于 Web 开发来说,这是一个过度的工具
如果你所做的只是调用webpack
,那么它就是。在其他情况下,可能根本不是。例如,一个包含样式、脚本和静态媒体库的简单网站,如下所示:
我们可能想要:
-
安装 NPM 依赖项
-
最小化 HTML 代码
-
转换 Typescript,打包并最小化
-
获取包含要由 Typescript 导入的数据的远程 JSON 文件
-
将 SASS 代码编译成 CSS 并打包
-
生成站点地图
-
优化图像和视频
-
ETC…
你可能正在考虑用一个简单的脚本来实现这个功能,运行几个命令就搞定了,对吧?好吧,你或许能建好网站,但代价是每次都要重新构建所有内容。
即使你只是修改了一个字符,你网站的视频也需要一次又一次地转码。即使你的样式相同,sass
每次启动都会重新加载。即使你有一个静态网站生成器,并且产品列表没有变化,你的整个应用程序也需要从头开始重建。
如果你注重速度和效率,那么 makemake
绝对是你的好帮手。但如果你只需要运行几个脚本,那么 make 就不是你想要的工具了。
使用 make 时发现的主要错误
如果你不花时间仔细阅读文档,它们可能很难理解。
像这样的 makefile 很常见:
.DEFAULT_TARGET: all | |
all: markup scripts styles media | |
markup: | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build | |
scripts: | |
./node_modules/.bin/tsc --build tsconfig.json | |
styles: | |
./node_modules/.bin/sass src/stylesheets/index.scss ./build/index.css | |
media: | |
cp -a ./src/media ./build/media | |
clean: | |
rm -Rf ./build/* |
.DEFAULT_TARGET: all | |
all: markup scripts styles media | |
markup: | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build | |
scripts: | |
./node_modules/.bin/tsc --build tsconfig.json | |
styles: | |
./node_modules/.bin/sass src/stylesheets/index.scss ./build/index.css | |
media: | |
cp -a ./src/media ./build/media | |
clean: | |
rm -Rf ./build/* |
.DEFAULT_TARGET: all | |
all: markup scripts styles media | |
markup: | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build | |
scripts: | |
./node_modules/.bin/tsc --build tsconfig.json | |
styles: | |
./node_modules/.bin/sass src/stylesheets/index.scss ./build/index.css | |
media: | |
cp -a ./src/media ./build/media | |
clean: | |
rm -Rf ./build/* |
典型的方法是将其视为makefile
任务/子任务树。当您运行 make all 时,所有依赖项都会被构建。
虽然这个示例最终可能能够正常工作,但主要问题是什么?
把规则当成一项简单的任务来使用
这更多的是一个概念上的问题,但规则是需要评估的,以决定是否需要构建目标。
然而,在上面的例子中,markdown:
它被用作“别名”,而不是防止无用计算的规则。
规则的依赖文件未声明
为了利用 make,markdown 规则应该(至少)写成如下形式:
# ... | |
markup: src/index.hbs src/partials/*hbs | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build | |
# more rules |
# ... | |
markup: src/index.hbs src/partials/*hbs | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build | |
# more rules |
# ... | |
markup: src/index.hbs src/partials/*hbs | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build | |
# more rules |
规则名称应该与实际输出文件绑定
使用抽象来all: markup scripts styles media
使代码简洁灵活是可以的。然而,间接目标应该始终链接到能够满足依赖关系的特定目标文件。
# ... | |
markup: build/index.html | |
build/index.html: src/index.hbs src/partials/*hbs | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build |
# ... | |
markup: build/index.html | |
build/index.html: src/index.hbs src/partials/*hbs | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build |
# ... | |
markup: build/index.html | |
build/index.html: src/index.hbs src/partials/*hbs | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build |
当这样定义时,依赖项和目标文件的修改日期会告诉 make 规则是否需要再次运行。
这些是您可以节省的几秒钟!
变量可以提供帮助
如果事先知道源文件列表,那么使用变量而不是每次都对依赖项进行硬编码不是很好吗?
MARKUP_FILES=$(wildcard src/index.hbs src/partials/*hbs) | |
# ... | |
markup: build/index.html | |
build/index.html: $(MARKUP_FILES) | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build |
MARKUP_FILES=$(wildcard src/index.hbs src/partials/*hbs) | |
# ... | |
markup: build/index.html | |
build/index.html: $(MARKUP_FILES) | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build |
MARKUP_FILES=$(wildcard src/index.hbs src/partials/*hbs) | |
# ... | |
markup: build/index.html | |
build/index.html: $(MARKUP_FILES) | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build |
注意,这里的$(MARKUP_FILES)
变量是用来定义依赖项的。但它也可以放在要执行的命令上:
STYLE_FILES=src/index.sass | |
# ... | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) | |
./node_modules/.bin/sass $(STYLE_FILES) ./build/stylesheets/index.css | |
STYLE_FILES=src/index.sass | |
# ... | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) | |
./node_modules/.bin/sass $(STYLE_FILES) ./build/stylesheets/index.css | |
STYLE_FILES=src/index.sass | |
# ... | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) | |
./node_modules/.bin/sass $(STYLE_FILES) ./build/stylesheets/index.css | |
看起来不错,但我们还可以做得更好。让我们也分解一下sass
可执行文件的路径:
STYLE_FILES=src/index.sass | |
SASS=./node_modules/.bin/sass | |
# ... | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) | |
$(SASS) $(STYLE_FILES) ./build/index.css | |
STYLE_FILES=src/index.sass | |
SASS=./node_modules/.bin/sass | |
# ... | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) | |
$(SASS) $(STYLE_FILES) ./build/index.css | |
STYLE_FILES=src/index.sass | |
SASS=./node_modules/.bin/sass | |
# ... | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) | |
$(SASS) $(STYLE_FILES) ./build/index.css | |
与 make 和 shell 变量混淆
在上面的例子中,请注意像 这样的变量$(STYLE_FILES)
是make变量,而不是 shell 变量。
对变量进行评估以生成精确的 shell 命令,然后执行该 shell 命令。
当编写如下命令时echo $(PWD)
:
-
make
将替换$(PWD)
为当前值(即/home/user
-
bash
然后将执行echo /home/user
这与运行 不同echo $$HOME
。在这种情况下:
-
make
将$$
取代$
-
bash
将执行echo $HOME
使用内置变量
还是基于同样的例子,我们可以改进规则。
想象一下,index.sass
内部导入了其他 sass 文件。我们如何将它们也声明为依赖项?
STYLE_FILES=$(wildcard src/index.sass src/styles/*.sass) | |
SASS=./node_modules/.bin/sass | |
# ... | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) | |
$(SASS) $< $@ |
STYLE_FILES=$(wildcard src/index.sass src/styles/*.sass) | |
SASS=./node_modules/.bin/sass | |
# ... | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) | |
$(SASS) $< $@ |
STYLE_FILES=$(wildcard src/index.sass src/styles/*.sass) | |
SASS=./node_modules/.bin/sass | |
# ... | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) | |
$(SASS) $< $@ |
好的,这个变化需要一点解释:
-
该
wildcard
关键字会计算 glob 的值,并将任何匹配的文件路径放入变量中。因此,我们的变量包含一个动态的源文件列表。 -
$@
被评估为目标的名称。在本例中,它是 的别名build/index.css
。我们可以使用此快捷方式,而不必重写自己的名称。 -
$<
被评估为规则的第一个依赖项。我们使用它是因为 sass 接受的是入口点,而不是整个列表。
在本例中,$<
评估为$(STYLE_FILES)
等于$(wildcard src/index.sass src/styles/*.sass)
。这相当于传递src/index.sass
-
如果 sass 获取了整个文件列表,那么我们会写
$(SASS) $^ $@
。
因此该命令$(SASS) $< $@
将转换为如下内容:./node_modules/.bin/sass src/index.sass build/index.css
确保目标文件夹也存在
如果我们按原样运行主目标,命令可能会抱怨构建文件夹不存在。
确保其存在的一个干净的方法是为该文件夹创建一个目标,并使目标在运行之前依赖于它。
.DEFAULT_TARGET: all | |
all: markup scripts styles media | |
build: | |
mkdir -p $@ | |
touch $@ | |
markup: build build/index.html | |
# ... |
.DEFAULT_TARGET: all | |
all: markup scripts styles media | |
build: | |
mkdir -p $@ | |
touch $@ | |
markup: build build/index.html | |
# ... |
.DEFAULT_TARGET: all | |
all: markup scripts styles media | |
build: | |
mkdir -p $@ | |
touch $@ | |
markup: build build/index.html | |
# ... |
标记将首先触发构建,然后触发构建build/index.html
。
我们也可以将它用于我们的 NPM 包。一种典型的方法是定义一个make init
静态操作,但是……如果可以自动化呢?
.DEFAULT_TARGET: all | |
all: node_modules markup scripts styles media | |
node_modules: package.json | |
npm install | |
touch $@ | |
markup: build build/index.html | |
# ... |
.DEFAULT_TARGET: all | |
all: node_modules markup scripts styles media | |
node_modules: package.json | |
npm install | |
touch $@ | |
markup: build build/index.html | |
# ... |
.DEFAULT_TARGET: all | |
all: node_modules markup scripts styles media | |
node_modules: package.json | |
npm install | |
touch $@ | |
markup: build build/index.html | |
# ... |
看看这个:
-
当
node_modules
不存在(目标)时,标尺将被触发。 -
当
package.json
发生变化(时间戳比 更新node_modules
)时,该规则也会触发。
将静态操作设置为 Phony
对于不依赖于任何先前状态的操作,应该使用特殊规则。通常,对于像 make clean 这样的操作,您希望无论当前工件是什么,都能触发该命令。
.DEFAULT_TARGET: all | |
all: markup scripts styles media | |
# ... | |
.PHONY: clean | |
clean: | |
rm -Rf ./build/* | |
.DEFAULT_TARGET: all | |
all: markup scripts styles media | |
# ... | |
.PHONY: clean | |
clean: | |
rm -Rf ./build/* | |
.DEFAULT_TARGET: all | |
all: markup scripts styles media | |
# ... | |
.PHONY: clean | |
clean: | |
rm -Rf ./build/* | |
设置.PHONY
确保如果匹配清洁规则,它将始终执行。
为什么我们需要这个?好吧,想象一下,如果在项目中意外创建了一个名为 clean 的文件。如果我们运行,会发生什么make clean
?嗯,我们会得到类似这样的信息:make:
clean' is up to date',你会想“没事,文件干净了”。
但这条消息实际上的意思是:目标文件 clean 已经存在,并且没有新的依赖项。所以,无需执行任何操作。
如果你设置.PHONY: clean
你确保clean
将始终运行rm -Rf ./build/*
makefile
这个例子的结尾是什么样的?
TSC=./node_modules/.bin/tsc | |
SASS=./node_modules/.bin/sass | |
MARKUP_FILES=$(wildcard src/index.hbs src/partials/*hbs) | |
STYLE_FILES=$(wildcard src/index.sass src/styles/*.sass) | |
SCRIPT_FILES=$(wildcard src/index.ts src/scripts/*.ts) | |
IMAGES=$(wildcard src/index.ts src/media/*.jpeg) | |
VIDEOS=$(wildcard src/index.ts src/media/*.mp4) | |
# ... | |
.DEFAULT_TARGET: all | |
all: build markup scripts styles media | |
build: | |
mkdir -p $@ | |
touch $@ | |
node_modules: package.json | |
npm install | |
touch $@ | |
markup: build/index.html | |
build/index.html: $(MARKUP_FILES) | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build | |
scripts: build/index.js | |
build:index.js: tsconfig.json $(SCRIPT_FILES) node_modules | |
$(TSC) --build $< | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) node_modules | |
$(SASS) $< $@ | |
media: images videos | |
images: $(IMAGES) | |
for image in $^; do \ | |
imagemin $$image > build/media/$$image ; \ | |
done | |
touch $@ | |
videos: $(VIDEOS) | |
for video in $^; do \ | |
HandBrakeCLI -i $$video -o build/media/$$video ; \ | |
done | |
touch $@ | |
.PHONY: clean | |
clean: | |
rm -Rf ./build/* |
TSC=./node_modules/.bin/tsc | |
SASS=./node_modules/.bin/sass | |
MARKUP_FILES=$(wildcard src/index.hbs src/partials/*hbs) | |
STYLE_FILES=$(wildcard src/index.sass src/styles/*.sass) | |
SCRIPT_FILES=$(wildcard src/index.ts src/scripts/*.ts) | |
IMAGES=$(wildcard src/index.ts src/media/*.jpeg) | |
VIDEOS=$(wildcard src/index.ts src/media/*.mp4) | |
# ... | |
.DEFAULT_TARGET: all | |
all: build markup scripts styles media | |
build: | |
mkdir -p $@ | |
touch $@ | |
node_modules: package.json | |
npm install | |
touch $@ | |
markup: build/index.html | |
build/index.html: $(MARKUP_FILES) | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build | |
scripts: build/index.js | |
build:index.js: tsconfig.json $(SCRIPT_FILES) node_modules | |
$(TSC) --build $< | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) node_modules | |
$(SASS) $< $@ | |
media: images videos | |
images: $(IMAGES) | |
for image in $^; do \ | |
imagemin $$image > build/media/$$image ; \ | |
done | |
touch $@ | |
videos: $(VIDEOS) | |
for video in $^; do \ | |
HandBrakeCLI -i $$video -o build/media/$$video ; \ | |
done | |
touch $@ | |
.PHONY: clean | |
clean: | |
rm -Rf ./build/* |
TSC=./node_modules/.bin/tsc | |
SASS=./node_modules/.bin/sass | |
MARKUP_FILES=$(wildcard src/index.hbs src/partials/*hbs) | |
STYLE_FILES=$(wildcard src/index.sass src/styles/*.sass) | |
SCRIPT_FILES=$(wildcard src/index.ts src/scripts/*.ts) | |
IMAGES=$(wildcard src/index.ts src/media/*.jpeg) | |
VIDEOS=$(wildcard src/index.ts src/media/*.mp4) | |
# ... | |
.DEFAULT_TARGET: all | |
all: build markup scripts styles media | |
build: | |
mkdir -p $@ | |
touch $@ | |
node_modules: package.json | |
npm install | |
touch $@ | |
markup: build/index.html | |
build/index.html: $(MARKUP_FILES) | |
hbs src/index.hbs --partial 'src/partials/*.hbs' -o ./build | |
scripts: build/index.js | |
build:index.js: tsconfig.json $(SCRIPT_FILES) node_modules | |
$(TSC) --build $< | |
styles: build/index.css | |
build/index.css: $(STYLE_FILES) node_modules | |
$(SASS) $< $@ | |
media: images videos | |
images: $(IMAGES) | |
for image in $^; do \ | |
imagemin $$image > build/media/$$image ; \ | |
done | |
touch $@ | |
videos: $(VIDEOS) | |
for video in $^; do \ | |
HandBrakeCLI -i $$video -o build/media/$$video ; \ | |
done | |
touch $@ | |
.PHONY: clean | |
clean: | |
rm -Rf ./build/* |
最后说一下:
-
以声明的方式思考 makefile ,而不是以命令的方式(有点像 ReactJS 组件)
-
将规则视为将某些输入转换为某些输出的语句,并且仅在源内容发生变化时运行
-
从末尾开始查找 makefile(目标文件,即使它们尚不存在),并将任何抽象规则绑定到特定的输出文件
今天的内容就到这里🎉🎊
我希望您觉得这篇文章很酷、很新鲜❄️🍦,可以向下滚动一点并点击拍手👏👏按钮😃。
未来还有更多精彩内容。如果您想持续关注,请随时关注Stack Me Up,下次我们还会为您带来类似的新文章。
到那时,请多保重!
鏂囩珷鏉ユ簮锛�https://dev.to/brickpop/modern-makefiles-used-the-right-way-in-2020-25mg