2020 年正确使用现代 makefile

2025-06-08

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
view raw makefile-1.make hosted with ❤ by GitHub
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
view raw makefile-1.make hosted with ❤ by GitHub
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
view raw makefile-1.make hosted with ❤ by GitHub
  • 创建每次执行时需要运行的命令的依赖树

  • 如果运行make edit,那么main.okbd.ocommand.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
view raw makefile-2.make hosted with ❤ by GitHub
.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
view raw makefile-2.make hosted with ❤ by GitHub
.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
view raw makefile-2.make hosted with ❤ by GitHub

在这种情况下,我们的(默认)目标是my-content.txt,它是通过简单地连接两个依赖文件(动态创建)的输出来构建的。

我已经成功地将它应用于其他场景,例如Web 开发移动应用开发。但它的使用方式没有任何限制。

误区二

它只是另一个任务运行器,NPM 脚本做同样的工作

这确实不对。是的,它确实运行任务(规则的命令),但不一定。我们用文本文件来举上面的例子。

第一次运行时make,它会触发依赖项,然后触发主目标。所以,我们运行了一堆任务。但是,如果我们make再次运行会发生什么?

什么也没发生,但是为什么呢?

事实证明, 的make设计初衷是跟踪文件的修改日期dependency-1.txt。在本例中,它检测到和 的修改时间自上次构建dependency-2.txt以来没有变化。因此,无需重建。my-content.txtmy-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,下次我们还会为您带来类似的新文章。

到那时,请多保重!

照片由 [Sorasak](https://unsplash.com/@boontohhgraphy?utm_source=medium&utm_medium=referral) 在 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral) 拍摄照片由SorasakUnsplash上拍摄

鏂囩珷鏉ユ簮锛�https://dev.to/brickpop/modern-makefiles-used-the-right-way-in-2020-25mg
PREV
最终像专业人士一样理解 JavaScript 闭包
NEXT
微服务架构到底是什么?