Skip to content

Instantly share code, notes, and snippets.

@kxxoling
Forked from isaacs/Makefile
Last active August 29, 2015 14:17
Show Gist options
  • Save kxxoling/7597cf4a7650ea5a935c to your computer and use it in GitHub Desktop.
Save kxxoling/7597cf4a7650ea5a935c to your computer and use it in GitHub Desktop.
# 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