-
-
Save kxxoling/7597cf4a7650ea5a935c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Hello, and welcome to makefile basics. | |
# | |
# 这里我将向大家介绍 `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 将被重新构建。 | |
# | |
# | |
# Let's say that we were working on a project with 100 .c files, and each of | |
# those .c files we wanted to turn into a corresponding .o file, and then link | |
# all the .o files into a binary. (This is effectively the same if you have | |
# 100 .styl files to turn into .css files, and then link together into a big | |
# single concatenated main.min.css file.) | |
# | |
# It would be SUPER TEDIOUS to create a rule for each one of those. Luckily, | |
# make makes this easy for us. We can create one generic rule that handles | |
# any files matching a specific pattern, and declare that we're going to | |
# transform it into the corresponding file of a different pattern. | |
# | |
# Within the ruleset, we can use some special syntax to refer to the input | |
# file and the output file. Here are the special variables: | |
# | |
# $@ The file that is being made right now by this rule (aka the "target") | |
# You can remember this because it's like the "$@" list in a | |
# shell script. @ is like a letter "a" for "arguments. | |
# When you type "make foo", then "foo" is the argument. | |
# | |
# $< The input file (that is, the first prerequisite in the list) | |
# You can remember this becasue the < is like a file input | |
# pipe in bash. `head <foo.txt` is using the contents of | |
# foo.txt as the input. Also the < points INto the $ | |
# | |
# $^ This is the list of ALL input files, not just the first one. | |
# You can remember it because it's like $<, but turned up a notch. | |
# If a file shows up more than once in the input list for some reason, | |
# it's still only going to show one time in $^. | |
# | |
# $? All the input files that are newer than the target | |
# It's like a question. "Wait, why are you doing this? What | |
# files changed to make this necessary?" | |
# | |
# $$ A literal $ character inside of the rules section | |
# More dollar signs equals more cash money equals dollar sign. | |
# | |
# $* The "stem" part that matched in the rule definition's % bit | |
# You can remember this because in make rules, % is like * on | |
# the shell, so $* is telling you what matched the pattern. | |
# | |
# You can also use the special syntax $(@D) and $(@F) to refer to | |
# just the dir and file portions of $@, respectively. $(<D) and | |
# $(<F) work the same way on the $< variable. You can do the D/F | |
# trick on any variable that looks like a filename. | |
# | |
# There are a few other special variables, and we can define our own | |
# as well. Most of the other special variables, you'll never use, so | |
# don't worry about them. | |
# | |
# So, our rule for result.txt could've been written like this | |
# instead: | |
result-using-var.txt: source.txt | |
@echo "buildling result-using-var.txt using the $$< and $$@ vars" | |
cp $< $@ | |
# Let's say that we had 100 source files, that we want to convert | |
# into 100 result files. Rather than list them out one by one in the | |
# makefile, we can use a bit of shell scripting to generate them, and | |
# save them in a variable. | |
# | |
# Note that make uses := for assignment instead of = | |
# I don't know why that is. The sooner you accept that this isn't | |
# bash/sh, the better. | |
# | |
# Also, usually you'd use `$(wildcard src/*.txt)` instead, since | |
# probably the files would already exist in your project. Since this | |
# is a tutorial, though we're going to generate them using make. | |
# | |
# This will execute the shell program to generate a list of files. | |
srcfiles := $(shell echo src/{00..99}.txt) | |
# How do we make a text file in the src dir? | |
# We define the filename using a "stem" with the % as a placeholder. | |
# What this means is "any file named src/*.txt", and it puts whatever | |
# matches the "%" bit into the $* variable. | |
src/%.txt: | |
@# First things first, create the dir if it doesn't exist. | |
@# Prepend with @ because srsly who cares about dir creation | |
@[ -d src ] || mkdir src | |
@# then, we just echo some data into the file | |
@# The $* expands to the "stem" bit matched by % | |
@# So, we get a bunch of files with numeric names, containing their number | |
echo $* > $@ | |
# Try running `make src/00.txt` and `make src/01.txt` now. | |
# To not have to run make for each file, we define a "phony" target that | |
# depends on all of the srcfiles, and has no other rules. It's good | |
# practice to define your phony rules in a .PHONY declaration in the file. | |
# (See the .PHONY entry at the very bottom of this file.) | |
# | |
# Running `make source` will make ALL of the files in the src/ dir. Before | |
# it can make any of them, it'll first make the src/ dir itself. Then | |
# it'll copy the "stem" value (that is, the number in the filename matched | |
# by the %) into the file, like the rule says above. | |
# | |
# Try typing "make source" to make all this happen. | |
source: $(srcfiles) | |
# So, to make a dest file, let's copy a source file into its destination. | |
# Also, it has to create the destination folder first. | |
# | |
# The destination of any dest/*.txt file is the src/*.txt file with | |
# the matching stem. You could just as easily say that %.css depends | |
# on %.styl | |
dest/%.txt: src/%.txt | |
@[ -d dest ] || mkdir dest | |
cp $< $@ | |
# So, this is great and all, but we don't want to type `make dest/#.txt` | |
# 100 times! | |
# | |
# Let's create a "phony" target that depends on all the destination files. | |
# We can use the built-in pattern substitution "patsubst" so we don't have | |
# to re-build the list. This patsubst function uses the same "stem" | |
# concept explained above. | |
destfiles := $(patsubst src/%.txt,dest/%.txt,$(srcfiles)) | |
destination: $(destfiles) | |
# Since "destination" isn't an actual filename, we define that as a .PHONY | |
# as well (see below). This way, Make won't bother itself checking to see | |
# if the file named "destination" exists if we have something that depends | |
# on it later. | |
# | |
# Let's say that all of these dest files should be gathered up into a | |
# proper compiled program. Since this is a tutorial, we'll use the | |
# venerable feline compiler called "cat", which is included in every | |
# posix system because cats are wonderful and a core part of UNIX. | |
kitty: $(destfiles) | |
@# Remember, $< is the input file, but $^ is ALL the input files. | |
@# Cat them into the kitty. | |
cat $^ > kitty | |
# Note what's happening here: | |
# | |
# kitty -> (all of the dest files) | |
# Then, each destfile depends on a corresponding srcfile | |
# | |
# If you `make kitty` again, it'll say "kitty is up to date" | |
# | |
# NOW TIME FOR MAGIC! | |
# | |
# Let's update just ONE of the source files, and see what happens | |
# | |
# Run this: touch src/25.txt; make kitty | |
# | |
# Note that it is smart enough to re-build JUST the single destfile that | |
# corresponds to the 25.txt file, and then concats them all to kitty. It | |
# *doesn't* re-generate EVERY source file, and then EVERY dest file, | |
# every time | |
# It's good practice to have a `test` target, because people will come to | |
# your project, and if there's a Makefile, then they'll expect `make test` | |
# to do something. | |
# | |
# We can't test the kitty unless it exists, so we have to depend on that. | |
test: kitty | |
@echo "miao" && echo "tests all pass!" | |
# Last but not least, `make clean` should always remove all of the stuff | |
# that your makefile created, so that we can remove bad stuff if anything | |
# gets corrupted or otherwise screwed up. | |
clean: | |
rm -rf *.txt src dest kitty | |
# What happens if there's an error!? Let's say you're building stuff, and | |
# one of the commands fails. Make will abort and refuse to proceed if any | |
# of the commands exits with a non-zero error code. | |
# To demonstrate this, we'll use the `false` program, which just exits with | |
# a code of 1 and does nothing else. | |
badkitty: | |
$(MAKE) kitty # The special var $(MAKE) means "the make currently in use" | |
false # <-- this will fail | |
echo "should not get here" | |
.PHONY: source destination clean test badkitty | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment