Skip to content

Instantly share code, notes, and snippets.

@kxxoling
Created March 1, 2018 16:45
Show Gist options
  • Save kxxoling/e8f2a34dd7c406620ca52072e1622636 to your computer and use it in GitHub Desktop.
Save kxxoling/e8f2a34dd7c406620ca52072e1622636 to your computer and use it in GitHub Desktop.
# 你好,欢迎学习 Makefile 基础!
#
# 这里我将向大家介绍 `make` 的优秀之处,虽然语法上有点“奇怪”,但却是一个高效、
# 快速、强大的程序构建解决方案。
#
# 学完了这里的基础之后,建议你前往 GNU 官网
# http://www.gnu.org/software/make/manual/make.html
# 更加深入 `make` 的用法。
# `make` 命令必须在一个存在 `Makefile` 文件的目录使用,当然,你也可以使用
# `make -f <makefile>` 来指定 `Makefile`。
#
# Makefile 种存储了一系列的规则,每一条规则都对应一条任务,类似于 grunt 中的
# task 或者 npm package.json 脚本。
#
# make 规则通常是这个样子的:
#
# <target>: <prerequisites...>
# <commands>
#
# `target` 是必须的,`prerequisites` 和 `command` 是可选,但是这两者必须存在一个。
#
# 输入 `make` 命令看看会出现什么:
tutorial:
@# todo: have this actually run some kind of tutorial wizard?
@echo "Please read the 'Makefile' file to go through this tutorial"
# 如果你不指定任务,默认会执行第一条任务,所以在本例中,`make` 和 `make tutorial` 是等价的。
#
# 默认情况下,make 会在运行一条任务前将其输出在控制台,让你清楚现在正在执行的
# 究竟是什么任务,这并不符合 UNIX “success should be silent” 理念,但如果不这样的话,
# 你将很难知道构建日志中究竟有什么。
#
# 我们在每一行行首的位置加上 @ 字符防止其输出。
#
# 命令列表中的每一行都是当作独立的 shell 命令执行,因此你在上一行定义的变量将
# 无法在下一行中取到。不相信的话,你可以执行 `make var-lost` 看看结果。
var-lost:
export foo=bar
echo "foo=[$$foo]"
# 注意:我们必须在命令行中使用 double-$ 。这是因为,每一行命令都是先作为 makefile 命令被读取,
# 然后才将其传向 shell。
# 想要在同样的环境下执行 shell 命令,我们可以使用 \n 换行符“连接”两行语句。
# 运行 `make var-kept` 看看跟上面的命令有什么不同。
var-kept:
export foo=bar; \
echo "foo=[$$foo]"
# 接下来,我们尝试根据一个文件来生成另一个文件。
# 比如,我们可以根据 "source.txt" 来创建一个 "result.txt"。
result.txt: source.txt
@echo "building result.txt from source.txt"
cp source.txt result.txt
# 运行 `make result.txt`,出错了!
# $ make result.txt
# make: *** No rule to make target `source.txt', needed by `result.txt'. Stop.
#
# 错误在于,我们告诉 make 根据 source.txt 来创建 result.txt,但是并没有告诉它如何
# 找到这个 source.txt,而 source.txt 现在并不存在于我们的目录树中。
#
# 将下面这组任务取消注释就能解决这个问题。
#
#source.txt:
# @echo "building source.txt"
# echo "this is the source" > source.txt
#
# 运行 `make result.txt` 你将发现它会先创建一个 source.txt 文件,再将其复制到 result.txt 中。
# 现在我们再一次运行 `make result.txt`,什么都不会发生!
# 这是因为 source.txt 并没有发生变化,因此没有必要重新构建 result.txt。
#
# 运行 `touch source.txt` 或者使用编辑器对它进行修改,这时在运行 `make result.txt`
# 你会发现 result.txt 将被重新构建。
#
#
# 假设我们现在的工作目录中有 100 个 .c 文件,而它们都需要被转换为 .o 文件,
# 最后将 .o 文件链接到二进制文件中。(或者你需要将 100 个 .styl 文件转换为 .css 文件
# 最后合并到一个 main.min.css 文件当中。)
#
# 如果手动为每一个文件创建一个任务,这将会无聊又麻烦。幸运的是,make 支持模式匹配,
# 可以批量处理符合模式的所有文件。
#
# 我们可以使用特殊的规则来匹配输入、输出文件的规则来设置任务,特殊变量如下:
#
# $@ 当前正在被 make 的文件(即”目标“)
# 记住这点,它和 shell 中 ”$@” 列一样。
# @ 符号和字母 a 很相似,它代表“argument/参数”的意思。
# 当你输入 make foo 的时候,“foo” 就是这个参数。
#
# $< 输入文件(即列表中的第一先决条件)
# 你可以把 < 当作 bash 中的管道输入符来记忆,`head <foo.txt` 使用 foo.txt 中的文件作为输入。
# < 也通用会将输入导向 $。
#
# $^ 它代表所有输入文件,而不仅仅是其中的第一个。
# 它和 $< 很像,不过是个上箭头。如果文件在输入列表中出现多次,它也仅仅会在 $^ 中出现一次。
#
# $? 所有比目标新的输入文件。
#
# $$ 文本中的普通 $ 符号,用于转义。
#
# $* 规则匹配的主干部分
# 在 make 规则中,% 类似于 shell 中的 *,想反 $* 则代表模式所代表的结果。
#
# 你还可以使用 $(@D) 以及 $(@F) 这样的特殊符号来分别代表当前目录及文件部分。
# $(<D) 及 $(<F) 和 $< 变量的使用方法一样。你可以在任何类似文件名的变量上使用
# 这种 D/F 技巧。
#
# 此外还有一些其它可用变量,不过大多书情况你并不需要依赖它们。
# 当然我们也可以自己定义变量。
#
# 因此 result.txt 的规则可以像下面这样改写:
result-using-var.txt: source.txt
@echo "buildling result-using-var.txt using the $$< and $$@ vars"
cp $< $@
# 假设现在有 100 多个原文件需要批量转换为目标文件,我们可以使用脚本
# 来转换文件,并使用一个变量存储下来,而不需要为每一个文件专门写一个任务。
#
# 注意赋值应该使用 := 而非 = ,这一点和 bash/sh 并不一样,我也不清楚具体原因,
# 不过你最好尽快习惯这一点.
#
# 通常你会使用 `$(wildcard src/*.txt)` 这样的通配符。
# 另外,你还需要注意目标文件是否已经存在于项目当中了,在这个示例我们使用
# make 来构建目标文件。
#
# 这行命令将会调用 shell 命令来声称特定文件:
srcfiles := $(shell echo src/{00..99}.txt)
# 如何在 src 目录创建一个文本文件?
# 我们使用 % 作为文件名主体的占位符,它代表 src/*.txt 匹配的所有文件,
# 并且会将 % 匹配到的部分传入 $* 变量。
src/%.txt:
@# 如果目录不存在则首先创建目录
@# 用 @ 隐藏掉,毕竟谁会关心 src 目录的创建啊
@[ -d src ] || mkdir src
@# 然后将数据 echo 到文件中
@# $* 会扩展为 % 匹配到的文件名主体
@# 这样我们就得到了一系列文件名为数字的 .txt 格式文件,并保存了对应数字作为文件内容。
echo $* > $@
# 现在运行 `make src/00.txt` 和 `make src/01.txt` 试试。
# 对于不需要进行构建的文件,我们可以各级所有的 srcfiles 使用 "phony" 标记,
# 通常建议在文件中声明 .PHONY 来定义 phony 规则。(见本文件底部)
#
# 现在运行 `make source` 即可构建 src/ 目录下的所有文件,但在这之前 `make source`
# 首先会生成 src/ 目录本身,然后将会将 "stem" 值(文件名中由 % 所匹配的数字部分)复制给文件。
#
# 运行 `make source` 来亲自试一试:
source: $(srcfiles)
# 接下来我们将源文件复制为目标文件,同样我们首先要创建目标目录。
#
# dest/*.txt 匹配的文件将对应 src/*.txt 文件,举个具体的例子:比如
# %.css 文件依赖于 %.styl 文件。
dest/%.txt: src/%.txt
@[ -d dest ] || mkdir dest
cp $< $@
# 很不错!但是我仍然不希望输入 `make dest/#.txt` 100 次。
#
# 我们可以根据目标文件来创建 "phony" 目标。
# 我们可以使用内置的模式替换 "patsubst" 来避免重新构建列表,patsubst 使用
# 和上面同样的 "stem" 规则。
destfiles := $(patsubst src/%.txt,dest/%.txt,$(srcfiles))
destination: $(destfiles)
# “destination” 并非真正的文件名,因此我们同样需要将他定义在 .PHONY 中。(如下)
# 这样 make 就不需要检测名为 ”destination“ 的文件是否存在了。
#
# 接下来所有目标文件都合并到一个文件中,在本例中我们使用古老的 UNIX 命令 cat。
kitty: $(destfiles)
@# 记住, $< 是输入文件, $^ 是所有的输出文件
@# 使用 cat 命令将它们合并到 kitty 中
cat $^ > kitty
# 注意,这里的过程:
#
# kitty -> (all of the dest files)
# 于是每一个目标文件都都对应上了一个源文件
#
# 如果你再次运行 `make kitty` ,将会得到 "kitty is up to date" 的输出。
#
# 接下来是见证奇迹的时刻!
#
# 现在我们对其中一个源文件稍作修改看看会发生什么
#
# 运行:`touch src/25.txt; make kitty`
#
# 注意:make 非常智能,只重新构建了修改过的 25.txt 这一个文件,然后将它们
# 合并到 kitty 中来,而不是每次都对所有的源文件进行构建操作。
# 记得编写 test 任务是一个好习惯,因为其他人在使用你的项目时可能会使用
# `make test` 来做一些事情。
#
# 如果 kitty 不存在则无法测试,也就是说 test 依赖于 kitty:
test: kitty
@echo "miao" && echo "tests all pass!"
# 最后,也很重要的是,实现 `make clean` 来清除 Makefile 所生成的所有文件。
clean:
rm -rf *.txt src dest kitty
# 出错了怎么办?假设你正在构建很多东西,其中一个命令执行失败了。
# 这时候 Make 会忽略返回非 0 错误代码的命令并退出。
# 这里我们使用 false 来故意触发错误(将返回错误代码 1)
badkitty:
$(MAKE) kitty # 特殊变量 $(MAKE) 表示“当前正在使用的 make”
false # <-- 这里会执行出错
echo "should not get here"
.PHONY: source destination clean test badkitty
@kxxoling
Copy link
Author

kxxoling commented Mar 1, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment